Skip to content

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 ada

Bucketing 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.missing

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