# 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.

Source: https://stringlane.app/docs/cli/agent-workflow
Last updated: 2026-09-14

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](/docs/cli/agent-setup) and let it work out which host it is in. By hand, it is one command:

```sh
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](/docs/cli/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](/docs/reference/mcp-tools). 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](/docs/cli/agent-hosts) 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.
