i18n — Internationalization Guide
OmniRoute supports 67 languages with full dashboard UI translation, translated documentation, and RTL support for Arabic and Hebrew.
Translation pipeline (recommended — v3.8.0)
Section titled “Translation pipeline (recommended — v3.8.0)”OmniRoute uses a hash-based incremental translator for docs, backed by an
OpenAI-compatible LLM endpoint (typically cx/gpt-5.4-mini through OmniRoute
Cloud):
# Run translations (incremental — only touches changed sources)npm run i18n:run
# Limit to one localenpm run i18n:run -- --locale=pt-BR
# Specific files (comma-separated, repo-relative paths)npm run i18n:run -- --files=CLAUDE.md,docs/architecture/ARCHITECTURE.md
# Force retranslate everything (expensive)npm run i18n:run -- --force
# Preview what would happen (no API calls, no writes)npm run i18n:run:dry
# CI gate — exits non-zero if state is drifting (blocking in CI since 2026-09)npm run i18n:checkA PR that edits one of the 22 core docs must bring its mirrors along: npm run i18n:run -- --files=<that doc> retranslates only the ## sections whose text changed (section cache in
.i18n-state.json); for a mechanical edit (counts, links, language bars) npm run i18n:run -- --adopt --files=<that doc> records the new source hash without translating.
Re-bootstrapping the state. npm run i18n:run -- --adopt rebuilds
.i18n-state.json from the mirrors already on disk — no API calls, no writes to
any .md. Use it after a source edit that needs no retranslation (a link list,
a count) or when the state file was lost; add --files=… / --locale=… to
adopt a subset and --dry-run to preview.
Source of truth. config/i18n.json lists every locale (UI + docs) plus
the RTL set and the docsExcluded codes. The runtime config in
src/i18n/config.ts is a thin adapter over that JSON.
Backend. Configured via env (set in .env, never committed):
| Variable | Purpose |
|---|---|
OMNIROUTE_TRANSLATION_API_URL |
OpenAI-compatible base URL, e.g. …/v1 |
OMNIROUTE_TRANSLATION_API_KEY |
bearer token (kept out of logs) |
OMNIROUTE_TRANSLATION_MODEL |
model id, e.g. cx/gpt-5.4-mini |
OMNIROUTE_TRANSLATION_TIMEOUT_MS |
optional, default 60000 |
OMNIROUTE_TRANSLATION_CONCURRENCY |
optional, default 4 |
State tracking. .i18n-state.json (committed) keeps SHA-256 hashes per
source + per locale. Drift detection is automatic and deterministic — no API
calls in i18n:check.
Output shape. Each translated file gets a top-level # <heading> (<native>) line, a 🌐 Languages: … bar, an --- separator, and the
translated body. That layout matches what scripts/check/check-docs-sync.mjs
already enforces for llm.txt and CHANGELOG.md mirrors.
Legacy scripts (deprecated)
Section titled “Legacy scripts (deprecated)”The older Python script (scripts/i18n/i18n_autotranslate.py) and the
Google-Translate-backed generator (scripts/i18n/generate-multilang.mjs)
still exist with a deprecation banner. They will be removed in v3.10. Do not
use generate-multilang.mjs messages to translate UI strings — the supported
path is the LLM backend:
# fills every missing/placeholder UI key in every locale catalognpm run i18n:sync-ui -- --translate-markers --batch-size=40Only the readme mode (root README variants) has no replacement yet.
Quick Reference
Section titled “Quick Reference”| Task | Command |
|---|---|
| Add a language everywhere | npm run i18n:add-locale -- --code=el … (config → docs → CLI → site) |
| Translate docs (LLM) | npm run i18n:run (preferred — incremental, hash-based) |
| Translate UI strings | npm run i18n:sync-ui -- --translate-markers --batch-size=40 |
| Check translation drift | npm run i18n:check |
| Real-translation ratchet | npm run i18n:check-ratio (identical-to-English / placeholder %) |
| Regenerate 🌐 language bars | npm run i18n:sync-bars |
| Validate a locale | python3 scripts/i18n/validate_translation.py quick -l cs |
| Check code keys | python3 scripts/i18n/check_translations.py |
| Generate QA report | node scripts/i18n/generate-qa-checklist.mjs |
| Visual QA (Playwright) | node scripts/i18n/run-visual-qa.mjs |
Architecture
Section titled “Architecture”Source: diagrams/i18n-flow.mmd
Source of Truth
Section titled “Source of Truth”- UI strings:
src/i18n/messages/en.json(English source, ~14,000 keys) - Locale files:
src/i18n/messages/{locale}.json(all the translations) - Framework:
next-intlwith cookie-based locale resolution - Config:
config/i18n.json— defines all the locales, language names, flags
Runtime Flow
Section titled “Runtime Flow”- User selects language →
NEXT_LOCALEcookie set src/i18n/request.tsresolves locale: cookie →Accept-Languageheader → fallbacken- Dynamic import loads
messages/{locale}.json - Components use
useTranslations("namespace")andt("key")
Supported Locales
Section titled “Supported Locales”| Code | Language | RTL | Google Translate Code |
|---|---|---|---|
am |
አማርኛ | No | am |
ar |
العربية | Yes | ar |
az |
Azərbaycan dili | No | az |
bg |
Български | No | bg |
bn |
বাংলা | No | bn |
bs |
Bosanski | No | bs |
cs |
Čeština | No | cs |
da |
Dansk | No | da |
de |
Deutsch | No | de |
el |
Ελληνικά | No | el |
en |
English | No | en |
es |
Español | No | es |
et |
Eesti | No | et |
fa |
فارسی | Yes | fa |
fi |
Suomi | No | fi |
fr |
Français | No | fr |
ga |
Gaeilge | No | ga |
gu |
ગુજરાતી | No | gu |
ha |
Hausa | No | ha |
he |
עברית | Yes | iw |
hi |
हिन्दी | No | hi |
hr |
Hrvatski | No | hr |
hu |
Magyar | No | hu |
hy |
Հայերեն | No | hy |
id |
Bahasa Indonesia | No | id |
ig |
Igbo | No | ig |
it |
Italiano | No | it |
ja |
日本語 | No | ja |
ka |
ქართული | No | ka |
km |
ខ្មែរ | No | km |
kn |
ಕನ್ನಡ | No | kn |
ko |
한국어 | No | ko |
lt |
Lietuvių | No | lt |
lv |
Latviešu | No | lv |
ml |
മലയാളം | No | ml |
mr |
मराठी | No | mr |
ms |
Bahasa Melayu | No | ms |
mt |
Malti | No | mt |
my |
မြန်မာ | No | my |
ne |
नेपाली | No | ne |
nl |
Nederlands | No | nl |
no |
Norsk | No | no |
or |
ଓଡ଼ିଆ | No | or |
pa |
ਪੰਜਾਬੀ | No | pa |
phi |
Filipino | No | tl |
pl |
Polski | No | pl |
pt |
Português (Portugal) | No | pt |
pt-BR |
Português (Brasil) | No | pt |
ro |
Română | No | ro |
ru |
Русский | No | ru |
si |
සිංහල | No | si |
sk |
Slovenčina | No | sk |
sl |
Slovenščina | No | sl |
sr |
Српски | No | sr |
sv |
Svenska | No | sv |
sw |
Kiswahili | No | sw |
ta |
தமிழ் | No | ta |
te |
తెలుగు | No | te |
th |
ไทย | No | th |
tr |
Türkçe | No | tr |
uk-UA |
Українська | No | uk |
ur |
اردو | Yes | ur |
uz |
Oʻzbekcha | No | uz |
vi |
Tiếng Việt | No | vi |
yo |
Yorùbá | No | yo |
zh-CN |
中文 (简体) | No | zh-CN |
zh-TW |
中文 (繁體) | No | zh-TW |
Adding a New Language
Section titled “Adding a New Language”One command adds a locale to every surface (config, flag, dashboard catalog, docs mirrors, CLI catalog, README + indexes, language bars, and optionally the site):
# needs OMNIROUTE_TRANSLATION_API_URL / _API_KEY / _MODEL in .envnode scripts/i18n/add-locale.mjs --code=el --english=Greek --native=Ελληνικά --flag=🇬🇷# Indian languages share the in.svg flag file:node scripts/i18n/add-locale.mjs --code=kn --english=Kannada --native=ಕನ್ನಡ --flag=🇮🇳 --flag-file=in.svg# preview only:node scripts/i18n/add-locale.mjs --code=el --english=Greek --native=Ελληνικά --flag=🇬🇷 --dry-runThen validate:
node --import tsx/esm --test tests/unit/i18n-locale-surfaces-parity.test.tsnpm run i18n:check-ui-coverage && npm run i18n:check-ratio && npm run check:docs-all && npm run check:cli-i18nconfig/i18n.json is the only place a locale is declared; src/i18n/config.ts is a typed
adapter and must not be edited by hand. The Google-Translate generator
(generate-multilang.mjs) is deprecated and is not part of this flow.
Stale mirrors after an interrupted refresh
Section titled “Stale mirrors after an interrupted refresh”npm run i18n:check also lists stale targets: mirrors whose locale record in .i18n-state.json was written from an older version of the source than the top-level source_hash now records. That is what a refresh run interrupted halfway (an upstream quota 429, a killed runner) leaves behind — the source no longer reads as changed and the mirrors’ own hashes still match, so without this check the gate would say PASS. Stale targets are a warning, not a failure: rerun npm run i18n:run -- --locale=<code> --files=<sources> for the listed locales (or _artifacts-style batch runner) and the list empties.
Keeping catalogs complete and retranslating English copies
Section titled “Keeping catalogs complete and retranslating English copies”Three gates guard the catalogs, and they see different things:
| Gate | Sees |
|---|---|
npm run i18n:check-ui-coverage |
≥ 80 % of leaves translated per locale |
npm run i18n:check-new-keys |
a key the PR adds to en.json is translated in every locale — a __MISSING__: marker counts as absent (since 2026-09-17) |
npm run i18n:check-keys |
every locale carries exactly the key set of en.json, whatever the age |
npm run i18n:check-keys:cli |
the CLI catalogs carry exactly the key set of bin/cli/locales/en.json |
npm run i18n:check-ratio |
share of leaves still identical to English may only fall (ratchet) |
Translating the keys a branch adds (parallel runner)
Section titled “Translating the keys a branch adds (parallel runner)”A branch that adds or changes keys in en.json must translate them before its PR opens — the
new-key gate rejects __MISSING__: markers (on 2026-09-16 eight feature PRs shipped 61 keys as
markers into all 65 locales and the ratio gate went red on the release tip for everybody).
scripts/i18n/translate-new-keys.sh translates every locale in parallel and is safe to detach:
# pinned-name guard first: keys a test pins to English go in scripts/i18n/untranslatable-keys.jsonrg -n 'assert\.equal\(.*messages\.[a-zA-Z.]+, "' tests/unit# needs OMNIROUTE_TRANSLATION_API_URL / _API_KEY / _MODEL in .env — refuses to start otherwisenpm run i18n:translate-new-keys # all locales, 5 workersbash scripts/i18n/translate-new-keys.sh --locales=pt-BR,es # a subsetbash scripts/i18n/translate-new-keys.sh --catalog=cli --workers=3 # bin/cli/locales# detached: the shell returns at once; poll _artifacts/i18n-new-keys/batch.rc (0 = done)nohup setsid bash scripts/i18n/translate-new-keys.sh > _artifacts/i18n-new-keys/runner.out 2>&1 &Each locale gets three attempts of sync-ui-keys.mjs --translate-markers --batch-size=40;
per-locale logs and <code>.exit land in _artifacts/i18n-new-keys/, and the script exits
non-zero while any selected locale still carries a marker. Then
npm run i18n:check-keys && npm run i18n:check-ratio && npm run i18n:check-new-keys.
After every merge of the base into a locale branch, re-sync the locales the branch owns — the base keeps adding keys while a batch translates:
node scripts/i18n/sync-ui-keys.mjs --locale=km,kn,ml --translate-markers --batch-size=40npm run i18n:check-keysRetranslating verbatim-English leaves (--retranslate-identical) turns every leaf that is
still byte-identical to en.json — outside scripts/i18n/untranslatable-keys.json — into a
__MISSING__: placeholder and translates it in the same run. Before a bulk run, put every key
that a test pins to its English value (product, engine and flag names — e.g. the Vietnamese
sidebar engines in dashboard-localization-contract.test.ts, the pt-BR label in
server-owned-tool-loop-flag.test.ts) into the allowlist first, then:
node scripts/i18n/sync-ui-keys.mjs --locale=es --retranslate-identical --translate-markers --batch-size=40npm run i18n:check-ratio:update # tighten the baseline once the locale improvednpm run i18n:check-glossary # zh-CN / zh-TW / ko protected termsAuto-Translation Pipeline
Section titled “Auto-Translation Pipeline”generate-multilang.mjs (Google Translate)
Section titled “generate-multilang.mjs (Google Translate)”Primary auto-translation engine — uses Google Translate free API to generate translations for UI strings, READMEs, and documentation.
node scripts/i18n/generate-multilang.mjs [messages|readme|docs|all]| Mode | What it does |
|---|---|
messages |
Translates missing keys in src/i18n/messages/{locale}.json from en.json |
readme |
Translates README.md into all locales as README.{code}.md in project root |
docs |
Translates DOC_SOURCE_FILES into docs/i18n/{locale}/{docName} |
all |
Runs all three modes |
Features:
- Text protection: Masks code blocks (
```), inline code (`), markdown links/images ([text](url)), HTML tags, tables, and ICU placeholders ({count},{value},{total}, etc.) before translation, then restores them - Chunked batching: Joins multiple strings with
__OMNIROUTE_I18N_SEPARATOR__delimiters to minimize API calls (max 1800 chars per request) - In-memory cache: Avoids redundant API calls for repeated strings within a session
- Retry logic: Exponential backoff (up to 5 attempts with 300ms × attempt delay) for 429/5xx errors
- Timeout: 20 seconds per request
- Skip existing: If target file already exists, it is NOT overwritten
Important behaviors:
docs/i18n/README.mdis regenerated each run — it’s an auto-generated index of all docs- Root
README.{code}.mdfiles are only created if they don’t exist (skips locales inEXISTING_README_CODES)
i18n_autotranslate.py (LLM-based)
Section titled “i18n_autotranslate.py (LLM-based)”Secondary translator — uses any OpenAI-compatible LLM API (including OmniRoute itself) to translate existing docs/i18n/ markdown files. Best for polishing or re-translating docs with better quality than Google Translate.
python3 scripts/i18n/i18n_autotranslate.py \ --api-url http://localhost:20128/v1 \ --api-key sk-your-key \ --model gpt-4oFeatures:
- Scans
docs/i18n/markdown files for English paragraphs - Skips code blocks, tables, and already-translated content
- Sends paragraphs to LLM with technical translation system prompt
- Supports all 42 languages
CLI i18n
Section titled “CLI i18n”The omniroute CLI has its own i18n layer separate from the Next.js dashboard.
How it works
Section titled “How it works”- Every user-facing string in CLI commands goes through
t("module.key", vars)frombin/cli/i18n.mjs. - Catalogs are JSON files in
bin/cli/locales/— 42 ship out-of-the-box. - Locale falls back to
enfor any missing key, so partial translations are valid. - The source of truth for available locales is
config/i18n.json(shared with the dashboard).
Locale selection
Section titled “Locale selection”Detection order (first match wins):
| Priority | Source | Example |
|---|---|---|
| 1 | --lang flag |
omniroute --lang de status |
| 2 | OMNIROUTE_LANG env var |
OMNIROUTE_LANG=ja omniroute providers |
| 3 | LC_ALL system env |
auto-detected from terminal locale |
| 4 | LC_MESSAGES system env |
auto-detected from terminal locale |
| 5 | LANG system env |
auto-detected from terminal locale |
| 6 | Fallback | en |
Locale codes with underscores (pt_BR) are normalized to hyphen form (pt-BR).
Locale codes are validated against /^[a-zA-Z0-9-]+$/ — path traversal is rejected.
Saving a language preference
Section titled “Saving a language preference”# Set language and save to ~/.omniroute/.env (persists across sessions)omniroute config lang set pt-BR
# View current languageomniroute config lang get
# List all 42 available languagesomniroute config lang list
# JSON outputomniroute config lang list --output jsonThe saved preference is written atomically to ~/.omniroute/.env and is loaded by the
CLI bootstrap before any command runs.
One-time override
Section titled “One-time override”# Override for one command only (not persisted)omniroute --lang de providers listNote: the --lang flag does NOT write to the env file — it only affects the current
invocation. Use config lang set to persist.
Available locales
Section titled “Available locales”42 locale files ship in bin/cli/locales/. Full translations: en, pt-BR.
Scaffold-only (all keys fall back to en): bn, gu, he, mr, ms, phi, sw, ta, te, ur.
All other 30 locales have common + program keys translated.
Adding a new CLI locale
Section titled “Adding a new CLI locale”- Add the locale entry to
config/i18n.json. - Run
node bin/cli/scripts/generate-locales.mjs— creates the locale file. - Translate the keys (or leave as
{}for en-fallback scaffold). - PRs must add strings to
en.jsonandpt-BR.json; other files are best-effort.
Validation & QA
Section titled “Validation & QA”validate_translation.py
Section titled “validate_translation.py”Translation validator — compares any locale JSON against en.json and reports issues.
# Quick check (counts only)python3 scripts/i18n/validate_translation.py quick -l cs# Output:# Missing: 0# Untranslated: 0# Ignored (UNTRANSLATABLE_KEYS): 236
# Detailed diff by categorypython3 scripts/i18n/validate_translation.py diff common -l cspython3 scripts/i18n/validate_translation.py diff settings -l cs
# Export to CSVpython3 scripts/i18n/validate_translation.py csv -l cs > report.csv
# Export to Markdownpython3 scripts/i18n/validate_translation.py md -l cs > report.md
# Full report (default)python3 scripts/i18n/validate_translation.py -l csDetects:
- Missing keys — keys in
en.jsonbut not in locale file - Extra keys — keys in locale file but not in
en.json - Untranslated keys — keys where locale value equals English source (excluding allowlist)
- Placeholder mismatches — ICU placeholders that don’t match between source and translation
Exit codes:
| Code | Meaning |
|---|---|
| 0 | OK |
| 1 | Generic error |
| 2 | Missing strings (hard error) |
| 3 | Untranslated warning (soft) |
Environment: Set TRANSLATION_LANG=cs or use -l cs flag.
check_translations.py
Section titled “check_translations.py”Code-to-JSON key checker — scans src/**/*.tsx and src/**/*.ts for useTranslations() calls and verifies all referenced keys exist in en.json.
# Basic checkpython3 scripts/i18n/check_translations.py
# Verbose outputpython3 scripts/i18n/check_translations.py --verbose
# Auto-fix (adds missing keys to en.json)python3 scripts/i18n/check_translations.py --fixgenerate-qa-checklist.mjs
Section titled “generate-qa-checklist.mjs”Static analysis QA — scans Next.js page files for i18n risk metrics and generates a Markdown report.
node scripts/i18n/generate-qa-checklist.mjsChecks:
- Fixed-width class usage (overflow risk)
- Directional left/right classes (RTL risk)
- Clipping-prone patterns
- Locale parity (missing/extra keys vs
en.json) - README language selector bars in priority locales (
es,fr,de,ja,ar)
Output: docs/reports/i18n-qa-checklist-{date}.md
run-visual-qa.mjs
Section titled “run-visual-qa.mjs”Visual QA via Playwright — takes screenshots of all dashboard routes in multiple locales and viewports, then evaluates page health.
# Default: es, fr, de, ja, ar, zh-CN on localhost:20128node scripts/i18n/run-visual-qa.mjs
# Custom base URL and localesQA_BASE_URL=http://staging.example.com QA_LOCALES=de,fr node scripts/i18n/run-visual-qa.mjs
# Custom routesQA_ROUTES=/dashboard/settings,/dashboard/providers node scripts/i18n/run-visual-qa.mjsDetects:
- Text overflow
- Element clipping
- RTL layout mismatches
Output: docs/reports/i18n-visual-qa-{date}.md + JSON report
Managing Untranslatable Keys
Section titled “Managing Untranslatable Keys”untranslatable-keys.json
Section titled “untranslatable-keys.json”File: scripts/i18n/untranslatable-keys.json
Allowlist of keys that should remain identical to English source. Used by validate_translation.py to avoid false-positive “untranslated” warnings.
{ "description": "Keys that should remain untranslated...", "keys": [ "common.model", "common.oauth", "health.cpu", ... ]}What belongs here:
- Brand/product names:
landing.brandName,common.social-github - Technical terms/acronyms:
health.cpu,mcpDashboard.pid,settings.ai - ICU/format strings:
apiManager.modelsCount,health.millisecondsShort - Placeholder values:
providers.openaiBaseUrlPlaceholder,cliTools.baseUrlPlaceholder - Protocol names:
common.http,common.oauth,providers.oauth2Label - Navigation sections:
sidebar.primarySection,sidebar.cliSection
To add a key: Edit the keys array in scripts/i18n/untranslatable-keys.json and re-run validation.
Terminology Glossary
Section titled “Terminology Glossary”Beyond key parity (check-ui-keys-coverage.mjs) and ICU validity
(validate_translation.py), a maintained per-locale terminology layer
catches semantic drift that those two gates cannot see — e.g. the same
English concept (“provider”) rendered with two different, equally-valid
Chinese words (提供商 vs 提供者) across thousands of strings. #8038 added
this layer starting with zh-CN.
scripts/i18n/glossary/<locale>.json
Section titled “scripts/i18n/glossary/<locale>.json”Versioned glossary of recurring concepts (provider, connection, routing,
fallback, quota, context window, reasoning, tool call, cache, circuit
breaker…) for that locale. Each concept has a canonical translation and an
optional synonyms list — any synonym found in the catalog is flagged as
drift. A concept with an empty synonyms array is documented but not yet
enforced (the catalog still has legitimate mixed usage for it; normalizing
it is a follow-up, not this gate’s job).
scripts/i18n/glossary/zh-CN.json seeds the ten concepts above; provider
is the only one currently enforced, following the one-shot
提供商→提供者 normalization pass (mirroring #8024’s zh-TW pass) across
src/i18n/messages/zh-CN.json and bin/cli/locales/zh-CN.json.
scripts/i18n/glossary/protected-terms.json
Section titled “scripts/i18n/glossary/protected-terms.json”Flat list of product/provider/model/protocol/CLI/env/identifier names
(OmniRoute, OAuth, MCP, A2A, DATA_DIR, …) that must appear verbatim
inside any translated value. This is a different granularity than
untranslatable-keys.json (which excludes whole KEYS from the parity/ICU
checks) — protected terms are checked wherever they show up inside any
value, keyed by concept rather than by key path.
check-glossary-consistency.mjs
Section titled “check-glossary-consistency.mjs”# zh-CN by defaultnpm run i18n:check-glossary
# explicit locale, JSON report, or non-failing report modenode scripts/i18n/check-glossary-consistency.mjs --locale=zh-CNnode scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN --jsonnode scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN --reportExports a pure checkGlossaryConsistency(localeMessages, glossary, protectedTerms) function (mirroring evaluateFileSizes in
check-file-size.mjs) returning { violations: [...] } — a
glossary-synonym violation for a non-canonical term, or a
protected-term-altered violation for a mistranslated protected name.
Wired into CI as the i18n-glossary-zhcn job (same gating as
i18n-ui-coverage: skips draft PRs, runs when i18n or code changes).
CI Integration
Section titled “CI Integration”GitHub Actions (.github/workflows/ci.yml)
Section titled “GitHub Actions (.github/workflows/ci.yml)”The CI pipeline validates all locales on every push and PR:
i18n-matrixjob — dynamically discovers all locale files (excludingen.json)i18njob — runsvalidate_translation.py quick -l '<lang>'for each locale in parallelci-summaryjob — aggregates results into a dashboard summary
# i18n-matrix: discovers languagesLANGS=$(ls src/i18n/messages/*.json | xargs -n1 basename | sed 's/.json$//' | grep -v '^en$')
# i18n: validates each languagepython3 scripts/i18n/validate_translation.py quick -l '${{ matrix.lang }}'Dashboard output:
## 🌍 Translations| Metric | Value ||--------|------|| Languages checked | 30 || Total untranslated | 0 |
✅ All translations completeFile Structure
Section titled “File Structure”src/i18n/├── config.ts # Locale definitions (42 locales, RTL config)├── request.ts # Runtime locale resolution└── messages/ ├── en.json # Source of truth (~2800 keys) ├── cs.json # Czech translation ├── de.json # German translation └── ... # 30 locale files total
scripts/├── i18n/│ ├── generate-multilang.mjs # Auto-translation engine (Google Translate, 888 lines)│ ├── generate-qa-checklist.mjs # Static analysis QA│ ├── run-visual-qa.mjs # Playwright visual QA│ └── untranslatable-keys.json # Allowlist for validation (236 keys)├── validate_translation.py # Translation validator├── check_translations.py # Code-to-JSON key checker└── i18n_autotranslate.py # LLM-based doc translator
.github/workflows/└── ci.yml # i18n validation in CI matrix
docs/├── I18N.md # This file — i18n toolchain documentation├── i18n/│ ├── README.md # Auto-generated language index│ ├── cs/ # Czech docs│ │ └── docs/│ │ ├── I18N.md # Czech translation of this file│ │ └── ...│ ├── de/ # German docs│ └── ... # 30 locale directories└── reports/ ├── i18n-qa-checklist-*.md # Static analysis reports └── i18n-visual-qa-*.md # Visual QA reportsBest Practices
Section titled “Best Practices”When Editing Translations
Section titled “When Editing Translations”- Always edit
en.jsonfirst — it’s the source of truth - Run
generate-multilang.mjs messagesto propagate new keys to all locales - Review auto-translations — Google Translate is a starting point, not final
- Validate before committing —
python3 scripts/i18n/validate_translation.py quick -l <lang> - Update
untranslatable-keys.jsonif a key should remain in English
Placeholder Safety
Section titled “Placeholder Safety”- ICU placeholders (
{count},{value},{total},{seconds}) must be preserved exactly - Plural formats (
{count, plural, one {# model} other {# models}}) must maintain structure - The validator detects placeholder mismatches automatically
Adding New Translation Keys in Code
Section titled “Adding New Translation Keys in Code”// Use namespaced keysconst t = useTranslations("settings");t("cacheSettings"); // maps to settings.cacheSettings in JSON
// Run check_translations.py to verify keys existpython3 scripts/i18n/check_translations.py --verboseRTL Considerations
Section titled “RTL Considerations”- Arabic (
ar) and Hebrew (he) are RTL locales - Avoid hardcoded
left/rightCSS — usestart/endlogical properties - Visual QA catches RTL layout mismatches via
run-visual-qa.mjs
Known Issues & History
Section titled “Known Issues & History”in.json → hi.json Fix
Section titled “in.json → hi.json Fix”The generator originally used code: "in" (deprecated Google Translate code) for Hindi instead of the correct ISO 639-1 hi. This created an orphaned in.json duplicate of hi.json. Fixed by changing code: "in" to code: "hi" in generate-multilang.mjs and removing the orphaned file.
Retired (2026-09-02):
inoutlived that fix as a second Indonesian locale —config/i18n.jsoncalled it “Indonesian (Legacy)” while the README still labelled it Hindi. It was removed from every surface (config, catalogs,docs/i18n/in/, README, indexes, baselines).idnow declares"aliases": ["in"], so a savedNEXT_LOCALE=inorOMNIROUTE_LANG=inresolves toid.
docs/i18n/README.md Is Hand-Maintained
Section titled “docs/i18n/README.md Is Hand-Maintained”It used to be regenerated wholesale by generate-multilang.mjs docs. It no longer is: npm run i18n:add-locale inserts the new locale’s row (and updates the counts sentence) in place, and every other edit is made by hand. tests/unit/i18n-locale-surfaces-parity.test.ts guards both directions — a configured docs locale must have a row, and a row must map back to a configured docs locale.
External Untranslatable Keys List
Section titled “External Untranslatable Keys List”The untranslatable-keys.json allowlist was moved from an inline Python set in validate_translation.py to an external JSON file for easier maintenance. The validator loads it at runtime.
generate-multilang.mjs Hindi Code Fix
Section titled “generate-multilang.mjs Hindi Code Fix”The generator originally used code: "in" (deprecated Google Translate code) for Hindi instead of the correct ISO 639-1 hi. This was introduced in upstream commit 952b0b22c by diegosouzapw. Fixed by changing code: "in" to code: "hi" in the LOCALE_SPECS array and removing the orphaned in.json file.
validate_translation.py Ignored Count Output
Section titled “validate_translation.py Ignored Count Output”The quick check now displays the count of ignored keys from untranslatable-keys.json:
Missing: 0Untranslated: 0Ignored (UNTRANSLATABLE_KEYS): <varies per release>HagiCode
HagiCode is an agentic coding workspace: structured workflows, multi-agent execution, and Hero Dungeon views turn ideas into shipped software.
Turn ideas into polished, usable software with a smarter, faster, and more enjoyable agentic coding workflow.

- SmartStructured workflows turn intent into an executable path from idea to shipped change.
- EfficientMulti-agent workflows keep research, implementation, and review moving in parallel.
- FunHero Dungeon interfaces make long coding sessions visual, collaborative, and rewarding.