StringLane

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

    Reference

    Validation Code Reference

    Every issue code StringLane reports, its severity, and what it means. The same codes appear in the desktop app Issues Panel, in stringlane check, and in the MCP validation tools.

    Last updated

    Every finding carries a stable code. Branch on that rather than on the message, which is written for a person and may be reworded.

    Severity is what StringLane thinks of the finding. What fails your build is a separate decision: by default any error or warning not in your baseline sets exit 1 and an info finding never does, and --fail-on replaces that rule with the codes you name.

    Errors#

    Something is structurally wrong. These are the findings that break an app rather than read badly.

    CodeMeaning
    missing_keyA key present in the base locale has no value in this locale. Also raised per plural quantity: a language needing many and not having it is missing, even though the key exists. French, Spanish, Italian, Portuguese and Catalan use many only for exact millions such as 1,000,000, so a missing many there is a warning that says so. It is still reported, and i18next shows your fallback language's text for such a count. In an easy_localization project a missing few or many is info and names ignorePluralRules: false, because that package does not read those forms by default. An Android regional folder such as values-fr-rCA/ owes only the strings it defines: Android shows the parent folder's string for the rest
    placeholder_mismatchAn ARB translation's {name} placeholders do not match the template's, in the whole value or in one plural or select branch. Neither direction stops flutter gen-l10n: a dropped placeholder renders the sentence without its value, and an extra one becomes a new parameter on the generated method in every locale, so existing call sites stop compiling
    format_placeholder_mismatchThe same, in each format's own placeholder syntax. On Apple and Android, printf-style specifiers such as %@, %d and %1$s have to match in number and in the type each argument is read as: %d items against %s Elemente is reported, because it crashes on Android and is undefined behaviour on iOS. %ld and %lld are the same type; %d and %ld are not. In i18next JSON the {{name}} names have to match. In easy_localization JSON, {} placeholders are counted, because they are filled in order, and {name} names have to match. In slang JSON, $name, ${name}, {name} and {{name}} names have to match
    malformed_placeholderA placeholder that is not valid syntax for the format at all
    markup_mismatchHTML or XML tags inside the value do not match the source's: a dropped closing tag, or one the source did not have
    icu_parse_errorThe value is not valid ICU MessageFormat at all, so nothing downstream can read it
    icu_missing_plural_categoryAn ICU plural is missing a category this locale's CLDR rules require, whether the plural is the whole value or sits inside text, as in You have {n, plural, one {# item} other {# items}} left. An exact selector covers a category only where the two always mean the same count: =1 stands for German one, but not for Russian one, which also covers 21. A warning when the only missing category is a many used only for exact millions, as in French or Spanish
    icu_plural_selector_unsupportedAn ARB plural uses an exact selector other than =0, =1 or =2, such as =3{three items}. flutter gen-l10n builds only those three as exact forms and fails on the whole file otherwise. Checked in every locale, the base included, and apply refuses a translation that adds one. Use a plural category instead
    select_branch_missingAn ICU select or selectordinal is missing a branch the base declared, or an ordinal branch CLDR requires for this locale. The missing case renders nothing

    Warnings#

    Legal, and probably not what you meant.

    CodeMeaning
    untranslatedThe value is identical to the base locale. Correct for a product name, suspicious for a sentence. It is info instead when at least 3 of your languages keep the same value and they are at least half of the languages that have the key, unless the value reads as a sentence. It is not raised for a regional variant of the base language in the same script, such as en-GB under en, or for a base value made only of placeholders, format specifiers, numbers or punctuation, such as {name}, %lld or %1$lld / %2$lld
    punctuation_mismatchTrailing punctuation differs from the source, or the locale's own convention is broken. French needs a non-breaking space before !, ?, ; and :, and Canadian French (fr-CA, Android values-fr-rCA/ and values-b+fr+CA/) before : only
    whitespace_mismatchLeading or trailing whitespace differs from the source. Invisible in an editor, visible when the string is concatenated
    double_spaceTwo consecutive spaces inside the value
    zero_width_spaceA zero-width character is present. Usually arrived by copy-paste and matches nothing a user can type
    max_length_exceededThe value is longer than the key's max length: x-max-length in an ARB @key block, or the key's entry in .stringlane/metadata.yaml on other formats
    source_length_exceededThe base value already exceeds its own limit, so every translation will too
    source_positional_placeholdersThe base string uses unnamed positional placeholders such as %@ and %d, which a language with different word order cannot reorder. Use indexed forms such as %1$s
    guarded_word_violationA term listed in guardedWords was translated instead of being kept verbatim
    icu_pound_literalA standalone # in an ARB plural branch. Flutter's gen-l10n does not replace # with the number, so the app displays a literal #; write {count} instead. ARB only: i18next-icu does replace it. Deliberate uses such as rank #1 and #{count} are not flagged
    same_plural_formsTwo plural categories in this locale have identical text, which usually means the distinction was not translated. Only languages with a zero, two, few or many form are compared. German, English, Dutch, Swedish and other languages whose only forms are one and other are not, because one {{count} Artikel} other {{count} Artikel} is correct German
    icu_plural_selector_overriddenAn ARB plural holds both =1 and one, =0 and zero, or =2 and two. Flutter treats each pair as one form and keeps the later one, so the earlier branch is never shown. Reported on the dead branch
    count_without_pluralAn i18next base value shows {{count}} (or {{- count}}) beside a word, such as "{{count}} genres", and no locale has plural forms for the key. i18next uses _one and _other forms only when they exist, so the app reads "1 genres". Reported once, on the base locale. A label that shows the number apart from the words, such as Found: {{count}} or Page {{count}}, or a short unit such as {{count}} MB, is not reported. i18next JSON only. Fix it with stringlane pluralize, the MCP pluralize_key tool, or Add plural forms… in the desktop app
    framed_width_variantAn iOS .stringsdict width entry puts its widths under a format variable (NSStringLocalizedFormatKey set to %#@w@) instead of directly under the key. Apple's Foundation does not resolve that layout: the app shows the text %#@w@, and formatting it can crash. Move the widths directly under the key with NSStringVariableWidthRuleType, as Apple documents. StringLane does not change the file
    translation_staleThe base text this translation was confirmed against has changed since. Only with sourceStamps: true in stringlane.yaml. A .xcstrings translation Xcode marks for review is reported here too, even when its stamp is current, and the message says the catalog marks it for review
    translation_unconfirmedNobody confirmed this translation against its base text, or the translation changed after it was confirmed while its base text did not. Only with sourceStamps: true
    ambiguous_origin_prefixTwo origins in a multi-origin project would claim the same merged-key prefix. One is prefixed differently so neither can take the other's keys, and this says which
    duplicate_module_keyThe same key exists in two origins. One of them wins, and this names the winner

    Info#

    Worth a look, never worth failing a build over.

    CodeMeaning
    inconsistent_translationThe same source string is translated differently in different keys
    reused_translationOne translated string is used for several different source strings
    duplicated_wordA word repeats consecutively in the value
    expansion_budget_exceededThe translation is much longer than a translation of a source that length usually needs, so it may break the layout. A budget, not a limit: max_length_exceeded is the limit you set yourself
    select_branch_extraAn ICU select carries a branch the base did not declare. It renders correctly when selected, so this is reported to be seen rather than because it is wrong
    orphan_keyA translation holds a key that the base locale does not. Flutter never generates such a key. Add it to the base, or remove it from the translation
    untranslated (info)The same code as the warning, at info level when most of your languages keep the value: see the Warnings table
    unused_keyNo call site in your source code names this key
    undefined_keyYour source code calls this key and no locale file defines it

    The last two come from the source-code scan rather than from reading your locale files, so they only exist on a run that read your code. They are info deliberately: upgrading the CLI must not turn a pipeline red, and a recorded baseline must not flip. --fail-on unused_key opts in, and --no-scan turns the scan off, but not both, which is refused rather than silently never firing. Neither is a verdict: see Unused keys for the ways a live key lands in that list.

    Stale and unconfirmed translations#

    translation_stale and translation_unconfirmed come from source stamps, not from reading a translation: with sourceStamps: true in stringlane.yaml, StringLane records in .stringlane/source-stamps.json the base text each translation was confirmed against. stringlane check and the MCP inspect_project tool report them; stringlane validate and the MCP validate_translations tool, which check proposed text, do not. Both are warnings, so they fail the build like any other warning, and --fail-on names them. A baseline cannot hold either code: stringlane baseline write leaves them out, and check exits 2 on a baseline that holds one. Confirm a translation that still fits with stringlane accept, or Accept translation in the desktop app. See sourceStamps.

    Choosing what fails CI#

    Start narrow and widen. The errors are the ones that break a running app:

    stringlane check . --fail-on missing_key,placeholder_mismatch,format_placeholder_mismatch,malformed_placeholder,markup_mismatch,icu_plural_selector_unsupported

    If the project already carries a great deal of this, do not encode it in --fail-on. Record a baseline instead, so the debt stays visible and only new findings can fail: see Inspect and check.

    Where these appear#

    The same codes surface in three places, because they come from one validator:

    • the desktop app's Issues Panel, as validation badges;
    • stringlane check, in the terminal and in the --json envelope;
    • the MCP validate_translations and apply_translations tools, which is what stops an agent writing a broken translation.

    Not every code can fire on every format. A format with no native comment syntax cannot raise findings about comments, and format_placeholder_mismatch checks each format's own placeholder syntax, so it fires on Apple, Android, i18next JSON, and easy_localization and slang JSON in their own syntax, and never on ARB, whose {name} placeholders are placeholder_mismatch.

    Frequently asked questions

    Which StringLane issues fail a CI build?
    By default every error or warning that is not in your committed baseline sets exit 1. An info finding never does. Pass --fail-on with a comma-separated list of codes to fail the build on exactly those codes, whatever their severity: it can narrow the gate to a few errors, and it can make an info code such as unused_key fail the build.
    Are the same validation rules used in the app and the CLI?
    Yes. The desktop app, stringlane check and the MCP validate_translations tool are built on one validator over the same files, rather than three implementations of the same rules that drift apart.
    The StringLane app showing the key home.welcomeBack, used in three places in Swift files, with four locale cards: English, German and Japanese in place and French flagged PARAM.

    See every locale side by side

    The desktop app flags what is missing or broken on the cell, and saves to your files.

    Download for macOS

    14-day trial, no email