Translation Sync MCP Server
# Translation Sync MCP Server
A general-purpose MCP server that keeps per-language JSON locale files in sync with an English source file, using the free [MyMemory](https://mymemory.translated.net/) translation API (no API key required). Works with any project that stores locales as `src/assets/i18n/<lang>/<lang>.json` (or a custom path you configure).
## Features
- π Translates to any of MyMemory's ~111 supported languages, not just a fixed list
- π No API key, no signup β free MyMemory API
- π Two ways to detect changes: a local cache diff, or a JSON-based git diff (immune to the "comma shift" false positives of text-based git diff parsing)
- π§© Validates that `{{placeholder}}` / `%s` / `%1$s` tokens survive translation β a mismatch is never written to disk
- π€ Agent fallback β if MyMemory can't produce a usable translation (API error, quota, bad language code, or a token mismatch), the server does **not** write English text or a broken translation. It reports the failure and asks the calling agent to translate it directly; you feed the result back via `manual_translations`
- ποΈ Prunes target-language keys that no longer exist in the source (including add-then-delete-in-the-same-diff via orphan detection)
- π Preserves each target file's existing JSON indentation
- β‘ Retries transient MyMemory failures (429/5xx) with backoff; bounded concurrency so it doesn't hammer the API
## Requirements
- Node.js 18+ (uses the native `fetch`)
- A git repository (only required for `preview_git_changes` / `diffGitChanges`; the cache-based tools work without git)
## Installation
```bash
cd .mcp/translation-sync
npm install
```
Add it to your project's `.mcp.json` (paths are resolved relative to this file, not hardcoded β see Configuration below):
```json
{
"mcpServers": {
"translation-sync": {
"command": "node",
"args": ["./.mcp/translation-sync/index.js"]
}
}
}
```
Restart your MCP client (Claude Code, etc.) to pick it up.
## Configuration
The server has no hardcoded project path. It resolves things in this order:
| Setting | Resolution order |
|---|---|
| Project root | `TRANSLATION_SYNC_ROOT` env var β nearest enclosing git repo root (`git rev-parse --show-toplevel`) β two directories above this script |
| Locale folder | `TRANSLATION_SYNC_I18N_PATH` env var (relative to project root) β `src/assets/i18n` |
Set these as env vars in `.mcp.json` if your project's locale folder lives somewhere else:
```json
{
"mcpServers": {
"translation-sync": {
"command": "node",
"args": ["./.mcp/translation-sync/index.js"],
"env": {
"TRANSLATION_SYNC_I18N_PATH": "src/locales"
}
}
}
}
```
Expected folder layout under the locale path β one subfolder per language code, containing a file whose name mirrors the source file's with `en` replaced by that code:
```
src/assets/i18n/
βββ en/
β βββ en.json
β βββ en-b2b.json
βββ es/
β βββ es.json
β βββ es-b2b.json
βββ fr/
βββ fr.json
```
A language folder that doesn't exist yet is created automatically the first time you sync to it.
## MCP Tools
### `list_available_languages`
```js
list_available_languages({ scope: "project" }) // this project's configured locale folders (default)
list_available_languages({ scope: "all" }) // every language MyMemory supports, project-agnostic
```
### `preview_changes`
Cache-based diff: compares the source file against the last time `sync_translations` ran (stored in `.translation-cache.json`). No git required.
```js
preview_changes({ source_file: "en.json" })
```
### `preview_git_changes`
Git-based diff against a ref (defaults to `HEAD`). Use this to see what's changed but not yet synced/committed.
```js
preview_git_changes({ source_file: "en.json", base_ref: "HEAD" })
```
### `translate_text`
One-off translation utility, independent of any locale file.
```js
translate_text({ text: "Collection Progress", target_language: "es" })
```
If MyMemory can't translate it, the response asks you (the calling agent) to translate it yourself instead of returning English or a broken string.
### `sync_translations`
The main tool. Syncs added/modified keys into every target language file and removes keys no longer in the source.
```js
// Sync everything that changed, across all of this project's configured languages
sync_translations({ source_file: "en.json" })
// Only specific languages
sync_translations({ source_file: "en.json", target_languages: ["es", "fr", "de"] })
// Only specific keys
sync_translations({ source_file: "en.json", keys: ["text.collection.progress"] })
// Re-translate keys even if the target already has a value
sync_translations({ source_file: "en.json", force_update: true })
// Supply your own translations for keys MyMemory couldn't handle
// (from a prior response's "needs_agent_translation" list)
sync_translations({
source_file: "en.json",
manual_translations: {
es: { "text.collection.progress": "Progreso de la colecciΓ³n" }
}
})
```
**When MyMemory fails a key** (API error, exhausted retries, or a placeholder/token mismatch), that key/language pair is **not written** to the target file. It's listed in the response under `needs_agent_translation` with the reason. Translate those yourself and resync with `manual_translations` to fill them in β `manual_translations` entries are written as-is, no API call.
## Recommended workflow
1. Edit `src/assets/i18n/en/en.json`.
2. Call `preview_git_changes` (or `preview_changes`) to see what changed.
3. Call `sync_translations` for the languages you need.
4. If the response lists `needs_agent_translation`, translate those yourself and call `sync_translations` again with `manual_translations`.
5. Review the diff in target language files, then commit.
## How it works
- **Change detection**: `preview_changes`/`sync_translations` diff the current source JSON against a cached snapshot from the last sync. `preview_git_changes` instead diffs parsed JSON against a git ref β both compare parsed objects, never raw text, so a key deletion elsewhere in the file never gets misread as an edit to its neighbor.
- **Translation**: each changed value is sent to MyMemory individually, 5 requests in flight at a time, with retry/backoff on 429/5xx. MyMemory's own errors (e.g. "invalid target language", quota messages) come back as HTTP 200 with the error text *as* `translatedText` β the server checks `responseStatus` in the body to catch these rather than trusting the field's mere presence.
- **Token safety**: after translation, `{{...}}` and `%s`/`%1$s`-style tokens in the source and translated text are compared; a mismatch is treated as a failure, not written.
- **Pruning**: any key present in a target file but absent from the current source is deleted, including keys that were added and removed again within the same diff (orphan detection).
## Known limitations
- **MyMemory free tier**: ~5,000 words/day per IP. Heavy use may hit quota; failures surface via `needs_agent_translation` rather than failing silently.
- **Translation quality**: MyMemory is a free machine-translation/translation-memory service, not professional-grade. Always review target-language diffs before committing, especially for domain-specific terminology.
- **Quota**: hitting the daily MyMemory word limit surfaces as a clear `needs_agent_translation` reason ("daily free-tier word quota exhausted") rather than a generic API error.
## Troubleshooting
| Symptom | Fix |
|---|---|
| Server not listed as a tool | Check `.mcp.json`, confirm Node 18+, restart the MCP client |
| `ENOENT ... scandir .../i18n` | `TRANSLATION_SYNC_I18N_PATH` / project root didn't resolve to your actual locale folder β set the env vars explicitly |
| A key keeps appearing in `needs_agent_translation` | MyMemory can't translate it (bad language code, quota, or the translation keeps dropping a `{{token}}`/`%s`) β translate it manually and pass it via `manual_translations` |
| Translations look wrong/literal | MyMemory is machine translation β use `force_update` after fixing the source text, or supply a manual translation |
## License
MIT
TDQS
Scored across 5 tools
Each tool has a clearly distinct role: sync performs writes, two previews use different comparison baselines (cache vs git ref), list reports locales, and translate_text handles ad hoc translation. The most similar pair, preview_changes and preview_git_changes, is explicitly differentiated by baseline and description.
All tool names use snake_case with a verb-first pattern: sync_translations, preview_changes, preview_git_changes, list_available_languages, translate_text. The convention is predictable and readable throughout.
Five tools is well-scoped for a translation synchronization server. Each tool supports a distinct part of the workflow without obvious bloat or missing core operations.
The tool set covers the core lifecycle: syncing translations, previewing changes from cache and git, listing languages, and translating individual text. Minor gaps exist around cache/config management or per-key editing, but agents can work around them.