StringLane

All articles

Your coding agent will hand-edit your locale files

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/cli

In 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@stringlane

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

Know what every locale says before your users do

StringLane adds a key to every locale at once, shows them side-by-side, and flags what's missing or broken before you ship. Edits saved straight to your source files, no cloud, no export step. One purchase, no subscription, yours to keep.

Download the app

Early-adopter price: $49 one-time. Full price is $79.