Resolution and Fallbacks
How a key becomes a string: the resolver walks the locale's fallback chain through the active published bundles, picks plural and context variants, interpolates variables, and applies any live experiment. Every behavior on this page was reproduced against a live server.
Fallback chains
The chain for a request is built from the project's fallback policy:
the requested locale first, then fallback_policy.locale_overrides[locale]
if present or default_chain otherwise, then the base locale if not
already listed - deduplicated and truncated to the configured max depth
(5 by default).
With locale_overrides: { "fr-CA": ["fr-FR", "en-US"] } and no fr-CA
value for the key:
snug --output json i18n resolve -p $PROJECT -l fr-CA -k checkout.title{
"resolved": {
"key": "checkout.title",
"value": "Paiement",
"source_locale": "fr-FR",
"fallback_path": ["fr-CA", "fr-FR"],
"version": 2,
"rich_text_markdown": false,
"variant": null,
"experiment_id": null
}
}Each fallback-served resolve also increments the requested locale's
fallback_hits_24h coverage counter (visible in locale list).
Gotcha, verified live: locale add --fallback stores a chain on the
locale record and echoes it back in locale list, but resolution never
reads it - only the project fallback policy counts. In a project with a
default policy, fr-CA fell back straight to en-US ("Checkout") even
though the locale's own chain listed fr-FR first, where "Paiement" was
published. Per-locale chains must go in
fallback_policy.locale_overrides, set over HTTP at project creation
(see the README).
Plurals
plural_forms maps CLDR categories (zero, one, two, few, many,
other) to strings. --count picks the category - but it does not fill a
{count} placeholder; pass the variable too, or the resolve fails with
400 interpolation_error: missing value for variable 'count':
snug i18n resolve -p $PROJECT -l en-US -k inventory.item_count -c 5 --var count:5
# -> "5 items" (-c 1 --var count:1 -> "1 item")Variables
Variables are declared per entry at upload and enforced at resolve time: a
declared variable without a supplied value is 400 interpolation_error,
not a silent {name} left in the output.
snug i18n resolve -p $PROJECT -l en-US -k greeting.welcome --var name:Ada
# -> "Welcome back, Ada!"Context variants and tone
An entry can carry conditional variants and tone metadata:
{
"cta.continue": {
"value": "Continue",
"tone": { "formality": "neutral", "energy": "calm" },
"context_variants": [
{ "id": "urgent-flow", "conditions": { "journey": "checkout" },
"value": "Continue to payment" }
]
}
}snug i18n resolve -p $PROJECT -l en-US -k cta.continue --context journey:checkout
# -> "Continue to payment", variant "urgent-flow"; without --context -> "Continue"tone (formality, energy, warmth, audience, urgency, notes) is returned
verbatim with every resolution so agents can adapt phrasing to an approved
register instead of inventing copy.
A/B experiments
An experiment attaches weighted copy variants to one key:
{
"experiment_id": "checkout-cta",
"key": "checkout.title",
"variants": [
{ "experiment_id": "checkout-cta", "variant": "control", "weight": 50, "value": "Checkout" },
{ "experiment_id": "checkout-cta", "variant": "urgent", "weight": 50, "value": "Grab your cart!" }
]
}snug i18n experiment create -p $PROJECT -f experiment.json
snug i18n publish -p $PROJECT -l en-US # required - see below
snug i18n resolve -p $PROJECT -l en-US -k checkout.title -s adaBucketing hashes the experiment and subject together, so assignment is
deterministic: subject ada resolved variant urgent on every call while
linus always got control, with variant and experiment_id set in the
response. Without --subject-id the base value is returned, no variant.
Experiments take effect at the next publish. Creating one attaches it to the draft translation records, but resolution reads the published snapshot - until the key's locales are republished, resolve keeps returning the base value with no variant (verified live).
Batch resolution
snug i18n resolve-batch -p $PROJECT -l fr-CA -k checkout.title -k nav.missingUnresolvable keys land in a missing array instead of failing the request
(single-key resolve of a missing key is 404 key_not_found). Batches are
capped at 250 keys by default. The HTTP body also accepts shared
variables and context maps; the CLI currently exposes only --count
and --subject-id for batches, so keys that require variables need the
single-key command or raw HTTP.
Bundles
Export a locale's entire active bundle, for client-side caching keyed on the ETag:
snug --output json i18n bundle -p $PROJECT -l fr-CA --include-fallback{
"project_id": "uGGxgswBtPwxmMQHgAmE",
"locale": "fr-CA",
"version": 2,
"etag": "\"uGGxgswBtPwxmMQHgAmE:fr-CA:v2\"",
"published_at": "2026-08-28T04:36:01.643269Z",
"strings": {
"checkout.title": "Paiement",
"greeting.welcome": "Welcome back, {name}!",
"inventory.item_count": ""
}
}Plain export returns only the locale's own strings (empty for this fr-CA,
which publishes nothing of its own); --include-fallback merges values
down the fallback chain. Two caveats, both reproduced live: bundles are
flat key -> string maps, so a plural-only key exports as an empty string
(resolve plural keys at runtime instead), and drafts never appear in a
bundle until published.