I18n
A runtime registry for localized app copy: projects hold locales and versioned translation strings, translators upload and approve copy through their own workflow, and clients resolve strings - through fallback chains, CLDR plurals, variable interpolation, context variants, and A/B copy experiments - without an application redeploy.
When to reach for it: app and game UI strings, agent-facing copy with per-locale tone metadata, translator workflows where developers keep release control, copy A/B tests, missing-translation tracking.
When not to: typed, environment-scoped configuration values belong to Remote Config; arbitrary JSON key-value data belongs to the KV Store.
Concepts
- Project - a namespace for one product or surface. Holds the base locale, the fallback policy, translator roles, and the approval switch.
- Locale - a BCP 47 tag (
es-ES,fr-CA) registered per project. Invalid tags are rejected with400 invalid_locale. - Translations are versioned - uploads create drafts; publishing snapshots eligible strings into a new immutable per-locale version and moves the locale's active pointer. Resolution only sees active versions.
- The fallback policy lives on the project - a locale's chain comes
from the project's
fallback_policy, not from the locale record. See Resolution and fallbacks. - Namespaces are derived - the key segment before the first
.(nav.settings->nav) groups keys in the missing-key report.
Projects and locales
snug i18n project create --name checkout-app --base-locale en-US \
--approval-required --role studio_translator
snug i18n project list -q checkout
snug i18n locale add -p $PROJECT -l en-US -d "English (US)"
snug i18n locale add -p $PROJECT -l fr-FR -d "French (France)"
snug i18n locale list -p $PROJECTTwo things the CLI flow does not make obvious, both verified live:
- Add the base locale explicitly. Creating a project does not register
it - uploading to
en-USbeforelocale add en-USfails with404 locale_not_found. - The fallback policy is set at creation time, over HTTP. The CLI's
project createcannot set it and there is no project update endpoint, so a project needing per-locale chains likefr-CA -> fr-FR -> en-USmust be created with a raw request:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "checkout-app", "base_locale": "en-US", "approval_required": true,
"fallback_policy": {"mode": "recursive", "default_chain": ["en-US"],
"locale_overrides": {"fr-CA": ["fr-FR", "en-US"]}}}' \
http://localhost:4000/api/v1/i18n/projects{
"status": 201,
"msg": "Created",
"data": {
"project_id": "uGGxgswBtPwxmMQHgAmE",
"name": "checkout-app",
"base_locale": "en-US",
"fallback_policy": { "mode": "recursive", "default_chain": ["en-US"],
"locale_overrides": { "fr-CA": ["fr-FR", "en-US"] } },
"approval_required": true,
"active_version": 0,
"created_by": "docs4-translator",
...
}
}Uploading strings
upload takes a JSON file keyed by translation key; each entry can carry a
singular value, CLDR plural_forms, declared variables,
context_variants, tone metadata, and a rich_text_markdown flag:
{
"checkout.title": { "value": "Checkout" },
"greeting.welcome": { "value": "Welcome back, {name}!", "variables": ["name"] },
"inventory.item_count": { "plural_forms": { "one": "{count} item", "other": "{count} items" },
"variables": ["count"] }
}snug i18n upload -p $PROJECT -l en-US -f strings.jsonThree modes via --mode: upsert_draft (default) upserts key by key and
reports per-key failures in validation_errors while the valid keys still
save; replace deletes the locale's existing strings first; fail_all
rejects the whole upload with 400 upload_rejected if any key fails.
Values that look like credentials are rejected per key
("value appears to contain a secret") - verified with an
api_key=sk_live_... value; the clean key in the same file still saved.
Approve, publish, roll back
Uploads are drafts and invisible to resolution until published - resolving
a freshly uploaded key returns 404 key_not_found until a publish.
snug i18n approve -p $PROJECT -l es-ES # all draft/pending; -k for specific keys
snug i18n publish -p $PROJECT # all locales, or -l for one
snug i18n rollback -p $PROJECT -l es-ES -v 2{
"project_id": "uGGxgswBtPwxmMQHgAmE",
"active_version": 2,
"published_locales": [ { "locale": "fr-FR", "version": 2, "key_count": 2 },
{ "locale": "en-US", "version": 2, "key_count": 3 } ]
}Publishing reserves a new monotonic version per locale and atomically moves
that locale's active pointer; rollback moves the pointer back without
deleting anything. Verified: a key published in version 3 stopped resolving
after rolling back to version 2.
With --approval-required, only approved strings are eligible - but
publishing unapproved drafts does not fail: it succeeds and publishes
an empty version (key_count: 0), which becomes the active bundle.
Approve before you publish.
Resolving strings
snug i18n resolve -p $PROJECT -l fr-CA -k checkout.titleThe response carries the resolved value plus where it came from
(source_locale, fallback_path), the bundle version, tone metadata,
and the experiment variant if one applied. Fallback chains, plurals,
variables, context variants, experiments, and bundle export have their
own page: Resolution and fallbacks.
Missing-key telemetry
Clients report keys they could not resolve; the service aggregates counts with first/last seen per key, locale, and caller:
snug i18n telemetry -p $PROJECT -l fr-CA -k nav.settings --caller web -n nav
snug i18n report -p $PROJECT --missing -l fr-CA{
"events": [
{ "key": "nav.settings", "namespace": "nav", "locale": "fr-CA",
"caller": "web", "fallback_used": "en-US", "count": 2,
"first_seen_at": "2026-08-28T04:37:20.015204Z", ... }
]
}Coverage stats ride along on locale list: translated keys, missing keys,
and fallback_hits_24h, which ticks up whenever a resolve for that locale
was served by a fallback locale (verified live).
Access control
Verified live with three differently-privileged tokens:
- Create a project: the
translatorcapability or a platform admin token; a plain user gets403 insufficient_permissions. - Upload, locale management, missing report: platform admin, the
project creator, or a capability named in the project's
translator_roles. - Approve, publish, rollback, experiments: platform admin or project
creator only - a
translator_rolestoken that uploads fine gets403 admin or maintainer role required to publish. Translators ship copy; developers keep release control. - Resolve, batch resolve, bundles, telemetry: any authenticated user.
Limits and configuration
Translation values up to 8 KiB, 10000 keys per upload, 250 keys per batch
resolve, fallback depth 5, and 10 variants per key by default - all tunable
via the I18N_* variables in the
I18n CONFIG reference.
Reference
- I18n API - every endpoint, callable
- Resolution and fallbacks - chains, plurals, variables, experiments, bundles
- Related: Remote Config for typed config flags with environments and targeting, KV Store for arbitrary JSON storage