StringLane

Browse docsExit Codes and the JSON Report
Reference

Exit Codes and the JSON Report

What each of the four stringlane exit codes means, the shape of the --json envelope, and the machine-readable refusal codes apply can return.

Last updated

Exit codes#

CodeMeaning
0No issues
1The project has localization issues
2The command could not run
3Something stringlane depends on did not answer

The separation of 1 and 2 is load-bearing. A missing stringlane.yaml, an unreadable file or a bad flag is not the same event as a genuinely broken translation, and a pipeline that treats them alike tells you the wrong thing on a bad morning.

3 narrows 2 further, and today only one thing produces it: the npm registry gave no answer to stringlane upgrade. The command itself was fine and your project is not implicated, so calling it 2 would say you invoked it wrong. Nothing about your translations can produce a 3, and neither can the background update check — that one leaves the exit code untouched whatever the registry does, so a check job never sees one.

--fail-on can only narrow which findings produce 1. It cannot suppress a 2 or a 3.

Per-command variations:

  • plan exits 0 even when there is a great deal to translate, because a plan is a list rather than a gate. It exits 1 only when a file failed to parse, since a plan built from an unreadable file is incomplete rather than empty.
  • validate exits 1 when the candidates have problems.
  • apply exits 0 when everything was applied, 1 when the candidates have problems, 2 when it could not run — which includes every refusal below.
  • upgrade exits 0 whether you are current or behind, since being behind is not an error, and 3 when the registry gave no answer.

The JSON envelope#

stringlane check . --json
{
  "protocolVersion": "1",
  "command": "check",
  "status": "issues",
  "result": {
    "kind": "single-origin",
    "locales": ["de", "en", "fr"],
    "baseLocale": "en",
    "totalKeys": 8,
    "completionPercent": 96,
    "localeCoverage": [
      { "locale": "de", "totalCells": 9, "filledCells": 9, "missingCells": 0 },
      { "locale": "fr", "totalCells": 10, "filledCells": 9, "missingCells": 1 }
    ],
    "localizationFiles": [
      "res/values-de/strings.xml",
      "res/values-fr/strings.xml",
      "res/values/strings.xml"
    ],
    "parseErrors": [],
    "loadIssues": [],
    "configWasAbsent": false,
    "configState": "present",
    "format": "android-xml",
    "detectedConfig": { "format": "android-xml", "path": "res" },
    "issues": [
      {
        "key": "item_count",
        "locale": "fr",
        "severity": "error",
        "code": "missing_key",
        "message": "Plural quantity \"many\" for \"item_count\" is missing in \"fr\"",
        "quantity": "many"
      }
    ],
    "issueCounts": { "error": 1, "warning": 3, "info": 0 }
  },
  "warnings": []
}

Envelope fields#

FieldMeaning
protocolVersionCurrently "1". The contract to build against
commandWhich command produced this
statusThe outcome, matching the exit code family
resultThe command's own payload, shaped per command
warningsNon-fatal notes about the run itself, not about your translations

Coverage fields#

Counts are cells, not keys. A plural key contributes one cell per quantity the target language requires, which is why a locale requiring one, few, many and other can show a higher totalCells than one requiring two forms. completionPercent is derived from cells across the project.

Issue fields#

FieldMeaning
keyThe translation key
localeThe locale the finding is about
severityerror, warning or info
codeStable identifier — see the validation codes
messageHuman-readable, and the string the terminal prints
quantityPresent on plural findings: the CLDR category
detailPresent on some codes: what was expected and what was found

code is the field to branch on. message is written for a person and is allowed to be reworded.

Refusal codes#

apply and the writing MCP tools report a machine-readable reason when they decline. Nothing is written on any of them.

CodeMeaningWhat to do
STALE_PLANA file the plan could touch changed since the plan was madeRe-run stringlane plan
PROJECT_BUSYAnother apply holds the per-project lockWait, or --force-unlock if you know the holder is dead
PATH_OUTSIDE_PROJECTA path resolved outside the canonical project rootCheck for a symbolic link in the repository
INVALID_PROJECTThe project could not be read as a StringLane projectRun stringlane init, or check stringlane.yaml

WRITE_FAILED is returned when the write itself could not happen — the project directory is not writable, the filesystem is read-only, or there is no space. Nothing is written on it either.

Two issue codes, unused_key and undefined_key, are produced only by the source-code scan, so check --no-scan --fail-on unused_key is refused rather than accepted: with the scan off nothing can emit that code, and a gate that can never fire looks exactly like a clean project.

A candidate naming a key and locale the plan does not contain is refused rather than ignored. Quietly dropping an agent's typo produces a missing translation nobody ever notices.

Frequently asked questions

What does stringlane exit code 2 mean?
The command could not run: a missing or invalid config, an unreadable project, a bad flag. It is deliberately distinct from exit 1, which means the command ran and found localization issues, and from exit 3, which means the command was fine but something it depends on did not answer. A broken gate and a failing gate are different answers.
Is the stringlane --json output stable?
The envelope carries protocolVersion "1" and is the contract to build against. Pin the CLI version in CI anyway: at 0.x the command surface is not promised, and the version number is how that is said.