Skip to content

Console i18n: catalogs and the parity tool

The console reads web/messages/<locale>.json. en.json is the source of truth and the runtime fallback (app/lib/i18n.tsx resolves a key in the active locale, then in English). The 29 other locale files are machine-translated subsets and are not expected to be complete.

Adding strings

Put new strings in web/messages/en.json, in a new namespace for a new screen. Do not hand-edit the other locale files: npm run translate-messages (web/scripts/translate-messages.mjs) asks an LLM to translate the keys that are missing or stale in each locale.

Parity tool

web/scripts/i18n-parity.mjs (npm: i18n:parity, i18n:parity:check).

Command Effect
node scripts/i18n-parity.mjs per locale: coverage (share of en.json leaf keys present), missing count, dead keys
... --prune removes locale keys that no longer exist in en.json; namespaces left empty are dropped; formatting and key order are preserved (files are JSON.stringify(_, null, 2) plus a newline)
... --check ratchet against messages/i18n-parity-baseline.json (no workflow under .github/workflows runs it yet; run it before a change that touches messages/)
... --update rewrites the baseline; refuses to loosen it unless --force

A "dead" key is a leaf in a locale file with no counterpart in en.json (left behind when English keys are renamed or removed). Arrays count as leaves.

Ratchet rules

  • Coverage per locale may only go up (tolerance 0.01 percentage points).
  • Dead keys per locale may only go down. After renaming or deleting English keys, run --prune.
  • Adding English keys lowers every locale's coverage. When that exceeds the tolerance, either translate or re-baseline deliberately with --update --force in the same change, saying so in the commit message.

Numbers (2026-10-15)

Before the first prune each locale carried 6,855 to 7,138 dead keys (202,300 in total) against 12,780 English keys; coverage was 53.0% to 55.0%. After: 0 dead keys in all 29 locales, coverage unchanged (the pruned keys were never read), about 202k lines removed from the locale files.

Agent-control screens: every key in every locale

The runtime settings, settings catalog copy, approvals queue, prompt registry, turn timeline, eval controls, agent Behavior tab and "Where do I control...?" screens must never show a raw key in any language. web/scripts/ui-i18n.mjs keeps that true without waiting for translators:

  • node scripts/ui-i18n.mjs --check fails when a locale file lacks a key of those screens (the namespaces in UI_NAMESPACES, plus any key added to another namespace since BASELINE_REF). web/app/lib/uiI18n.test.ts runs the same check in vitest.
  • --fill adds every missing key to every locale with the English text as the fallback, touching nothing else.
  • --todo rewrites ui-translation-todo.md: the keys (with their English text) that still need a human translation, and a per-locale count.

When you add a message to one of those namespaces: add it to en.json, run --fill, then --todo. A setting's copy (settings.<key>.label|description|help) is also checked against the real catalog by web/app/admin/runtime-settings/settingsCoverage.test.tsx.