Ask a coding agent to add a button label and it will do the obvious thing. It will open your base locale file, add the key, and stop. That is half the job, and the half it skipped is invisible: every other language is now behind, the build is green, and the tool call it made came back successful.
Ask it to translate the rest and something worse can happen. It writes the locale files directly.
Today StringLane ships the three pieces that close that gap: desktop 1.2.0, the free
stringlane CLI at 0.3.1, and an agent plugin that installs on eight hosts. This post
is about the specific problem the plugin exists for, because it is the one I did not expect and
the one that took the longest to get right.
The bytes on disk are not the string
Four of the five formats StringLane reads do not store your translation as you typed it.
An ARB file is JSON, so a quote inside a string is \" and a newline is \n. An Android
strings.xml needs \' for an apostrophe and & for an ampersand, and an apostrophe
written bare is a parse error rather than an apostrophe. An iOS .strings file escapes its own
way, and a plural does not live there at all: it lives in a companion .stringsdict, which is a
plist. An i18next JSON file nests, so the key your code calls is a path through several objects
rather than a line you can search for.
A model writing into those files is doing string manipulation on a format with rules it is
holding in its head. Most of the time it gets it right. The times it does not, the output is a
file that still parses and a UI that shows a literal %1$s to a user, or a plural that renders
nothing in Polish because the few category was never written.
None of that is a translation quality problem. Every one of those is structural, and structural problems are checkable.
An MCP server cannot see the edit
The obvious fix is to give the agent proper tools, so StringLane has an MCP server. It exposes ten tools, five of which read and five of which write, and every write goes through the parser and the placeholder, plural and length checks rather than through the model's idea of the format.
That works right up until the agent does not use it.
An MCP server sees its own tool calls and nothing else. It has no way to know that your agent
edited app_de.arb with its own Write tool, or with a sed command through Bash,
because neither of those is addressed to the server. I have watched that happen twice in this
project's own issues. The tools being available is not the same as the tools being used.
That is the entire reason StringLane's agent integration is a plugin rather than a line of MCP configuration. A plugin can register a hook, and a hook sees every write the agent makes, including the ones that never come near the MCP server.
What the guard can actually do, per host
Here is the part I would rather write as one sentence and cannot, because it would not be true.
The MCP server works in every host that speaks the protocol. The write guard does not, because the guard is not one feature with eight installers. It is built out of whatever interception each host offers, and the hosts differ:
| Host | What the guard does |
|---|---|
| Claude Code | Asks you first |
| Codex CLI | Asks you first |
| Gemini CLI | Refuses, and says how to lift it |
| Cursor | Refuses, and says how to lift it |
| Zed | Confirms, from Zed's own permissions |
| OpenCode | Confirms, from OpenCode's own permissions |
| VS Code Copilot | Installs, but guards nothing yet |
| Windsurf | None |
"Asks you first" and "refuses, with a way out" are different promises. Cursor and Gemini CLI
have no ask in their hook protocol, only allow and deny, so what StringLane can do there is
deny the write and tell you how to lift the block. Writing "yes" in both rows would have meant
claiming a human prompt on a host that cannot show one.
The last two rows are the ones I was tempted to leave out. VS Code's hook installs and guards nothing yet, because its edit-tool names are documented rather than emitted, so the list StringLane would have to match against is empty. Windsurf's pre-write hook is exit-code only, which teaches the model nothing about what went wrong, so there is no guard there at all. Both are rows in the table instead of absences from it, because a table with six rows and two silences is a table that lies by omission.
What replaces the hand-edit
When the guard stops a write, the agent needs somewhere to go, and that somewhere is three calls: plan, validate, apply.
prepare_translation_plan says what is missing and hands over the descriptions, the
placeholders and your never-translate terms with it. The model writes the candidates.
validate_translations checks them without touching a file. apply_translations writes, and
the write is all or nothing: if one string in the batch fails its checks, nothing lands.
It also refuses a plan that has gone stale. If the base string changed between the plan and the apply, you get this, and you get it before anything is written:
NOTHING WAS WRITTEN.
the project changed since this plan was created:
"Localizable.xcstrings". Build a new plan.
at Localizable.xcstrings
(STALE_PLAN)Two things about that block, because a transcript that quietly drops a line is a mock. The plan summary line that sits above it is omitted, and the long message is wrapped across two lines the way a terminal wraps it anyway. Nothing is reworded. The exit code is 2, which in StringLane means "could not run" rather than "found problems".
The other half: the keys nothing calls
The second thing in this release came out of a different observation about AI-era projects, and it is the one I did not plan for.
When you use a model to go wide, you accumulate keys the same way you accumulate anything else that is cheap to produce. A screen gets redesigned. A feature is cut. A key is renamed and the old one stays. It is still translated into every language you support, still counted against your completion percentage, still sent to your provider on the next run at your expense, and nothing anywhere ever says it is dead.
Desktop 1.2.0 and stringlane scan now read your source code and answer two questions: which
keys your project defines that no call site names, and which keys your code calls that your
project never defined. The second kind is a bug that is already shipping.
The list comes with a sentence that is part of the feature rather than a caveat on it: it reports what the scan saw, and that is not a verdict. A key whose name your code builds at runtime, or that is reached through a wrapper the scanner does not recognise, or that a platform reads straight out of the locale file, will appear in that list and be perfectly alive. Check before you delete, and never delete keys in the same change as anything else.
It also refuses rather than guessing. If it read no source files, or read them and recognised no keys at all, it says so and reports nothing as unused, because "I could not read your code" and "everything here is dead" produce an identical empty list and only one of them is safe to act on.
In stringlane check this is reported at info level, so upgrading the CLI cannot turn a green
pipeline red. Name unused_key in --fail-on if you want it to.
Getting it
The CLI is free. No licence, no account, no sign-in, and the desktop licence and the CLI do not grant each other anything in either direction.
npm install -g @stringlane/cliIn Claude Code or Codex CLI, take the plugin instead, which brings the MCP server and the guard with it:
/plugin marketplace add thebedcoder/stringlane-plugin
/plugin install stringlane@stringlaneThen run /stringlane:setup, which checks whether the stringlane binary the plugin calls
actually exists on your machine and whether the copy it found is new enough. That check exists
because a guard that cannot run looks exactly like a guard with nothing to report.
On the other six hosts, stringlane setup mcp --host <host> prints the real configuration for
the exact file that host reads, and you paste it. Claude Code is the one host StringLane writes
it for you, because it is the one whose config format has been recorded well enough to merge
into safely.
The desktop app is where you look at all of this: every locale side by side, validation as you type, and now the call sites for each key. It is a one-time licence, and 1.2.0 is a free update if you already have one.
Full notes for desktop 1.2.0 and CLI 0.3.1. If you would rather hand the whole setup to your agent, there is a page written for exactly that: paste the link and it works out its own host, installs what that host takes, and asks before every change.