Back to Omniroute

i18n — Internationalization Guide

docs/guides/I18N.md

3.8.4926.0 KB
Original Source

i18n — Internationalization Guide

OmniRoute supports 43 languages with full dashboard UI translation, translated documentation, and RTL support for Arabic and Hebrew.

🌐 Languages: 🇺🇸 English | 🇧🇷 Português (Brasil) | 🇪🇸 Español | 🇫🇷 Français | 🇩🇪 Deutsch | 🇮🇹 Italiano | 🇷🇺 Русский | 🇨🇳 中文 (简体) | 🇯🇵 日本語 | 🇰🇷 한국어 | 🇸🇦 العربية | 🇮🇳 हिन्दी | 🇹🇭 ไทย | 🇹🇷 Türkçe | 🇺🇦 Українська | 🇻🇳 Tiếng Việt | 🇧🇬 Български | 🇩🇰 Dansk | 🇫🇮 Suomi | 🇮🇱 עברית | 🇭🇺 Magyar | 🇮🇩 Bahasa Indonesia | 🇲🇾 Bahasa Melayu | 🇳🇱 Nederlands | 🇳🇴 Norsk | 🇵🇹 Português (Portugal) | 🇷🇴 Română | 🇵🇱 Polski | 🇸🇰 Slovenčina | 🇸🇪 Svenska | 🇵🇭 Filipino | 🇨🇿 Čeština

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):

bash
# Run translations (incremental — only touches changed sources)
npm run i18n:run

# Limit to one locale
npm 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
npm run i18n:check

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):

VariablePurpose
OMNIROUTE_TRANSLATION_API_URLOpenAI-compatible base URL, e.g. …/v1
OMNIROUTE_TRANSLATION_API_KEYbearer token (kept out of logs)
OMNIROUTE_TRANSLATION_MODELmodel id, e.g. cx/gpt-5.4-mini
OMNIROUTE_TRANSLATION_TIMEOUT_MSoptional, default 60000
OMNIROUTE_TRANSLATION_CONCURRENCYoptional, 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)

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. The messages and readme modes of generate-multilang.mjs (UI strings + root README variants) are not yet handled by the new pipeline and are still used.

Quick Reference

TaskCommand
Translate docs (LLM)npm run i18n:run (preferred — incremental, hash-based)
Translate UI stringsnode scripts/i18n/generate-multilang.mjs messages
Check translation driftnpm run i18n:check
Validate a localepython3 scripts/i18n/validate_translation.py quick -l cs
Check code keyspython3 scripts/i18n/check_translations.py
Generate QA reportnode scripts/i18n/generate-qa-checklist.mjs
Visual QA (Playwright)node scripts/i18n/run-visual-qa.mjs

Architecture

Source: diagrams/i18n-flow.mmd

Source of Truth

  • UI strings: src/i18n/messages/en.json (English source, ~2800 keys)
  • Locale files: src/i18n/messages/{locale}.json (43 translations)
  • Framework: next-intl with cookie-based locale resolution
  • Config: src/i18n/config.ts — defines all 43 locales, language names, flags

Runtime Flow

  1. User selects language → NEXT_LOCALE cookie set
  2. src/i18n/request.ts resolves locale: cookie → Accept-Language header → fallback en
  3. Dynamic import loads messages/{locale}.json
  4. Components use useTranslations("namespace") and t("key")

Supported Locales

CodeLanguageRTLGoogle Translate Code
arالعربيةYesar
bgБългарскиNobg
csČeštinaNocs
daDanskNoda
deDeutschNode
esEspañolNoes
fiSuomiNofi
frFrançaisNofr
heעבריתYesiw
hiहिन्दीNohi
huMagyarNohu
idBahasa IndonesiaNoid
itItalianoNoit
ja日本語Noja
ko한국어Noko
msBahasa MelayuNoms
nlNederlandsNonl
noNorskNono
phiFilipinoNotl
plPolskiNopl
ptPortuguês (Portugal)Nopt
pt-BRPortuguês (Brasil)Nopt
roRomânăNoro
ruРусскийNoru
skSlovenčinaNosk
svSvenskaNosv
thไทยNoth
trTürkçeNotr
uk-UAУкраїнськаNouk
viTiếng ViệtNovi
zh-CN中文 (简体)Nozh-CN
zh-TW中文 (繁體)Nozh-TW

Adding a New Language

1. Register the Locale

Edit src/i18n/config.ts:

ts
// Add to LOCALES array
"xx",
// Add to LANGUAGES array
{ code: "xx", label: "XX", name: "Language Name", flag: "🏳️" },

2. Add to Generator

Edit scripts/i18n/generate-multilang.mjs — add entry to LOCALE_SPECS:

js
{
  code: "xx",
  googleTl: "xx",
  label: "XX",
  flag: "🏳️",
  languageName: "Language Name",
  readmeName: "Language Name",
  docsName: "Language Name",
},

3. Generate Initial Translation

bash
node scripts/i18n/generate-multilang.mjs messages

This creates src/i18n/messages/xx.json auto-translated from en.json via Google Translate.

4. Review & Fix Auto-Translations

Auto-translations are a starting point. Review manually for:

  • Technical accuracy
  • Context-appropriate terminology
  • Proper handling of placeholders ({count}, {value}, etc.)

5. Validate

bash
python3 scripts/i18n/validate_translation.py quick -l xx
python3 scripts/i18n/validate_translation.py diff common -l xx

6. Generate Translated Documentation

bash
node scripts/i18n/generate-multilang.mjs docs

Auto-Translation Pipeline

generate-multilang.mjs (Google Translate)

Primary auto-translation engine — uses Google Translate free API to generate translations for UI strings, READMEs, and documentation.

bash
node scripts/i18n/generate-multilang.mjs [messages|readme|docs|all]
ModeWhat it does
messagesTranslates missing keys in src/i18n/messages/{locale}.json from en.json
readmeTranslates README.md into all locales as README.{code}.md in project root
docsTranslates DOC_SOURCE_FILES into docs/i18n/{locale}/{docName}
allRuns 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.md is regenerated each run — it's an auto-generated index of all docs
  • Root README.{code}.md files are only created if they don't exist (skips locales in EXISTING_README_CODES)
  • Language bars (🌐 **Languages:** ...) are automatically inserted/updated in all translated docs

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.

bash
python3 scripts/i18n/i18n_autotranslate.py \
  --api-url http://localhost:20128/v1 \
  --api-key sk-your-key \
  --model gpt-4o

Features:

  • 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 43 languages

CLI i18n

The omniroute CLI has its own i18n layer separate from the Next.js dashboard.

How it works

  • Every user-facing string in CLI commands goes through t("module.key", vars) from bin/cli/i18n.mjs.
  • Catalogs are JSON files in bin/cli/locales/ — 43 ship out-of-the-box.
  • Locale falls back to en for 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

Detection order (first match wins):

PrioritySourceExample
1--lang flagomniroute --lang de status
2OMNIROUTE_LANG env varOMNIROUTE_LANG=ja omniroute providers
3LC_ALL system envauto-detected from terminal locale
4LC_MESSAGES system envauto-detected from terminal locale
5LANG system envauto-detected from terminal locale
6Fallbacken

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

bash
# Set language and save to ~/.omniroute/.env (persists across sessions)
omniroute config lang set pt-BR

# View current language
omniroute config lang get

# List all 42 available languages
omniroute config lang list

# JSON output
omniroute config lang list --output json

The saved preference is written atomically to ~/.omniroute/.env and is loaded by the CLI bootstrap before any command runs.

One-time override

bash
# Override for one command only (not persisted)
omniroute --lang de providers list

Note: the --lang flag does NOT write to the env file — it only affects the current invocation. Use config lang set to persist.

Available locales

43 locale files ship in bin/cli/locales/. Full translations: en, pt-BR. Scaffold-only (all keys fall back to en): bn, gu, he, in, mr, ms, phi, sw, ta, te, ur. All other 30 locales have common + program keys translated.

Adding a new CLI locale

  1. Add the locale entry to config/i18n.json.
  2. Run node bin/cli/scripts/generate-locales.mjs — creates the locale file.
  3. Translate the keys (or leave as {} for en-fallback scaffold).
  4. PRs must add strings to en.json and pt-BR.json; other files are best-effort.

Validation & QA

validate_translation.py

Translation validator — compares any locale JSON against en.json and reports issues.

bash
# 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 category
python3 scripts/i18n/validate_translation.py diff common -l cs
python3 scripts/i18n/validate_translation.py diff settings -l cs

# Export to CSV
python3 scripts/i18n/validate_translation.py csv -l cs > report.csv

# Export to Markdown
python3 scripts/i18n/validate_translation.py md -l cs > report.md

# Full report (default)
python3 scripts/i18n/validate_translation.py -l cs

Detects:

  • Missing keys — keys in en.json but 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:

CodeMeaning
0OK
1Generic error
2Missing strings (hard error)
3Untranslated warning (soft)

Environment: Set TRANSLATION_LANG=cs or use -l cs flag.

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.

bash
# Basic check
python3 scripts/i18n/check_translations.py

# Verbose output
python3 scripts/i18n/check_translations.py --verbose

# Auto-fix (adds missing keys to en.json)
python3 scripts/i18n/check_translations.py --fix

generate-qa-checklist.mjs

Static analysis QA — scans Next.js page files for i18n risk metrics and generates a Markdown report.

bash
node scripts/i18n/generate-qa-checklist.mjs

Checks:

  • 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

Visual QA via Playwright — takes screenshots of all dashboard routes in multiple locales and viewports, then evaluates page health.

bash
# Default: es, fr, de, ja, ar, zh-CN on localhost:20128
node scripts/i18n/run-visual-qa.mjs

# Custom base URL and locales
QA_BASE_URL=http://staging.example.com QA_LOCALES=de,fr node scripts/i18n/run-visual-qa.mjs

# Custom routes
QA_ROUTES=/dashboard/settings,/dashboard/providers node scripts/i18n/run-visual-qa.mjs

Detects:

  • Text overflow
  • Element clipping
  • RTL layout mismatches

Output: docs/reports/i18n-visual-qa-{date}.md + JSON report

Managing Untranslatable Keys

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.

json
{
  "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

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

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

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

bash
# zh-CN by default
npm run i18n:check-glossary

# explicit locale, JSON report, or non-failing report mode
node scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN
node scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN --json
node scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN --report

Exports 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

GitHub Actions (.github/workflows/ci.yml)

The CI pipeline validates all locales on every push and PR:

  1. i18n-matrix job — dynamically discovers all locale files (excluding en.json)
  2. i18n job — runs validate_translation.py quick -l '<lang>' for each locale in parallel
  3. ci-summary job — aggregates results into a dashboard summary
yaml
# i18n-matrix: discovers languages
LANGS=$(ls src/i18n/messages/*.json | xargs -n1 basename | sed 's/.json$//' | grep -v '^en$')

# i18n: validates each language
python3 scripts/i18n/validate_translation.py quick -l '${{ matrix.lang }}'

Dashboard output:

## 🌍 Translations
| Metric | Value |
|--------|------|
| Languages checked | 30 |
| Total untranslated | 0 |

✅ All translations complete

File Structure

src/i18n/
├── config.ts                    # Locale definitions (43 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 reports

Best Practices

When Editing Translations

  1. Always edit en.json first — it's the source of truth
  2. Run generate-multilang.mjs messages to propagate new keys to all locales
  3. Review auto-translations — Google Translate is a starting point, not final
  4. Validate before committingpython3 scripts/i18n/validate_translation.py quick -l <lang>
  5. Update untranslatable-keys.json if a key should remain in English

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

tsx
// Use namespaced keys
const t = useTranslations("settings");
t("cacheSettings"); // maps to settings.cacheSettings in JSON

// Run check_translations.py to verify keys exist
python3 scripts/i18n/check_translations.py --verbose

RTL Considerations

  • Arabic (ar) and Hebrew (he) are RTL locales
  • Avoid hardcoded left/right CSS — use start/end logical properties
  • Visual QA catches RTL layout mismatches via run-visual-qa.mjs

Known Issues & History

in.jsonhi.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.

⚠️ Audit (2026-05-13): The docs/i18n/in/ directory still exists on disk (full duplicate of hi/). Translation generator no longer writes to it, but the historical tree was not pruned. Safe to delete with rm -rf docs/i18n/in/ after confirming no external links reference the old path.

docs/i18n/README.md Is Auto-Generated

The docs/i18n/README.md file is completely regenerated by generate-multilang.mjs docs. Any manual edits will be lost. Use docs/guides/I18N.md (this file) for hand-written documentation that should persist.

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

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

The quick check now displays the count of ignored keys from untranslatable-keys.json:

Missing: 0
Untranslated: 0
Ignored (UNTRANSLATABLE_KEYS): <varies per release>