i18n — Internationalization Guide
OmniRoute supports 30 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
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 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:checkSource 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)
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
| Task | Command |
|---|---|
| Translate docs (LLM) | npm run i18n:run (preferred — incremental, hash-based) |
| Translate UI strings | node scripts/i18n/generate-multilang.mjs messages |
| Check translation drift | npm run i18n:check |
| 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
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(30 translations) - Framework:
next-intlwith cookie-based locale resolution - Config:
src/i18n/config.ts— defines all 30 locales, language names, flags
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
| Code | Language | RTL | Google Translate Code |
|---|---|---|---|
ar | العربية | Yes | ar |
bg | Български | No | bg |
cs | Čeština | No | cs |
da | Dansk | No | da |
de | Deutsch | No | de |
es | Español | No | es |
fi | Suomi | No | fi |
fr | Français | No | fr |
he | עברית | Yes | iw |
hi | हिन्दी | No | hi |
hu | Magyar | No | hu |
id | Bahasa Indonesia | No | id |
it | Italiano | No | it |
ja | 日本語 | No | ja |
ko | 한국어 | No | ko |
ms | Bahasa Melayu | No | ms |
nl | Nederlands | No | nl |
no | Norsk | No | no |
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 |
sk | Slovenčina | No | sk |
sv | Svenska | No | sv |
th | ไทย | No | th |
tr | Türkçe | No | tr |
uk-UA | Українська | No | uk |
vi | Tiếng Việt | No | vi |
zh-CN | 中文 (简体) | No | zh-CN |
Adding a New Language
1. Register the Locale
Edit src/i18n/config.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:
{
code: "xx",
googleTl: "xx",
label: "XX",
flag: "🏳️",
languageName: "Language Name",
readmeName: "Language Name",
docsName: "Language Name",
},3. Generate Initial Translation
node scripts/i18n/generate-multilang.mjs messagesThis 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
python3 scripts/i18n/validate_translation.py quick -l xx
python3 scripts/i18n/validate_translation.py diff common -l xx6. Generate Translated Documentation
node scripts/i18n/generate-multilang.mjs docsAuto-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.
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) - 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.
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 30 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)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
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
# 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 jsonThe saved preference is written atomically to ~/.omniroute/.env and is loaded by the
CLI bootstrap before any command runs.
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
42 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 29 locales have common + program keys translated.
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
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 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 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
Code-to-JSON key checker — scans src/**/*.tsx and src/**/*.ts for useTranslations() calls and verifies all referenced keys exist in en.json.
# 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 --fixgenerate-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
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 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.mjsDetects:
- 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.
{
"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.
CI Integration
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 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 completeFile Structure
src/i18n/
├── config.ts # Locale definitions (30 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
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
- 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
// 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 --verboseRTL 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
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.
⚠️ Audit (2026-05-13): The
docs/i18n/in/directory still exists on disk (full duplicate ofhi/). Translation generator no longer writes to it, but the historical tree was not pruned. Safe to delete withrm -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>