StringLane

Browse docsHow the Write Gate Works
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.

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 a fingerprint of the files it was built from. Both matter in step 3.

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 at all: 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 could touch and compares it to what was there when the plan was made: your source files, the target files, and stringlane.yaml itself. Edit any of them 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.

Two applies cannot interleave. The second gets PROJECT_BUSY while the first is running. 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, because there is no StringLane store. Every write lands in your working tree as an ordinary modification of a file you already had.

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/

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 could touch changed between the plan being made and apply running: a source string, a target file, or stringlane.yaml itself. Rather than overwrite work it never read, apply refuses and names what moved. 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, because there is no separate store.