---
title: "verbatra check"
description: "Report which keys are missing or stale per locale without writing files or calling a provider, and fail CI while any locale is out of sync."
type: "reference"
---
# verbatra check

Report which keys are missing or stale per locale without writing files or calling a provider, and fail CI while any locale is out of sync.

Report, per target locale, how many keys are missing, stale, or up to date, and exit with a code a CI step can branch on. `check` runs the read and diff [`verbatra translate`](https://verbatra.kreitz-webdev.de/docs/cli/translate.md) runs before calling a provider, then stops: no provider call, no file written, no API key needed.

## Synopsis [#synopsis]

```bash
verbatra check [flags]
```

## Examples [#examples]

```bash
# fail the build when translations drifted
verbatra check

# machine-readable status for two locales
verbatra check --locales de,fr --json

# also list source strings translated more than one way
verbatra check --consistency

# fail on broken placeholders, markup and ICU, and on warnings too
verbatra check --qa --strict

# fail while machine translations wait for approval
verbatra check --require-reviewed

# fail on emails, API keys or card numbers in the source
verbatra check --sensitive

# check the one file an editor or agent just changed
verbatra check --file locales/de.json --json
```

## Flags [#flags]

| Flag | Argument | Default | Effect |
| --- | --- | --- | --- |
| `--locales` | `<list>` | all configured targets | check only this comma-separated subset; an unconfigured locale fails with `UNKNOWN_LOCALE` and an empty list with `INVALID_LOCALES`, both exit `2` |
| `--consistency` | none | off | also list source strings translated more than one way; report only, see [Consistency report](#consistency-report) |
| `--qa` | none | off | also run the [quality check](#quality-check) over every committed translation; exit `1` on an error |
| `--file` | `<path>` | off | check only this locale file, see [One file](#one-file); not with `--locales`, `--consistency`, `--require-reviewed` or `--sensitive` |
| `--severity` | `error` or `warning` | `warning` | with `--qa` or `--file`, the lowest severity to report |
| `--strict` | none | off | with `--qa` or `--file`, also exit `1` on a warning or [incomplete plural](#plural-categories); not with `--severity error` |
| `--require-reviewed` | none | off | also exit `1` while a machine-written translation is [not approved](#require-reviewed-translations) |
| `--sensitive` | none | off | also scan the source file and glossary for [sensitive content](#sensitive-content); exit `1` on any finding |

Also takes the [global flags](https://verbatra.kreitz-webdev.de/docs/cli.md#global-flags) `--cwd`, `--config` and `--json`.

A key is missing when the source has it and the target does not, and stale when its source string changed since it was last translated. A locale is in sync when it has neither. For the key lists behind the counts, use [`verbatra diff`](https://verbatra.kreitz-webdev.de/docs/cli/diff.md).

## Behavior [#behavior]

### Consistency report [#consistency-report]

<AvailableFrom version="0.11.0" />

`--consistency` lists, per target locale, every source string that two or more keys translate differently: the source, each distinct translation, and the keys holding it. It never changes the exit code, a value, or the review queue. The comparison rules:

- Values are compared after Unicode NFC normalization, `\r\n` folded to `\n`, and outer whitespace trimmed. Inner whitespace and letter case count.
- Only up-to-date keys are compared, and a translation equal to its own source is left to `EQUALS_SOURCE`.
- Keys whose description, meaning, plural flag, or gettext `msgctxt` differ are never grouped.
- Plural forms are compared per form: the `one` and `other` forms of one key are never grouped with each other.

Known false positives: i18next context variants (`invite_male`, `invite_female`), gettext entries marked `#, fuzzy`, plural-style keys in a format without plurals (`yaml`, `ngx-translate-json`, `xliff`, `properties`, `ini`, `resx`), and the plural forms of a `custom:` format. Give such keys a differing description or meaning, or ignore the finding. With `--json`, each locale carries `inconsistencies: { source, context?, description?, meaning?, isPlural, pluralForm?, translations: [{ value, keys }] }[]`.

### Protected keys [#protected-keys]

<AvailableFrom version="0.12.0" />

A stale key a person wrote, imported, or changed outside verbatra, or a key matching `pinnedKeys`, is one `translate` leaves for a person (see [Protecting human translations](https://verbatra.kreitz-webdev.de/docs/protecting-human-translations.md)). `check` still counts it and shows the protected share, for example `2 stale (1 protected)`. `--json` carries it as `protected` per locale; when `verbatra.provenance.json` is unreadable, only pinned keys count.

### Plural categories [#plural-categories]

<AvailableFrom version="0.12.0" />

Every `check` lists the plurals that lack a CLDR plural category their target language uses, such as a Polish Android `<plurals>` with only `one` and `other`:

```text output
plural categories (warning: exit 1 only under --qa --strict)
  pl: 2 plurals missing CLDR categories
    cart {count} plural: missing few, many
    files: missing few, many
```

The categories come from the CLDR rules built into Node.js (see [language support](https://verbatra.kreitz-webdev.de/docs/language-support.md#plurals)). Formats checked:

| Format | Plural forms read |
| --- | --- |
| `i18next-json` | the `_one`, `_few`, ... key suffixes, and `_ordinal_` plurals against the ordinal rules |
| `android-xml` | the `<item quantity>` entries of each `<plurals>` |
| `apple-strings` | the plural rules in the `.stringsdict` beside the `.strings` file |
| `apple-xcstrings` | the `variations.plural` categories of each locale |
| `next-intl-json`, `arb` | every ICU `plural` and `selectordinal`, including nested ones |

`other` is always required. An exact arm such as `=1` never stands in for a category, `select` arms are never checked, and only a plural the target holds at least one form of is checked. It is a warning: a plain `check` keeps its exit code, and `--qa --strict` fails on it. [`verbatra doctor`](https://verbatra.kreitz-webdev.de/docs/cli/doctor.md) reports the same plurals. With `--json`, each locale carries `incompletePlurals`, ordered by key:

```ts
interface IncompletePlural {
  code: "PLURAL_CATEGORIES_INCOMPLETE";
  key: string;              // base key, or the ICU message's key
  argument?: string;        // the ICU plural's argument, such as count
  ruleType: "cardinal" | "ordinal";
  missing: ("zero" | "one" | "two" | "few" | "many" | "other")[]; // in CLDR order
}
```

### Quality check [#quality-check]

<AvailableFrom version="0.12.0" />

`--qa` runs the checks a translate run applies to provider output over the values already in your locale files, stale or not, including extra plural forms the target language needs. Findings come in two severities:

- **error**: the value fails the [integrity gate](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#the-integrity-gate) with `placeholder`, `markup`, `icu`, `degenerate`, or `empty`. Details list dropped tokens as `-{{name}}` and added ones as `+{{nom}}`.
- **warning**: the value carries a [review reason](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#review-flags) computed from the value, its source and your config (glossary, `maxLength`), so none that depends on the provider or the cache; for `FOREIGN_PLACEHOLDER_CHANGED`, the finding's `details` name each dropped or changed token, such as `-{name}`. A value with an error gets no warnings.

```text output
qa: 1 error, 1 warning
  de: 1 error, 1 warning in 42 values checked
    greeting: error placeholder (-{{name}}, +{{nme}})
    title: warning EQUALS_SOURCE
```

Orphaned and missing keys are skipped, and source keys that are not valid ICU are listed once under `invalidSourceKeys`. Protected, pinned and rejected values are checked like any other. With `--json`, `result.qa` holds the totals and each locale its own `qa`:

```ts
interface CheckQaSummary { errors: number; warnings: number; invalidSourceKeys: string[] }
interface LocaleQaReport { checked: number; errors: number; warnings: number; findings: QaFinding[] }
type QaFinding =
  | { key: string; severity: "error"; reason: "placeholder" | "markup" | "icu" | "degenerate" | "empty"; details?: string[] }
  | { key: string; severity: "warning"; reason: string; details?: string[] }; // one of the review reasons above
```

### Require reviewed translations [#require-reviewed-translations]

<AvailableFrom version="0.12.0" />

`--require-reviewed` fails with exit `1` while a value whose [origin](https://verbatra.kreitz-webdev.de/docs/the-lock-file.md#provenance-who-wrote-each-value) is `machine`, `memory`, `fuzzy`, or `agent` is not approved in the committed `verbatra.provenance.json`, where [Studio](https://verbatra.kreitz-webdev.de/docs/review-in-studio.md) and the [MCP tools](https://verbatra.kreitz-webdev.de/docs/cli/mcp.md) record decisions. An approval from before the source text changed, or before the value changed, does not count.

```text output
review: failed [REVIEW_REQUIRED] 2 machine-written translations not approved
  de: 2 unreviewed: checkout.title, checkout.total
```

With `--json`, `result.review` is `{ reviewed, unreviewed, code? }`, where `code` is `REVIEW_REQUIRED`, or `REVIEW_STATE_UNREADABLE` when the provenance file is corrupt or from a newer verbatra (the gate then fails). Each locale carries `review.unreviewed`, the keys in source order.

### Sensitive content [#sensitive-content]

<AvailableFrom version="0.12.0" />

`--sensitive` runs the [sensitive content guard](https://verbatra.kreitz-webdev.de/docs/data-handling.md#sensitive-content-guard) scan over each source key (key name, value, description, meaning) and the glossary of each checked locale, with the detectors, patterns and allow list of `sensitiveData` (default detectors without the block), whatever its `mode` and provider, `none` included: values still leave the machine through export, XLIFF and TMX handoffs. It reports keys and detectors, never matched text outside a key name, and fails with exit `1` on any finding.

The `email` detector skips [reserved example domains](https://verbatra.kreitz-webdev.de/docs/data-handling.md#sensitive-content-guard), so test with a real-looking address.

```text output
sensitive: 1 key and 0 glossary terms hold content that looks sensitive
  support.contact: email in value
```

With `--json`, `result.sensitive` is `{ findings, glossaryTerms }`, each finding `{ key, fields, detectors }`; `detectors` may hold `pattern-timeout-<n>`.

### One file [#one-file]

<AvailableFrom version="0.12.0" />

`--file <path>` checks one locale file, for editor and agent hooks after an edit. It reads that file, `verbatra.limits.json` <AvailableFrom version="0.13.0" /> (if unusable, ignored with a notice on stderr, as under `--qa`; [`doctor`](https://verbatra.kreitz-webdev.de/docs/cli/doctor.md) names the problem) and, for a target locale, the source file, nothing else. The path is resolved against `--cwd` and matched against `files.pattern` to find its locale.

- A file that does not parse is one `syntax` error with the adapter's code (`INVALID_JSON`, `INVALID_YAML`, ...) and the line and column when the parser reports a position.
- A target file is checked like [`--qa`](#quality-check), plural categories included; a source file for syntax only. An `apple-xcstrings` catalogue is checked for every target locale.
- A path that is no configured locale's file fails with `NOT_A_LOCALE_FILE` and exit `2`.

With `--json`, `result` reuses the `--qa` report, so a hook parses one shape:

```ts
interface CheckFileSummary {
  file: string;                          // relative to the project root
  role: "source" | "target" | "catalogue";
  locales: {
    locale: string;
    incompletePlurals: IncompletePlural[];
    qa: { checked: number; errors: number; warnings: number; findings: (QaFinding | QaSyntaxFinding)[] };
  }[];
  qa: CheckQaSummary;
}
interface QaSyntaxFinding {
  severity: "error"; reason: "syntax"; code: string; message: string;
  line?: number; column?: number;       // when the parser reports a position, counted from 1
}
```

Missing and stale keys are not part of a file check.

### JSON output [#json-output]

Schema: [`check-envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/check-envelope.json) validates the whole envelope; [Schemas](https://verbatra.kreitz-webdev.de/docs/cli/output.md#schemas) lists the others.

`--json` prints one [envelope](https://verbatra.kreitz-webdev.de/docs/cli/output.md#json-output) whose `result` is the status document. Without `--qa`, `--require-reviewed` or `--sensitive`, `inSync` is true exactly when the command exits `0`:

```ts
interface CheckSummary {
  inSync: boolean;
  locales: LocaleCheckSummary[];
  qa?: CheckQaSummary;          // only with --qa
  review?: CheckReviewSummary;  // only with --require-reviewed
  sensitive?: CheckSensitiveSummary; // only with --sensitive
}

interface LocaleCheckSummary {
  locale: string;
  missing: number;              // in source, absent from target
  stale: number;                // source changed since last translated
  upToDate: number;
  inSync: boolean;              // missing === 0 && stale === 0
  emptySource?: number;         // source value empty: never missing or stale
  provenance?: ProvenanceSummary; // counts by origin and review state
  protected?: number;
  incompletePlurals?: IncompletePlural[];
  inconsistencies?: InconsistencyGroup[]; // only with --consistency
  qa?: LocaleQaReport;          // only with --qa
  review?: LocaleReviewReport;  // only with --require-reviewed
}
```

A source key whose value is empty or whitespace only, such as one `extract` added without a default, has nothing to translate yet: it is counted in `emptySource` instead of `missing` or `stale`, never makes a locale out of sync, and the terminal output names the count.

### Terminal feedback [#terminal-feedback]

<AvailableFrom version="0.12.0" />

`check` shows `verbatra: checking the locales... done (0.1s)` on stderr, or `verbatra: checking locales/de.json...` with `--file`. `--quiet` drops it. See [Terminal output](https://verbatra.kreitz-webdev.de/docs/cli/output.md#terminal-output).

## Exit codes [#exit-codes]

| Code | Meaning |
| --- | --- |
| `0` | in sync, and no finding the flags given turn into a failure |
| `1` | a locale has a missing or stale key, `--qa` or `--file` found an error (or a warning or incomplete plural under `--strict`), `--require-reviewed` found an unapproved translation, or `--sensitive` found sensitive content |
| `2` | could not run: a whole-run error such as `SOURCE_UNREADABLE` or `NOT_A_LOCALE_FILE`, or a usage error such as `INVALID_QA_OPTION`, `INVALID_SEVERITY`, or `INVALID_OPTION` for an empty `--file` or one combined with `--locales`, `--consistency`, `--require-reviewed` or `--sensitive` |

Exit `1` means the result is not clean, not that the run failed; the report is still printed. On a project whose source file is not in place yet, [`verbatra doctor`](https://verbatra.kreitz-webdev.de/docs/cli/doctor.md) still reports. See [Exit codes](https://verbatra.kreitz-webdev.de/docs/cli/output.md#exit-codes) for the contract.

## Related [#related]

- [`verbatra diff`](https://verbatra.kreitz-webdev.de/docs/cli/diff.md) lists the exact keys behind these counts.
- [CI and exit codes](https://verbatra.kreitz-webdev.de/docs/ci-and-exit-codes.md) shows `check` as a drift gate.
- [The lock file](https://verbatra.kreitz-webdev.de/docs/the-lock-file.md) explains how staleness is detected.
