# 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.

Source: https://stringlane.app/docs/cli/ci
Last updated: 2026-09-16

The whole gate is one command that already sets the right exit code, so there is no scripting to wrap around it.

## GitHub Actions

```yaml
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.3.2
      - 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

| Code | Meaning | What your pipeline should do |
|---|---|---|
| `0` | No issues | Continue |
| `1` | The project has localization issues | Fail the job |
| `2` | The command could not run | Fail the job, and look at the message |
| `3` | Something `stringlane` depends on did not answer | Not reachable from `check` |

Keeping `2` separate is the point. 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 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:

```sh
stringlane check . --fail-on unused_key
```

Or skip the scan entirely, which is worth doing on a runner that does not check out your application source:

```sh
stringlane check . --no-scan
```

Passing 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](/docs/cli/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:

```sh
stringlane baseline write . --yes
git add .stringlane/baseline.yaml
```

From 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](/docs/cli/inspect-and-check).

## Keep the report

```sh
stringlane check . --json --output stringlane-report.json
```

Upload it as an artifact, or parse it to post a comment. The envelope is stable and versioned; its shape is in the [protocol reference](/docs/reference/exit-codes).

```yaml
      - run: stringlane check . --json --output stringlane-report.json
        continue-on-error: true
      - uses: actions/upload-artifact@v4
        with:
          name: stringlane-report
          path: stringlane-report.json
```

`continue-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:

```sh
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 anonymous usage data by default, and sends **nothing at all** when it detects a continuous-integration environment. There is nothing to configure. 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 is read-only and writes nothing at all, and 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.
