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.
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: 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.
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: 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.
