StringLane

Browse docsLet a Coding Agent Translate Safely
Tutorial

Let a Coding Agent Translate Safely

Serve your localization files to Claude, Codex or any MCP host with stringlane mcp. Your agent does the translating; StringLane does the reading, the validating and the writing, and refuses anything it cannot verify.

Last updated

Your agent can already read your .arb files, and that is exactly the problem. Reading the file is the wrong way to know what a string says: Android XML and iOS .strings are escaped, i18next keys carry a namespace prefix that is not in the file, and .xcstrings buries values under variation dictionaries. An agent that edits those by hand gets it subtly wrong and reports success.

stringlane mcp hands the agent the engine instead — the same parser, validator and completion figures the desktop app is built on, rather than a second implementation of the same rules for agents to read.

Step 1: Configure your host#

The fastest route is to hand your agent the setup page and let it work out which host it is in. By hand, it is one command:

stringlane setup mcp --host <host>

--host takes claude-code, codex, vscode, gemini-cli, cursor, windsurf, zed or opencode. It prints the entry in that host's own shape and the exact file to put it in. It writes nothing.

On Claude Code, --apply then merges it, having first copied the file to a timestamped backup whose path it tells you. Running it again when the entry is already there changes nothing and exits 0, so it is safe in a setup script. On the other seven hosts --apply prints and exits non-zero, because StringLane will not merge into a config file for a host nobody has recorded — the printed entry is the same bytes, and you paste it.

Three hosts skip the paste, in two different shapes. Claude Code and Codex CLI take a plugin, which carries the server, the commands and the guard together. Gemini CLI takes an extension, which carries the server but declares no hooks, so on that host the guard is one more command. See Agent hosts.

The entry's arguments are mcp ., so it serves whichever project the host has open: one entry covers every StringLane project on the machine, and you only do this once.

Restart your agent afterwards and the tools appear.

Step 2: Ask for what you want#

You do not drive the tools yourself. Ask in the language you would use with a colleague:

Finish the German translations.

Your agent calls inspect_project to see what is missing, prepare_translation_plan to get the exact list with each string's base value, description, placeholders and plural categories, writes the German, and calls validate_translations to check it.

Step 3: Read what it wants to write#

validate_translations reports every problem without touching a file: placeholders, ICU structure, plural completeness, guarded terminology, length limits. A well-behaved agent fixes what comes back and validates again.

Then it asks you whether to write. Only then does it call apply_translations, and only with your authorization.

Why it cannot go wrong quietly#

apply_translations is deliberately hard to trigger by accident:

  • Your agent asks you before it runs, the same way it asks before editing a file. That prompt comes from your coding agent host, not from StringLane, and it is the gate; everything below sits underneath it.
  • It only accepts keys, locales and plural categories the plan offered. There is no way to ask it to translate something else.
  • It re-runs every check validate_translations runs. A passing validation earlier in the conversation is not permission.
  • It refuses if the project changed since the plan was made — you edited a string, a teammate's branch landed, a formatter ran — rather than overwriting work it never read.
  • It builds every proposed file in memory and re-parses it before touching anything. If one file would come out broken, no file is written.

If it does refuse, it says which of those it was.

Earlier versions also required an authorize: true argument on each writing tool. That is gone, and it is worth saying why, because it sounds like a protection being removed. It was not one. The agent filled that field in itself, so it was the agent asserting that you had decided, which is not the same as you deciding. In two recorded incidents an agent that met the refusal, and still had a job to finish, wrote the locale files with a script instead, bypassing every check above. The gate did not stop a write; it moved the write somewhere nothing could check it. A leftover authorize argument from an older caller is ignored, never rejected.

describe_keys, add_key, add_locale and set_project_context sit behind the same host prompt, and every write lands as one atomic commit under a per-project lock, so a failure leaves your project exactly as it was.

Two things worth doing first#

Write descriptions before translating. A description says what a string is for: where it appears, and which sense of a word is meant. It is what turns "Submit" into "the button that files the expense report", and every locale is translated better for it. prepare_translation_plan tells your agent how many keys have none, so it can offer to write them first.

Do it in that order. A description changes what a translation depends on, so adding one to a key that is already translated marks that translation out of date. Correct, since it was made without the context, but it means work. Describing first costs nothing.

Set the project context. set_project_context records what the product is, how it should sound, and the terms that must never be translated. Every later translation is made against it, in the app and in the CLI both, because it lives in stringlane.yaml.

Adding a language is two jobs#

add_locale creates the locale file in the right place and the right format. That is the half StringLane owns, and the half with the escaping and plural categories that are easy to get wrong by hand.

The other half is in your app, and it decides whether anyone ever sees the translation: a Flutter supportedLocales literal, an Info.plist, an Xcode project, an Android locale config or a Gradle localeFilters list, an i18next supportedLngs array, a language picker with a hardcoded list. Every one of them is optional, so none can be assumed, and missing one produces a fully translated locale the app never offers.

The file add_locale creates is empty on purpose — just the locale marker. Every key then shows up as missing in the next plan, which is what you want; pre-filling it with blank strings would look like translations you had chosen to leave empty.

Adding a key starts in the base locale#

An agent that has written t('checkout.title') into a component needs that key to exist, and apply_translations refuses any key the plan did not offer. add_key is the honest way to create one. It writes the key and its source string to the base locale only; every other locale then reports it missing in the next plan, and the ordinary plan, validate and apply chain fills it.

It takes every key from the component in one call, and that is one atomic write: either every key lands or none does. Each key can carry a description of what the string is for, stored where your format keeps one, and a key added without one comes back on the next prepare_translation_plan as still needing it. A key name your format cannot carry is refused before it breaks a build: checkout.title is legal ARB and i18next, and a broken Android resource name. One bad name refuses the whole set.

From the terminal, stringlane add-key --key <key> --value <string> does the same for a single key, and asks first.

The ten tools#

Five read: inspect_project, read_translations, prepare_translation_plan, validate_translations and read_key_usage. Five write: apply_translations, add_key, describe_keys, add_locale and set_project_context. The full list, with arguments and behaviour, is in the MCP tool reference. The project is fixed when the server starts and no tool takes a file path, so there is nothing an agent can point at a directory you did not open.

The write guard adds something an MCP server structurally cannot: it notices when the agent edits a locale file without going through these tools. It is a hook rather than an MCP capability, so what it can do depends on your host — and on one of the eight, it cannot do anything.

Frequently asked questions

Do I need an API key for the MCP server?
No. In agent mode the model is the one your agent already uses, so no provider key is involved anywhere. Only stringlane update, which is a separate command, reads a key, and it reads it from your environment.
Can the agent write to my locale files without asking?
No. Five of the ten tools can write, and your agent asks you before any of them runs, the same way it asks before editing a file. That prompt comes from your coding agent host, not from StringLane, and it is the gate. Underneath it, apply_translations accepts only the keys, locales and plural categories the plan offered, re-runs every check, and refuses if a file changed since the plan was made.
Which MCP hosts work with StringLane?
Any host that speaks the Model Context Protocol, and eight of them have a configuration StringLane prints by name: Claude Code, Codex CLI, Gemini CLI, Cursor, Zed, OpenCode, VS Code Copilot and Windsurf. Claude Code and Codex CLI install it as a plugin, Gemini CLI as an extension, and the rest take an entry you paste. Claude Code is the one where stringlane setup mcp --apply writes it for you.