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#
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 decides which findings the exit code is computed from, in both directions: naming a code that is present exits 1, even for an info finding, and naming codes that are all absent exits 0. It cannot suppress a 2 or a 3.
Per-command variations:
planexits0even when there is a great deal to translate, because a plan is a list rather than a gate. It exits1only when a file failed to parse, since a plan built from an unreadable file is incomplete rather than empty.validateexits1when the candidates have problems.checkexits2, whatever--fail-onsays, whensourceStamps: trueis set and.stringlane/source-stamps.jsonis missing, unreadable, malformed or not a plain file; when the baseline holds atranslation_staleortranslation_unconfirmedentry; and when--fail-onnames one of those two codes whilesourceStampsis off.applyexits0when everything was applied,1when the candidates have problems,2when it could not run, which includes every refusal below and, withsourceStamps: true, a stamps file that is missing, unreadable, malformed or not a plain file.updateexits2before any credential is read when--parallelismorSTRINGLANE_AI_PARALLELISMis not a whole number from 1 to 8.pluralizeexits0when the forms were written, previewed or declined,2when it refused or could not run.acceptexits0when the stamps were written or you declined,2when it refused or could not run, including a project withoutsourceStamps: true.upgradeexits0whether you are current or behind, since being behind is not an error, and3when the registry gave no answer.
The JSON envelope#
stringlane check . --json --no-scan{
"protocolVersion": "1",
"command": "check",
"status": "issues",
"result": {
"kind": "single-origin",
"locales": [
"de",
"en",
"fr"
],
"baseLocale": "en",
"baseLocaleSource": "detected",
"totalKeys": 8,
"completionPercent": 96,
"localeCoverage": [
{
"locale": "de",
"totalCells": 9,
"filledCells": 9,
"missingCells": 0,
"totalKeys": 8,
"filledKeys": 8,
"missingKeys": 0
},
{
"locale": "en",
"totalCells": 9,
"filledCells": 9,
"missingCells": 0,
"totalKeys": 8,
"filledKeys": 8,
"missingKeys": 0
},
{
"locale": "fr",
"totalCells": 10,
"filledCells": 9,
"missingCells": 1,
"totalKeys": 8,
"filledKeys": 7,
"missingKeys": 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": "app_name",
"locale": "de",
"severity": "warning",
"code": "untranslated",
"message": "Key \"app_name\" in \"de\" appears to be untranslated (same as base)"
},
{
"key": "app_name",
"locale": "fr",
"severity": "warning",
"code": "untranslated",
"message": "Key \"app_name\" in \"fr\" appears to be untranslated (same as base)"
},
{
"key": "greeting",
"locale": "fr",
"severity": "warning",
"code": "punctuation_mismatch",
"message": "Key \"greeting\" in \"fr\" needs a non-breaking space before \"!\" but has a plain space before \"!\"",
"detail": {
"expected": "a non-breaking space before \"!\"",
"actual": "a plain space before \"!\""
}
},
{
"key": "item_count",
"locale": "fr",
"severity": "warning",
"code": "missing_key",
"message": "Plural quantity \"many\" for \"item_count\" is missing in \"fr\"; \"fr\" uses \"many\" only for exact millions, so it shows only for counts like 1,000,000",
"quantity": "many"
}
],
"issueCounts": {
"error": 0,
"warning": 4,
"info": 0
}
},
"warnings": []
}That is the real output of CLI 0.9.0 on a three-locale Android project, exit 1: a French plural missing its many form is a warning, because French uses many only for exact millions.
Envelope fields#
Coverage fields#
baseLocale is the source language. baseLocaleSource says where it came from: declared when baseLocale: in stringlane.yaml names it, detected when StringLane guessed it from your file names.
Each localeCoverage row carries two counts. totalKeys, filledKeys and missingKeys count keys out of the same total in every locale, and a key is filled once every form the language needs holds a value; the text report's rows show these. totalCells, filledCells and missingCells count strings: a plural key contributes one cell per form the 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, and the text report prints it as N% of strings filled.
Issue fields#
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.
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.
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