Run Localization Checks in CI
Gate a pull request on missing keys and broken placeholders with stringlane check. Exit codes, a machine-readable report, a baseline for an unclean repository, and the state-directory setting CI runners need.
Last updated
The whole gate is one command that already sets the right exit code, so there is no scripting to wrap around it.
GitHub Actions#
name: Localization
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm install -g @stringlane/cli@0.9.0
- run: stringlane check .Pin the version. The CLI is at 0.x, which is the notation for a command surface that is not promised yet, and a pipeline should not change behaviour because the registry served a new minor that morning.
What the exit codes mean#
Keeping 2 separate is the point. An unreadable file, a bad flag, or a .stringlane/source-stamps.json that sourceStamps: true needs and the checkout does not have is not the same event as a genuinely broken translation, and a pipeline that collapses them tells you the wrong thing on a bad morning. --fail-on cannot suppress a 2.
3 exists for the same reason one step further out: the command itself was fine and something it asked did not answer. Only stringlane upgrade can return it, so a check job will never see one.
The unused-key scan#
check also reads your source code and reports keys nothing calls, and keys your code calls that do not exist. Both land at info severity, so upgrading the CLI does not turn your pipeline red and a committed baseline does not flip.
Opt in when you want it to:
stringlane check . --fail-on unused_keyOr skip the scan entirely, which is worth doing on a runner that does not check out your application source:
stringlane check . --no-scanPassing both 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. What the scan can and cannot see: Unused keys.
Start green on a repository that is not#
A gate that fails on its first run gets removed. Record today's findings, once, and commit the result:
stringlane baseline write . --yes
git add .stringlane/baseline.yamlFrom then on only new issues fail the build. Accepted ones are still counted and printed in every run, so the debt stays visible rather than disappearing. Full behaviour in Inspect and check.
A baseline never holds translation_stale or translation_unconfirmed. baseline write leaves them out and says how many, and check exits 2 on a baseline that holds either code, naming the entry.
Fail the build on a stale translation#
With sourceStamps: true in stringlane.yaml, check reports translation_stale when the English a translation was confirmed against has changed, and translation_unconfirmed when nobody confirmed it. Both are warnings, so they fail the gate like any other warning. See how StringLane finds stale translations.
Three things for a pipeline:
Commit .stringlane/source-stamps.json. check reads it from the checkout. If sourceStamps: true is set and the file is missing, unreadable, malformed or not a plain file, check exits 2:
.stringlane/source-stamps.json is missing, and stringlane.yaml sets sourceStamps: true. Restore it from version control (it must be committed, not ignored), or run stringlane accept --all to create it.
at .stringlane/source-stamps.json
(VALIDATION_FAILED)A .gitignore line that hides .stringlane/ is the usual cause; stringlane init warns when it finds one.
Name the codes in --fail-on only when stamps are on. --fail-on translation_stale in a project without sourceStamps: true exits 2, because the code can never fire there:
--fail-on translation_stale cannot fire: source stamps are off in this project.
Only the source-stamp pass reports that code, and it runs only when stringlane.yaml sets sourceStamps: true.
Run stringlane init to turn source stamps on, or add sourceStamps: true to stringlane.yaml and run stringlane accept --all. Otherwise drop translation_stale from --fail-on.
(VALIDATION_FAILED)Review the stamps file in pull requests. Confirming a translation is a change to that file, made by stringlane accept or by the desktop app. accept --yes in a shell is not visible to the agent write guard, so the diff is where a person sees it.
Keep the report#
stringlane check . --json --output stringlane-report.jsonUpload it as an artifact, or parse it to post a comment. The envelope is stable and versioned; its shape is in the protocol reference.
- run: stringlane check . --json --output stringlane-report.json
continue-on-error: true
- uses: actions/upload-artifact@v4
with:
name: stringlane-report
path: stringlane-report.jsoncontinue-on-error on that step only, so the report is still uploaded when the check fails, which is the run you actually want it for. Keep a plain stringlane check . step afterwards if you want the job itself to go red.
Two things CI runners get wrong#
No writable home directory. StringLane keeps a small amount of its own state outside your repository, in the usual per-user location for the platform. On a runner or in a container where that has nowhere to go, point it somewhere writable:
export STRINGLANE_STATE_HOME="$RUNNER_TEMP/stringlane"It must be an absolute path. A relative value is ignored rather than honoured, because otherwise the state would land wherever the command happened to be started from.
Telemetry. The CLI sends usage data by default, and sends nothing at all when it detects a continuous-integration environment. It reads CI, plus the variables Azure Pipelines, TeamCity, AWS CodeBuild, Jenkins, Bamboo and Buildkite set for themselves: TF_BUILD, TEAMCITY_VERSION, CODEBUILD_BUILD_ID, JENKINS_URL, bamboo_buildKey and BUILDKITE. On those runners there is nothing to configure. On any other, or if you want to be explicit anyway, DO_NOT_TRACK=1 or STRINGLANE_TELEMETRY=0 in the job environment says so.
What CI must not run#
Every writing command refuses when there is no terminal to ask at, unless you pass --yes:
refusing to write your localization files without confirmation.
stdin is not a terminal, so there is nobody to ask: pass --yes to confirm up front.That refusal is the feature. Do not add --yes to apply or update in a pipeline to get past it: those commands change your translations, and a job that translates on its own is a job that commits work nobody read. Gate on check, and let a person or an agent do the writing.
Frequently asked questions
- How do I fail a build when a translation is missing?
- Run stringlane check in the job. It exits 1 when the project has localization issues and 0 when it is clean, so the step fails on its own with no scripting around it.
- Can stringlane check modify files during a CI run?
- No. check writes nothing into your repository: what it keeps between runs, such as its cache of scanned source files, lives in StringLane's own state directory. Every command that does write refuses to run without a terminal unless it is passed --yes explicitly. A CI job cannot accidentally commit a translation decision nobody made.
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