StringLane

    Type to search titles, sections and body text across all 58 pages.

    How-To

    Inspect and Check a Project

    The two reporting commands that write nothing into your repository, what the difference between them is, and how to adopt a check gate on a project that is already carrying hundreds of missing keys.

    Last updated

    Two commands, both read-only with respect to your repository: neither writes a lock file, a re-saved locale file, or anything else inside your project.

    What inspect and check do keep lives in StringLane's own state directory, outside your repository, so there is nothing to add to .gitignore:

    • install.json: a random install ID. The first command you run on a machine creates it, stringlane telemetry status included, and so does a run after stringlane telemetry off. With DO_NOT_TRACK=1 or STRINGLANE_TELEMETRY=0 set, or on a CI runner, nothing is written to this directory at all, and telemetry status reports random install ID: none.
    • update-check.json: when the CLI last asked the npm registry for the latest version, and the answer. It asks at most once a day, or an hour after a check that failed. It is not written when STRINGLANE_NO_UPDATE_CHECK=1, DO_NOT_TRACK=1 or STRINGLANE_TELEMETRY=0 is set, or on a CI runner; stringlane telemetry off does not stop it.
    • From check only, projects/<hash>/usage-cache-v1.json, in a folder named by a hash of the project path. For each source file it scanned for key usage, it holds the file's path inside the project, its size and modification time, and every key name found there with its line number, so the next run can skip the files that have not changed.

    STRINGLANE_STATE_HOME moves the whole directory; see the CI guide for the runners that need that, and Install and update for everything else it can hold.

    Look at a project#

    stringlane inspect .
    android-xml · single-origin · base en (detected)
    3 locales · 8 keys · 96% of strings filled
     
      de      8/8        complete
      en      8/8        complete
      fr      7/8        1 missing

    The header names what was detected: the format, whether every locale lives in one file (single-origin) or several, and which locale is the base. (detected) means the base was guessed from your file names; baseLocale: in stringlane.yaml names it instead.

    The two kinds of count differ on purpose. The percentage counts strings, where each plural form is one string, and a string counts as filled once it holds a value. The rows count keys, every locale out of the same total, and a key is done only when every plural form its language needs is filled. Here French has 7 of 8 keys done because one plural key lacks a form, while 96% of all strings are filled. --json keeps the per-form counts and adds totalKeys, filledKeys and missingKeys to each locale.

    Validate it#

    stringlane check .

    Everything inspect prints, plus the findings, plus an exit code. On the same project:

    0 errors · 4 warnings · 0 info
      warning  untranslated          de/app_name: Key "app_name" in "de" appears to be untranslated (same as base)
      warning  untranslated          fr/app_name: Key "app_name" in "fr" appears to be untranslated (same as base)
      warning  punctuation_mismatch  fr/greeting: Key "greeting" in "fr" needs a non-breaking space before "!" but has a plain space before "!"
      warning  missing_key           fr/item_count[many]: 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
     
    1 warning(s):
      Source files were read but no translation keys were recognised in them. This usually means a framework or call style the scanner does not know yet, rather than a project with no translations.

    Each finding is severity, code, locale/key, then the message. The codes are stable and greppable: the full list is in the validation code reference. The closing warning is from the unused-key scan, which says when it could not match your code to your keys rather than calling every key unused. See Unused keys.

    Warnings fail the gate too: this project exits 1.

    Exit 0 clean, 1 issues, 2 could not run, 3 something StringLane depends on did not answer. See exit codes and the JSON protocol.

    Adopt it on a project that is not clean#

    Most real projects fail on day one. A repository translated by several people over a couple of years carries hundreds of missing keys, and a gate that goes red on its first run is a gate somebody switches off that afternoon.

    So record where you are:

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

    check now passes, and from that point it fails only on issues that are new.

    Four things about how that behaves, because they are the difference between a baseline and muting the problem:

    Recorded issues are reported, not hidden. Every run prints how many are accepted. The debt is visible in the output and in your repository, in a file your team can read.

    Fixing something is noticed. When a recorded issue no longer exists, its entry goes stale: still reported, still suppressing, and cleared the next time you run baseline write. It does not silently vanish, and you are not surprised by a gate that starts failing because a file was tidied.

    Nothing expires on a timer. A baseline entry suppresses until somebody rewrites the file. A gate that turns red on a date nobody chose is a gate that gets disabled, and then the project has no gate at all.

    A baseline cannot hide a stale translation. With source stamps on, baseline write leaves out translation_stale and translation_unconfirmed and says how many it left out, because one recorded staleness would silence every later one for that translation:

      10 findings will be recorded as accepted:
        8 errors · 2 warnings · 0 info
     
      Left out 1 stamp finding (translation_stale or translation_unconfirmed): a baseline cannot hide it. Check the translation, then run stringlane accept.

    check refuses a baseline that holds either code and exits 2, naming the entry.

    Narrow what fails the build#

    While you dig out, you can let only some kinds of issue set exit 1:

    stringlane check . --fail-on missing_key,icu_parse_error

    --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 that would not have failed on its own. Naming only codes that are absent exits 0. That is how you turn on a check that ships as info, such as unused_key or expansion_budget_exceeded.

    Naming translation_stale or translation_unconfirmed in a project without sourceStamps: true exits 2, because that code can never fire there, and the message says how to turn source stamps on. A run that could not read your project also exits 2 whatever you pass, because "the gate failed" and "the gate could not run" are different answers.

    It composes with a baseline rather than replacing it: the baseline decides which findings are eligible to fail at all, and --fail-on filters what is left. An accepted finding stays accepted either way.

    How does StringLane find stale translations?#

    A translation goes stale when the English it was made from changes and nobody updates it. A missing-key check cannot see that, because the cell still holds text. Source stamps can.

    With sourceStamps: true in stringlane.yaml, StringLane keeps .stringlane/source-stamps.json in your repository: for each translation, the base text it was confirmed against. check then reports two warnings on top of everything else:

    • translation_stale: the base text changed since the translation was confirmed, whoever wrote the translation.
    • translation_unconfirmed: nobody has confirmed the translation, or the translation changed after it was confirmed while its base text did not.
    0 errors · 3 warnings · 0 info
      warning  translation_stale        de/farewell: Key "farewell" in "de" is stale: it was confirmed against the base text "Goodbye", and the base text is now "See you soon". Run stringlane update to translate it again, or stringlane accept if it still fits.
      warning  translation_unconfirmed  de/subtitle: Key "subtitle" in "de" was never confirmed against its base text. Check it, then run stringlane accept.
      warning  translation_stale        de/title: Key "title" in "de" is stale: it was confirmed against the base text "Preferences", and the base text is now "Settings". The translation also changed since it was confirmed, so stringlane update will not overwrite it. Check it against the new base text, then run stringlane accept.

    Both are warnings, so a stale translation makes check exit 1, and both work with --fail-on. With the flag off, or without the field, check behaves exactly as before.

    Turn source stamps on#

    In a new project that loads cleanly, stringlane init does it for you: after one question it writes stringlane.yaml with sourceStamps: true and creates the stamps file. In an existing project, add the line yourself and confirm what is there today:

    stringlane accept . --all
    git add stringlane.yaml .stringlane/source-stamps.json

    accept --all stamps every translation as current against today's base text. It cannot know whether they were current before today, and it says so. Commit the stamps file: with sourceStamps: true and no stamps file, check exits 2 and tells you to restore it from version control.

    Confirm a translation you checked#

    stringlane accept . --key farewell --locale de

    stringlane accept marks translations as current against the base text as it is now. --key, --locale and --quantity choose which, each repeatable, and different flags narrow each other: --key farewell --locale de is that key in German only. --all confirms everything and cannot be combined with them. It shows how many translations it will mark in each locale and asks first; with no terminal it refuses unless you pass --yes.

    accept needs sourceStamps: true and never edits stringlane.yaml. Without the flag it prints the line to add and exits 2.

    stringlane apply and stringlane update stamp the translations they write, so a translation StringLane wrote is current from the moment it lands. Confirming one somebody else wrote is meant to be a person's decision, so there is no MCP tool for accept. That is a convention, not a lock: an agent with a shell can run stringlane accept --yes, and the write guard does not see a flag inside a command, so review .stringlane/source-stamps.json in the pull request as you would a locale file.

    In an .xcstrings catalog, a translation Xcode marks for review is reported too, with the sentence "The catalog marks it for review." Confirming it with accept --key, --locale or --quantity also marks it translated in the catalog. accept --all and init leave these alone and say how many they left.

    Review changes to the stamps file#

    An edit to .stringlane/source-stamps.json marks translations as current without a review. The StringLane write guard asks before an agent edits it, as it does for stringlane.yaml. A stringlane accept --yes typed in a shell is not visible to the guard, so review the file in pull requests, and consider adding it to CODEOWNERS.

    Get it as data#

    stringlane check . --json --output stringlane-report.json

    The envelope carries the same figures the human report shows, plus every finding with its code, severity, locale, key and message. Its shape is in the protocol reference.

    --output refuses to write over a file the project loaded as a translation file. A report should never replace your translations.

    Frequently asked questions

    What is the difference between stringlane inspect and stringlane check?
    inspect reports what the project holds: every locale, how complete it is, and which files were read. check reports the same thing plus every validation finding, and sets an exit code. Use inspect to look, check to gate.
    Does a baseline hide localization problems?
    No. Recorded issues are still counted and printed on every run, and the file itself is committed and readable. What a baseline changes is which findings decide the exit code: only issues that are not in it can fail the build. A baseline also 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.
    How does stringlane check find translations that went stale?
    With sourceStamps: true in stringlane.yaml, StringLane keeps .stringlane/source-stamps.json, a committed record of the base text each translation was confirmed against. check then reports translation_stale for a translation whose base text changed since it was confirmed, whoever wrote it, and translation_unconfirmed for one nobody confirmed. stringlane accept confirms a translation once you have checked it. stringlane init turns this on in a new project.

    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