This document explains how Claude Terminal handles internationalization (i18n), how to add a new language, and how the automated coverage badges work.
- Locale file format
- Adding a new language
- Running the coverage check locally
- Automated badge generation
- Coverage thresholds
All locale files live in:
src/renderer/i18n/locales/
├── en.json ← reference/base (always 100 %)
├── fr.json ← French translation
├── es.json ← Spanish translation
├── id.json ← Indonesian translation
└── zh-CN.json ← Simplified Chinese translation
Each file is a nested JSON object where leaf values are translated strings.
Interpolation variables are wrapped in curly braces: {variable}.
Rules:
- The structure (nesting, key names) must mirror
en.jsonexactly. - Variable names inside
{...}must not be translated. - Do not remove or rename top-level namespace keys (e.g.,
common,git). - Extra keys that don't exist in
en.jsonare allowed but will be reported by the coverage checker.
-
Copy the base locale:
cp src/renderer/i18n/locales/en.json src/renderer/i18n/locales/<lang>.json
Replace
<lang>with the ISO 639-1 code (e.g.,de,es,ja,pt).Use a plain code unless the language genuinely needs different translations per region. When it does, use the full BCP 47 tag with an uppercase region (
zh-CN,zh-TW,pt-BR): the loader tries the exact tag before the bare primary subtag, so both forms auto-detect correctly. Do not register a barezhalongsidezh-CNexpecting them to be interchangeable, they are two separate locale files. -
Translate the values (not the keys) in the new file.
-
Verify coverage:
node scripts/check-i18n.js --locale=<lang>
-
Register the new locale in the i18n loader:
- Open
src/renderer/i18n/index.jsand add the code toSUPPORTED_LANGUAGESplus its display name toLANGUAGE_NAMES.
- Open
-
Ship the locale file with the build — add the code to
LAZY_LOCALESinscripts/build-renderer.js:const LAZY_LOCALES = ['fr', 'es', 'id', '<lang>'];
Only
en.jsonis bundled intorenderer.bundle.js; every other locale is read at runtime fromdist/locales/, which this list populates. Skipping this step is silent: the language shows up in the picker and is persisted, but the UI keeps rendering English, and the Jest suite still passes because it loads locales straight from the source tree. -
Translate the project-type locales (optional but recommended) — each type under
src/project-types/*/i18n/ships its own small locale file, and the type registers them in itsgetTranslations(). Without a<lang>.jsonthere the FiveM / web app / API / Minecraft / Python / Discord panels fall back to English key by key while the rest of the UI is translated. -
Add a badge block to
.github/workflows/i18n-badge.yml:- name: Update badge — German (de) if: ${{ steps.parse.outputs.message_de != '' && secrets.GIST_SECRET != '' && secrets.GIST_ID != '' }} uses: schneegans/dynamic-badges-action@e9a478b16159b4d31420099ba146cdc50f134483 # v1.7.0 with: auth: ${{ secrets.GIST_SECRET }} gistID: ${{ secrets.GIST_ID }} filename: i18n_de.json label: "🌐 German (de)" message: ${{ steps.parse.outputs.message_de }} color: ${{ steps.parse.outputs.color_de }}
-
Add the badge to
README.md(in the badges block at the top):
-
Update
TRANSLATIONS.mdat the project root with the new locale row. -
Open a Pull Request — the CI pipeline will run the coverage check automatically.
No extra dependencies needed — only the Node.js built-in modules are used.
# Check all locales (human-readable table)
node scripts/check-i18n.js
# Check a single locale
node scripts/check-i18n.js --locale=fr
# JSON output (for CI or badge generation)
node scripts/check-i18n.js --json
# Combined
node scripts/check-i18n.js --locale=fr --jsonThe script exits with code 0 when all locales meet the threshold, and 1
when any locale is below it.
Sample output:
────────────────────────────────────────────────────────────
i18n Coverage Report — Claude Terminal
────────────────────────────────────────────────────────────
Base locale : en.json (800 keys — 100%)
Threshold : 80%
────────────────────────────────────────────────────────────
Locale : fr (fr.json)
Status : ✓ PASS
Coverage: 800/800 keys (100%)
Missing : 0 key(s)
Extra : 0 key(s) not in base
────────────────────────────────────────────────────────────
✓ All locales meet the 80% threshold.
────────────────────────────────────────────────────────────
The GitHub Actions workflow .github/workflows/i18n-badge.yml runs whenever a
file under src/renderer/i18n/locales/** is pushed to the repository. It:
- Calls
node scripts/check-i18n.js --jsonto compute coverage figures. - For each non-English locale, calls
schneegans/dynamic-badges-actionto write a Shields.io endpoint payload to a GitHub Gist. - The README badge image is served by Shields.io, pointing at the raw Gist URL.
| Step | Action |
|---|---|
| 1 | Create a public Gist at https://gist.github.com (any file, any content). |
| 2 | Copy the Gist ID from the URL (the long hex string after your username). |
| 3 | Create a Personal Access Token (PAT) at https://github.com/settings/tokens with the gist scope. |
| 4 | Add two repository secrets under Settings → Secrets → Actions: |
GIST_SECRET — the PAT |
|
GIST_ID — the Gist ID |
|
| 5 | Trigger the workflow manually once via Actions → i18n Coverage Badges → Run workflow. |
| Percentage | Badge color | Meaning |
|---|---|---|
| ≥ 90 % | 🟢 brightgreen | Excellent coverage |
| 60 – 89 % | 🟡 yellow | Work in progress |
| < 60 % | 🔴 red | Needs attention |
| < 80 % | (any) | CI warning is emitted |
The threshold for CI warnings is configured at the top of
scripts/check-i18n.js via the COVERAGE_THRESHOLD constant.