Scripts
Generate missing translations with Claude and audit unused keys.
The package has two maintenance scripts. Run them from the repo root with pnpm --filter:
pnpm --filter @workspace/i18n-messages generate-translations
pnpm --filter @workspace/i18n-messages audit-keysgenerate-translations
scripts/generate-translations.ts fills in missing keys in the player-facing namespaces by asking Claude to translate them from English.
The player runs with fallbackLng: false (see Consumption), so every key must exist in every language. Other namespaces (account, deals, deal-editor, errors, …) are still translated by hand.
Prerequisites
The script calls claude -p (Claude Code headless mode, --output-format json, model sonnet), so it uses your existing Claude Code login (claude auth status). It needs no ANTHROPIC_API_KEY, but the claude binary must be on your PATH.
Workflow
- Add or change keys in
<namespace>/en.json. - Run the generator. Without
--force, it only fills keys that are missing. - Commit
en.jsontogether with every generated locale file.
pnpm --filter @workspace/i18n-messages generate-translations --dry-run
pnpm --filter @workspace/i18n-messages generate-translations --namespace player --lang de,itOptions
| Flag | Default | Effect |
|---|---|---|
--namespace <ns[,ns…]> | player,bridge,game,common,billing | Namespaces to fill |
--lang <lang[,lang…]> | fr,de,es,ca,nl,pl,pt,el,sv,it | Target languages (en is dropped with a warning) |
--force | off | Re-translate keys that already exist. This overwrites them, so use it only for one-off corrections |
--dry-run | off | Translate and validate, but do not write files |
-h, --help | Print usage |
What it does
-
Reads
<namespace>/en.jsonas the reference shape. A missing target file counts as empty. -
Collects every leaf (a string or an array of strings) that exists in English but not in the target language (or every leaf, with
--force). -
Sends the leaves in batches of 50 keys, running up to 8 namespace × language pairs at a time. The system prompt is
scripts/translation-agent/system-prompt.md, and the prompt also includes the bridge glossaryscripts/translation-agent/bridge-glossary.md. -
Validates each batch:
- the keys match the request exactly (none missing, none extra)
{{placeholders}}match the source- no empty strings
- arrays keep their length
- no value just repeats its key path
If a batch fails validation, the script exits with a non-zero code.
-
Merges the result into the target file in the same key order as
en.json, with a 2-space indent and a trailing newline.
Adding a language to the player
- Add the code to
SUPPORTED_LANGUAGESinapps/player/src/lib/i18n/constants.ts(and tosrc/supported-languages.tshere). - Import and export the new locale in each namespace's
index.ts. - Create
<namespace>/<lang>.jsoncontaining{}. - Run the generator with
--lang <lang>and commit.
audit-keys
scripts/audit-unused-keys.ts lists translation keys that no product source uses, and can delete them from every locale file.
pnpm --filter @workspace/i18n-messages audit-keys # report, all namespaces
pnpm --filter @workspace/i18n-messages audit-keys --namespace deals,teaching
pnpm --filter @workspace/i18n-messages audit-keys --csv report.csv --json report.json
pnpm --filter @workspace/i18n-messages audit-keys --prune # delete unused keys in all locales
pnpm --filter @workspace/i18n-messages audit-keys --fail-on-unused # exit 1 if any remain (CI)Options
| Flag | Effect |
|---|---|
--namespace <ns[,ns…]> | Audit only these namespaces (default: every folder in src/locales) |
--json <file> | Write { namespace, total, used[], unused[] } per namespace |
--csv <file> | Write namespace,key,status rows |
--prune | Delete unused leaves from every <namespace>/*.json, remove parents left empty, and keep key order |
--fail-on-unused | Exit 1 when any unused key remains |
--no-ts | Skip the TypeScript property-reference detector |
--ts-project <tsconfig> | Project to scan for property references (can be repeated). Default: apps/web/tsconfig.json, packages/i18n-messages/tsconfig.package.json |
The report prints total / used / unused per namespace, then lists the unused keys.
How usage is detected
The audit runs two detectors. A key counts as used if either one finds it.
- String literals. It parses every
.ts/.tsx/.js/.jsx/.mjsfile underapps/andpackages/with the TypeScript parser and records every string literal:t("a.b"),"ns:a.b", key registries,i18nKey="a.b", and so on. For.mdxand.astrofiles it uses a regex scanner instead. A leaf is used when a literal matches its dotted path or its i18next plural base (x_one→x), either on its own or prefixed withnamespace:. - Typed property access. For consumers that read the JSON as an object (
dictionary.home.hero.titleinapps/web,rotationMessages[code].direction), the TypeScript checker traces member accesses,["key"]element accesses and destructuring back to the property in the locale JSON.
An ancestor key marks its whole subtree as used only when it is read as an object: t("faq.items", { returnObjects: true }) or a property reference. A bare literal such as "deals" does not count.
Excluded from the scan
node_modules, build outputs, __tests__, __mocks__, e2e, *.test.*, *.spec.*, *.gen.ts and *.d.ts files are not scanned. Neither is apps/docs: a key used only in a docs story is dead in the product. Locale JSON files are not scanned either, but the key registries next to them (locales/bridge/keys.ts) are.
Allowlist
The audit cannot see some consumers: a build-time plugin that reads the JSON from disk, or code that walks an untyped object. Declare the subtrees they use in scripts/audit-keys.allowlist.ts, with one entry per namespace and a comment naming the consumer. Each entry marks the whole subtree as used.
export const AUDIT_KEYS_ALLOWLIST: Record<string, readonly string[]> = {
player: [
// apps/player/vite/unsupported-page.ts reads `<lang>.json` from disk at
// build time and inlines `locale.unsupported` into unsupported.html.
"unsupported",
],
};Known blind spots
- A key built by string concatenation outside
t(). A Biome plugin guardst()itself, but not other code. Read the report before pruning. - A key read through an untyped object in a project not passed with
--ts-project, or read from outside TypeScript entirely. Add it to the allowlist.
Always run the report before --prune, and commit the pruned JSON so it can be reviewed.