Structured locale leaves (non-English)
locales/en.json stays plain strings at every leaf — it is the source of truth for copy and key shape.
Each non-English file (locales/<lang>.json) uses structured leaves at the same dot paths:
json
{
"value": "Translated text",
"status": "translated",
"confidence": 0.88,
"needsReview": false,
"source": "google-translate",
"updatedAt": "2026-04-04T12:00:00.000Z"
}Fields
| Field | Type | Meaning |
|---|---|---|
value | string | The string passed to i18next at runtime (after stripping; see below). |
status | "pending" | "translated" | pending — synced from English or not yet machine-translated; translated — filled or generated with a completed translation step. |
confidence | number | null | Heuristic 0–1 from generate/fill (null until scored). |
needsReview | boolean | Flag for QA (e.g. low confidence or EN-identical output). |
source | string | How value was produced (see below). |
updatedAt | string (optional) | ISO-8601 when the leaf was last written by tooling. |
source values
| Value | When |
|---|---|
google-translate | Machine translation from generate/fill. |
copy-en | Verbatim English (preserve-as-en keys, or English copy from sync for new paths). |
sync-default | Reserved for future use when sync only patches metadata. |
manual | Hand-edited (convention for future editor flows). |
unknown | Legacy string wrapped by sync before meta was added. |
Runtime
src/i18n/index.ts loads locale JSON and passes bundles through stripLocaleBundleToStrings() so i18next only sees plain strings at leaves. Legacy files that still use bare strings are unchanged by the strip step.
Tooling
| Command | Role |
|---|---|
pnpm locales:sync | Aligns key shape to en.json, copies English into new paths as structured leaves, and merges missing meta keys on existing leaves. |
pnpm locales:generate / pnpm locales:fill | Writes value + meta after each translation API call. |
pnpm locales:review | Read-only aggregates over meta (no en.json comparison). |
pnpm locales:validate:quality | Parity vs en.json and English-identical detection (uses string values from leaves). |
Shared types and helpers live in apps/web/scripts/locales/shared/localeLeaf.ts.