i18n-mcp
# i18n-mcp
[](https://www.npmjs.com/package/@robinheat/i18n-mcp)
> **Vibe coded project** — built fast, works well, but may have rough edges. Missing a feature or hit a bug? [Open an issue](https://github.com/robinheat/i18n-mcp/issues) — contributions welcome.
MCP server for managing i18n JSON translation files. Gives Claude structured read/write access to your translation files — add keys, check coverage, find duplicates — without ever leaving your editor.
Works with monorepos. Supports both flat (`en.json`) and i18next folder (`en/translation.json`) structures, auto-detected per namespace.
## Quick Start
Run this once in your project root:
```bash
npx @robinheat/i18n-mcp install
```
This installs the Claude Code skills and adds the MCP server to your project's `.mcp.json`. Then:
1. **Restart Claude Code**
2. **Run `/i18n-setup`** — auto-detects your translation files, infers tone and brand terms, writes `.i18n-mcp.json`
## Configuration
`.i18n-mcp.json` lives in your project root:
```json
{
"primaryLocale": "en",
"style": {
"tone": "informal",
"glossary": {
"Wärmepumpe": "heat pump"
},
"doNotTranslate": ["Robin", "COP"]
},
"namespaces": [
{
"name": "common",
"description": "Shared UI strings",
"path": "packages/ui/locales"
},
{
"name": "web",
"description": "Web app strings",
"path": "apps/web/locales"
}
]
}
```
| Field | Required | Description |
|---|---|---|
| `primaryLocale` | Yes | Source-of-truth locale (used for integrity checks) |
| `namespaces` | Yes | Array of namespace definitions |
| `namespaces[].name` | Yes | Short name used in tool calls |
| `namespaces[].description` | Yes | Helps Claude choose the right namespace |
| `namespaces[].path` | Yes | Path to locale directory, relative to project root |
| `style.tone` | No | `"informal"` or `"formal"` |
| `style.glossary` | No | Terms with fixed translations |
| `style.doNotTranslate` | No | Terms that should never be translated |
## Project Root
Every path the server touches — `.i18n-mcp.json` and each namespace path — is resolved
against a single project root. The root is resolved in this order:
| Source | Precedence |
|---|---|
| `--root <path>` argument | 1 (highest) |
| `I18N_MCP_ROOT` environment variable | 2 |
| MCP `roots` reported by the client | 3 |
| The server process's working directory | 4 (fallback) |
The fallback is the launch directory of the MCP client, and it is fixed for the life of
the server process. **If you work in a git worktree or any checkout other than the one
the client was launched in, set the root explicitly** — otherwise reads answer from, and
writes land in, the launch checkout, which looks like success and leaves the other tree
dirty:
```json
{
"mcpServers": {
"i18n-mcp": {
"command": "npx",
"args": ["-y", "@robinheat/i18n-mcp@latest", "--root", "/absolute/path/to/project"]
}
}
}
```
Clients that implement MCP roots (Claude Code does) are asked for their workspace root
when no explicit root is given, and the config is re-read when the client reports that
its roots changed. Whether a client sends `roots/list_changed` on a mid-session
directory move is client-dependent, so `--root` / `I18N_MCP_ROOT` remain the guaranteed
fix.
Every write reports the absolute path of each file it touched, and `get_i18n_status`
reports the resolved root at any time.
## File Structure Support
Both layouts are auto-detected per namespace:
**Flat:**
```
locales/
en.json
de.json
fr.json
```
**i18next folder style:**
```
locales/
en/
translation.json
de/
translation.json
```
## Tools
All tools are available to Claude once the MCP server is running.
### `get_translation`
Returns translations for a single key across all locales. Faster than `get_translations` for targeted spot-checks.
```
get_translation("common", "button.save")
// → { "en": "Save", "de": "Speichern" }
```
### `get_namespace_keys`
Returns a sorted list of all dot-notation keys in a namespace without values. Use to plan batch translation work without loading full locale content.
```
get_namespace_keys("common")
// → ["button.cancel", "button.save", "title"]
```
### `get_translations`
Returns all keys for a namespace as `{ "key.path": { "en": "...", "de": "..." } }`.
```
get_translations("common")
get_translations("common", "button.*") // glob filter on keys
get_translations("common", "save") // substring filter on values
```
Results are capped at 100 KB (override with `I18N_MCP_MAX_RESULT_BYTES`). A namespace
with thousands of keys across many locales runs to megabytes, which stalls the MCP
client, so oversized results are truncated and a second content block reports how many
entries were dropped. Filter with a query, or list keys with `get_namespace_keys` and
fetch them individually with `get_translation`.
The query is matched as a glob against keys and as a substring against values. Brace
expansion, extglob, and leading-`!` negation are disabled, so natural-language queries
like `{{count}} tickets` or `!important` behave predictably. The same cap applies to
`get_namespace_keys` and `find_untranslated_values`; `check_translation_integrity`
lists at most 100 keys per locale per category.
### `add_translation`
Adds or updates a single key. Only the provided locales are written.
```
add_translation("common", "button.save", {
en: "Save",
de: "Speichern",
fr: "Enregistrer"
})
```
### `add_multiple_translations`
Batch version — one disk write per locale file regardless of entry count.
```
add_multiple_translations("common", [
{ key: "button.save", translations: { en: "Save", de: "Speichern" } },
{ key: "button.cancel", translations: { en: "Cancel", de: "Abbrechen" } }
])
// Only write "de" even if other locales are provided:
add_multiple_translations("common", [...], ["de"])
```
### `delete_translation`
Removes a key from all locale files in a namespace.
```
delete_translation("common", "button.save")
```
### `find_untranslated_values`
Finds keys where the translated value is identical to the primary locale — placeholder translations that were never actually translated. Terms in `doNotTranslate` are excluded.
```
find_untranslated_values("web") // all non-primary locales
find_untranslated_values("web", "de") // one locale
```
Returns `{ locale: { key: primaryValue } }` for each stale entry found.
### `get_pending_translations`
Returns the complete translation work list for one locale: every key that is missing, empty, or still identical to the primary value, as `{ key: primaryValue }`. Identical values listed in `doNotTranslate` are excluded; missing `doNotTranslate` keys are included so they get copied verbatim. This is the preferred way for a per-locale translation agent to fetch its own work in one call.
```
get_pending_translations("web", "de")
```
Results are size-capped like `get_translations`. Since the tool only ever returns still-pending keys, an agent can translate and write a slice with `add_multiple_translations`, then call again for the remainder until it comes back empty.
### `check_translation_quality`
Checks specific keys for quality issues across all non-primary locales. Returns issues per locale per key: `untranslated` (value identical to primary), `empty` (missing or blank), `short` (< 30% of primary value length for strings longer than 15 chars). Terms in `doNotTranslate` are excluded from the `untranslated` check.
```
check_translation_quality("web", ["header.title", "onboarding.description"])
```
### `copy_from_primary`
Copies the primary locale value verbatim to specified locales for specified keys. Use for brand names, units, prices, and other terms that should not be translated. Returns an error if any key is missing from the primary locale.
```
copy_from_primary("common", ["brand.name", "unit.percent"], ["de", "fr"])
```
### `check_translation_integrity`
Compares all locales against `primaryLocale`. Returns missing keys, extra keys, and empty values per locale.
```
check_translation_integrity() // check all namespaces
check_translation_integrity("common") // check one namespace
```
### `get_i18n_status`
Reports where the server is actually operating: the resolved project root, which source
it came from, the config file path, and the absolute path, file structure and locales of
every namespace. Warns when the root fell back to the working directory, and flags a
root that is a linked git worktree.
```
get_i18n_status()
// → { "root": "/Users/you/project", "rootSource": "--root argument", ... }
```
Call this before writing if you are working in a worktree or any checkout other than the
directory the MCP client was launched in.
## Array Values
JSON arrays are not supported as leaf values. Use **indexed dot-keys** instead — this is what i18next expects when you call `t('key', { returnObjects: true })` anyway.
**In your translation file:**
```json
{
"steps": {
"0": "Connect your device",
"1": "Open the app",
"2": "Follow the setup guide"
}
}
```
**Adding via tools:**
```
add_multiple_translations("common", [
{ key: "steps.0", translations: { en: "Connect your device", de: "Gerät verbinden" } },
{ key: "steps.1", translations: { en: "Open the app", de: "App öffnen" } },
{ key: "steps.2", translations: { en: "Follow the setup guide", de: "Setup-Anleitung folgen" } }
])
```
**Reading via tools:**
```
get_translations("common", "steps.*")
```
Integrity checks and missing-key detection work the same as for any other key.
## Usage Skills
**For day-to-day work** (small edits, targeted key additions):
```
/i18n-usage
```
Guides Claude to check integrity first, search before adding, always add all locales at once, and verify coverage when done.
**For large translation jobs** (20+ keys or 3+ locales):
```
/i18n-translate
```
Orchestrates parallel agents — one per locale — so large jobs run faster without self-review loops or sequential batching.
## Manual Installation (without npm)
Add the server to `.mcp.json` in your project root:
```json
{
"mcpServers": {
"i18n-mcp": {
"command": "npx",
"args": ["-y", "@robinheat/i18n-mcp@latest"]
}
}
}
```
Then create `.i18n-mcp.json` in your project root manually.
## Development
```bash
npm test # run tests (124 tests)
npm run build # compile to dist/
npm run dev # run server directly with tsx (needs .i18n-mcp.json in cwd,
# or pass --root: npm run dev -- --root /path/to/project)
```
### Releasing
```bash
npm version patch -m "chore: release %s" # bumps package.json, commits, tags
git push origin main --follow-tags
npm publish # runs the build via prepublishOnly
```
**Run `npm publish` from a real terminal, not from inside Claude Code or any other
wrapper that pipes output.** The npm account has 2FA on writes, and npm's web
authorization flow prints an `npmjs.com/auth/cli/...` link and then waits for you to
approve it — it only offers that prompt when stdin/stdout are a TTY. With output piped
it skips straight to `npm error code EOTP` asking for an authenticator code.
`--auth-type=web` does not help; that is already the default and only affects `npm login`.
## License
MIT
TDQS
Scored across 12 tools
Get-side tools (get_translation, get_translations, get_namespace_keys) are well differentiated by scope thanks to clear descriptions. However, the four audit tools (check_translation_integrity, find_untranslated_values, get_pending_translations, check_translation_quality) overlap substantially, especially get_pending_translations which is nearly a superset of check_translation_integrity and find_untranslated_values; descriptions mitigate but don't eliminate the confusion.
Every tool uses snake_case with a consistent verb_noun(phrase) pattern (get_, add_, check_, find_, copy_, delete_). The one variation, get_i18n_status, still fits the schema and reads clearly.
12 tools is well-scoped for a translation-management domain, covering reads, writes, deletion, integrity/quality audits, and status without excessive redundancy.
Core key lifecycle is covered (create/update via add_translation and add_multiple_translations, read, delete, plus quality checks), but there is no key rename/move, no bulk delete, and no namespace or locale creation/management. These are minor gaps an agent can mostly work around.