StringLane

    Type to search titles, sections and body text across all 58 pages.

    Concept

    How the Write Gate Works

    What plan, validate and apply each guarantee, why a clean validate grants nothing, what STALE_PLAN and PROJECT_BUSY mean, and how to review or undo a write.

    Last updated

    The CLI does not translate anything on the plan path, and it holds no API key there. What it does is hand an agent an exact list of work, then refuse to write back anything it cannot verify. Three commands, in order, and each one is allowed to say no.

    1. plan: what needs translating#

    stringlane plan . --json --out plan.json

    Each item names one key in one locale:

    {
      "key": "bye",
      "locale": "de",
      "originPath": "lib/l10n/app_de.arb",
      "sourceValue": "Goodbye",
      "reason": "missing",
      "inputFingerprint": "943e881307729c67"
    }

    reason is why it is on the list: missing, source-changed, context-changed, invalid or requested.

    • source-changed and context-changed: a translation StringLane wrote earlier, whose English text or context has changed since. plan offers it again only while the cell still holds the text StringLane wrote; a cell somebody re-translated by hand is left alone. Applying the item replaces the old translation.
    • With sourceStamps: true in stringlane.yaml, the committed stamps file decides instead, whoever wrote the translation. A source-changed item then also carries previousSourceValue, the English the translation was confirmed against, so the agent can make the smallest edit that fits the new text. It is advisory: apply ignores it. In an .xcstrings catalog, a unit Xcode marks for review is offered the same way, with previousSourceValue equal to sourceValue, under its own heading in the human report.

    From a plan on a test project whose English changed from "Goodbye" to "See you soon" after the German was confirmed:

    {
      "key": "farewell",
      "locale": "de",
      "originPath": "l10n/app_de.arb",
      "sourceValue": "See you soon",
      "previousSourceValue": "Goodbye",
      "reason": "source-changed",
      "inputFingerprint": "ab82ce60203b8773"
    }

    The human report lists the first ten of these:

    Base text moved since these were confirmed:
      Key "farewell" in "de": confirmed against "Goodbye", now "See you soon"

    Some stale translations are not offered, because writing over them would discard somebody's work: one that was also changed since it was confirmed, and one nobody confirmed. stringlane check reports each as translation_stale or translation_unconfirmed, with the advice to edit it and run stringlane accept. See stale translations.

    inputFingerprint is what makes the item expire. It covers the source text, the description and metadata, the placeholders and plural structure, your product and locale context, guarded terms and length constraints. Change any of them and that item is stale even though the English never moved, which is the point: a translation made without a description is not the same translation as one made with it.

    The plan also carries a planId, and one fingerprint for each file apply re-reads before it writes, which covers every locale file the project loads, not only the targets.

    A plan is a list, not a gate. It exits 0 even when there is a lot to do, and 1 only when a file failed to parse, because a plan built from a file nobody could read is incomplete rather than empty.

    --out alone writes the human report you see in a terminal. Pass --json as well to write the plan itself, which is the file validate and apply read. Point apply at a human report and it tells you the flag you meant.

    Keys with an empty or missing base value are skipped, and named in a warning. Sending an agent an empty source string is a waste of work and money; fill them in the base locale, or remove them.

    2. The candidate file: what the agent produced#

    {
      "protocolVersion": "1",
      "planId": "plan_2d9c308b50693561",
      "items": [
        { "key": "bye", "locale": "de", "value": "Tschüss" }
      ]
    }

    planId must match the plan it was working from. A key/locale pair this plan does not contain is refused, not ignored: quietly dropping an agent's typo produces a missing translation nobody ever notices, which is the failure this whole tool exists to prevent. Plural forms carry a quantity alongside key and locale.

    3. validate: check it without writing#

    stringlane validate . --plan plan.json --candidates candidates.json

    It writes nothing into your project: not a lock file, not a temp file. It runs the same validation the desktop app runs: placeholders, ICU and plural structure, guarded terminology, length constraints.

    Its own output says the honest thing:

    This is not permission to write. `stringlane apply` runs these checks again
    and additionally verifies your project has not changed since the plan was made.

    A green validate says the translations are well-formed. It does not say your files are unchanged since the plan was made, because checking that requires a lock and this command takes none.

    4. apply: write them#

    stringlane apply . --plan plan.json --candidates candidates.json --yes

    Four guarantees, and each is a way the command can refuse:

    Either every proposed change lands or none does. Before a single byte is written, apply builds every file it intends to produce in memory, parses each one back, compares it to what was intended, and validates the whole project as it would be afterwards. One structural error anywhere and nothing is written.

    A plan expires. Under a per-project lock, apply re-reads every file the plan fingerprinted and compares it to what was there when the plan was made. The plan fingerprints every locale file the project loads, not only the locales it targets (the base, the targets and every other locale), plus the .stringsdict beside each .strings file, stringlane.yaml, and .stringlane/metadata.yaml. So a teammate's edit to app_de.arb stops an apply of a plan made with --target uk. A file the plan recorded as missing, such as stringlane.yaml in a project without one, counts as changed once it appears. A file the plan did not list, such as a new locale file, is not checked, and apply does not write to it. The comparison is on each file's full text, so a formatter or a line-ending change counts as an edit. Change any of those files and you get STALE_PLAN, naming what changed. Re-run plan. This is the check validate deliberately does not do, and it is why a clean validate grants nothing.

    A plan also has to name only files the project manages. Before opening anything, validate and apply both refuse a plan that names a file the project does not load any more: a locale file deleted, renamed or broken since the plan was made, or a plan edited to point elsewhere. That is also STALE_PLAN, exit 2. It matters most when apply runs in CI on a branch you did not write, because the plan file is input anyone on that branch can edit.

    Two writers cannot interleave. A second apply gets PROJECT_BUSY while the first is running, and from desktop 1.3.0 the desktop app's saves take the same per-project lock, so the app and an apply never write one project at once. If a previous run was killed, the lock it left behind is reclaimed, and reclaiming it is reported as a warning, never done silently. --force-unlock exists for the one case the lock cannot resolve itself: a killed run left a lock naming a process id that something else now wears, so it looks alive forever.

    Nothing follows a symbolic link out of your repository. Not the plan's file paths, not the config, not --output. A repository that ships a redirect does not get to choose which of your files an agent's translations land in.

    Reviewing and undoing#

    There is no StringLane undo. Every write lands in your working tree as an ordinary modification of a file you already had. StringLane does keep a record of what it wrote, in its own state directory outside your repository, but that record never overrides your files.

    git diff lib/l10n/

    That is the review surface, and it is the same one you use for every other change in the repository. If you do not want it:

    git checkout -- lib/l10n/

    Plan again afterwards and the cells you reverted come back as missing, ready to translate again.

    This is the whole argument for writing to source files rather than a database: the tooling for reviewing, reverting and blaming a change already exists, your team already knows it, and it works when StringLane is not installed.

    Frequently asked questions

    What does STALE_PLAN mean?
    A file the plan fingerprinted changed between the plan being made and apply running. A plan fingerprints every locale file the project loads, not only the locales it targets, plus the .stringsdict beside each .strings file, stringlane.yaml and .stringlane/metadata.yaml. So in a project with German and Ukrainian locales, an edit to the German file stops an apply of a plan made only for Ukrainian. Rather than overwrite work it never read, apply refuses and names what moved. validate and apply also return it, with exit 2, when the plan names a file the project no longer manages, such as a locale file deleted or renamed since. Re-run stringlane plan.
    How do I undo translations StringLane wrote?
    With git. Every write lands in your working tree as an ordinary modification, so git diff shows exactly what changed and git checkout throws it away. There is no separate undo. StringLane keeps a record of what it wrote outside your repository, but your files decide what is done: a cell you revert with git is offered again as missing the next time you plan.

    Free · In your coding agent

    Run the same checks from your coding agent

    Paste this into Claude Code, Cursor, Codex or five other agents. It sets up the CLI and MCP server for this project, proposing each command and waiting for your yes.

    Read https://stringlane.app/docs/cli/agent-setup.md and set up StringLane for this project.

    Or install it yourself:

    npm install -g @stringlane/cli