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

Source: https://stringlane.app/docs/reference/validation-codes
Last updated: 2026-09-14

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 finding not in your baseline sets exit 1, and `--fail-on` narrows that to the codes you name.

## Errors

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

| Code | Meaning |
|---|---|
| `missing_key` | A 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 |
| `placeholder_mismatch` | The translation's placeholders do not match the source's. A missing or invented `{name}` is a crash or a literal brace in the UI |
| `format_placeholder_mismatch` | The same, for printf-style format specifiers such as `%@`, `%d`, `%1$s`. Type and count both have to line up |
| `malformed_placeholder` | A placeholder that is not valid syntax for the format at all |
| `markup_mismatch` | HTML 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_error` | The value is not valid ICU MessageFormat at all, so nothing downstream can read it |
| `icu_missing_plural_category` | An ICU plural is missing a category this locale's CLDR rules require |
| `select_branch_missing` | An 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.

| Code | Meaning |
|---|---|
| `untranslated` | The value is identical to the base locale. Correct for a product name, suspicious for a sentence |
| `punctuation_mismatch` | Trailing punctuation differs from the source, or the locale's own convention is broken — French needs a non-breaking space before `!` and `:` |
| `whitespace_mismatch` | Leading or trailing whitespace differs from the source. Invisible in an editor, visible when the string is concatenated |
| `double_space` | Two consecutive spaces inside the value |
| `zero_width_space` | A zero-width character is present. Usually arrived by copy-paste and matches nothing a user can type |
| `max_length_exceeded` | The value is longer than the `keyConstraints` limit set for this key |
| `source_length_exceeded` | The **base** value already exceeds its own limit, so every translation will too |
| `source_positional_placeholders` | The 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_violation` | A term listed in `guardedWords` was translated instead of being kept verbatim |
| `icu_pound_literal` | A literal `#` appears inside an ICU plural where it will be substituted for the number |
| `same_plural_forms` | Two plural categories in this locale have identical text, which usually means the distinction was not translated |
| `ambiguous_origin_prefix` | Two 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_key` | The 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.

| Code | Meaning |
|---|---|
| `inconsistent_translation` | The same source string is translated differently in different keys |
| `reused_translation` | One translated string is used for several different source strings |
| `duplicated_word` | A word repeats consecutively in the value |
| `expansion_budget_exceeded` | The 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_extra` | An 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 |
| `unused_key` | No call site in your source code names this key |
| `undefined_key` | Your 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](/docs/cli/unused-keys) for the four ways a live key lands in that list.

## Choosing what fails CI

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

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

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](/docs/cli/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](/docs/read-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` is about printf-style specifiers, so it is an Apple and Android concern rather than an ARB one.

## Frequently asked questions

### Which StringLane issues fail a CI build?

By default every finding that is not in your committed baseline sets exit 1, whatever its severity. Pass --fail-on with a comma-separated list of codes to narrow that to the ones you care about; it can only soften the result, never harden it.

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