---
title: "verbatra diff"
description: "Show the keys a run would add or re-translate per locale, and the orphaned keys it would leave, without writing files or calling a provider."
type: "reference"
---
# verbatra diff

Show the keys a run would add or re-translate per locale, and the orphaned keys it would leave, without writing files or calling a provider.

List the exact pending keys per target locale: the ones a run would add, the ones it would re-translate, and the ones now orphaned. `diff` runs the same read and diff as [`verbatra translate`](https://verbatra.kreitz-webdev.de/docs/cli/translate.md), then stops: no provider call, no file written, no [lock file](https://verbatra.kreitz-webdev.de/docs/the-lock-file.md) update, no API key needed.

## Synopsis [#synopsis]

```bash
verbatra diff [flags]
```

## Examples [#examples]

```bash
# list the pending keys per locale (exit 1 if any are pending)
verbatra diff

# only the German and French locales
verbatra diff --locales de,fr

# machine-readable key lists for tooling or a PR comment
verbatra diff --json

# also list the source keys your code no longer names
verbatra diff --unused
```

## Flags [#flags]

| Flag | Argument | Default | Effect |
| --- | --- | --- | --- |
| `--locales` | `<list>` | all configured targets | diff only this comma-separated subset; an unconfigured locale fails with `UNKNOWN_LOCALE` and an empty list with `INVALID_LOCALES`, both exit `2` |
| `--unused` | none | off | also list the source-locale keys no call site under the [`extract` roots](https://verbatra.kreitz-webdev.de/docs/config-file.md#extract) names; see [Unused source keys](#unused-source-keys) |

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

## Behavior [#behavior]

### Pending and orphaned keys [#pending-and-orphaned-keys]

Each locale lists four kinds of key:

| List | Meaning |
| --- | --- |
| `missing` (shown as `add`) | in the source, absent from the target: a run would add it |
| `changed` (shown as `re-translate`) | the source text changed since the last translation: a run would re-translate it |
| `orphaned` | in the target, gone from the source: reported only |
| `emptySource` (shown as `empty source`) | the source value is empty or whitespace only: nothing to translate until you write it, never pending |

A locale has pending changes when it has at least one missing or changed key. Orphaned keys never count as pending, because a default run leaves them in place; only [`verbatra translate --prune`](https://verbatra.kreitz-webdev.de/docs/cli/translate.md) removes them. Where [`verbatra check`](https://verbatra.kreitz-webdev.de/docs/cli/check.md) gives the counts, `diff` gives the key lists.

### Unused source keys [#unused-source-keys]

<AvailableFrom version="0.11.0" />

`--unused` answers a different question from `orphaned`: which keys in the source locale file does your application no longer name? It scans the `roots` of the [`extract` block](https://verbatra.kreitz-webdev.de/docs/config-file.md#extract) with the reader [`verbatra extract`](https://verbatra.kreitz-webdev.de/docs/cli/extract.md) uses and lists every source key no static reference names. The two lists are never merged. The scan stays read-only: nothing is removed, and no API key is read. Deleting a key is your call.

The scan models the i18next runtime. These count as a reference:

- a `t(...)`, `$t(...)` or `<object>.t(...)` call anywhere under the roots, also through an alias such as `const { t: translate } = useTranslation()`, `const tr = i18n.t`, `t.bind(i18n)` or `i18n.getFixedT(...)`;
- a static `keyPrefix` or third `getFixedT` argument, which also covers the prefixed form of every static key in that file, and a static `i18nKey` on `Trans` or in an object;
- a namespace-qualified key (`common:nav.home` covers `nav.home` too), a natural-language key, and every entry of a static key array;
- a plural or context variant of a referenced key (`t("items", { count })` covers `items_one` and `items_other`), and every key under a referenced parent.

Keys are compared in the catalog format's own encoding, so a flat dotted JSON key, a gettext plural or `msgctxt` entry, and an Android `<plurals>` entry all match the key your code names. A template literal with a static head, such as `` t(`nav.${page}`) ``, lists the keys under `nav.` as `possiblyDynamic`, apart from `unused`, and never fails the run.

The report carries a `status`: `complete` (every reachable key was bounded, so `unused` is trustworthy), `unreliable` (a listed key may still be in use; `unreliableBecause` names each reason with its count and sites) or `not-run` (nothing is listed; `reason` says why):

| Reason | `status` | When |
| --- | --- | --- |
| `dynamic-keys` | `unreliable` | a key with no static part: `t(key)`, a template literal with no static head, or a concatenation |
| `dynamic-key-prefix` | `unreliable` | a `keyPrefix` or `getFixedT` prefix that is not a static string |
| `trans-without-key` | `unreliable` | a `Trans` element without a static `i18nKey` |
| `aliased-translate-function` | `unreliable` | `t` assigned to anything but a plain variable, such as `this.tr = t` |
| `translate-function-escapes` | `unreliable` | a translate function used anywhere but a direct call, its binding, a local alias, `t={t}` on `Trans` or `Translation`, or a hook dependency array: `renderRow(t)`, `{ t }`, `return t`, `export default t` |
| `unrecognised-translate-source` | `unreliable` | a `t` or `$t` from a source the scan does not model, such as a wrapper hook returning `useTranslation(...)`, hook options built with a spread, or an exported `getFixedT` result |
| `template-files-not-scanned` | `unreliable` | `.vue`, `.svelte`, `.html`, `.htm`, `.astro`, `.hbs`, `.handlebars`, `.ejs`, `.pug`, `.njk`, `.liquid`, or `.mdx` files under the roots (the first 20 are listed, all are counted) |
| `incomplete-scan` | `unreliable` | a file that is too large, unreadable, or not readable to its end (also an unclosed JSX element or a quoted string running into the end of its line), or a directory that cannot be listed |
| `EXTRACT_NOT_CONFIGURED` | `not-run` | no `extract` block |
| `EXTRACT_FS_UNSUPPORTED` | `not-run` | an SDK caller's file system cannot list directories |
| `FRAMEWORK_NOT_MODELED` | `not-run` | the format is `next-intl-json`, `vue-i18n-json`, or `ngx-translate-json`, whose runtimes the scan does not model |
| `NO_SOURCE_FILES` | `not-run` | there is no source file under the roots |
| `NO_REFERENCES_FOUND` | `not-run` | the scanned files name no key and hold no dynamic key site while the catalog is not empty, which usually means the roots miss your code |

The recognised translate sources are `useTranslation(...)` destructured into a local, not exported binding with static arguments; a local `i18n.getFixedT(...)` with static arguments; a direct call on a member `t` (`i18n.t("key")`, `this.props.t("key")`) and its local alias; `<Translation>{(t) => ...}</Translation>`; `withTranslation("ns")(Component)` with `props.t(...)`; and `import { t } from "i18next"`. Any variable or member named `t` or `$t` is judged, so an unrelated `frame.t + 1` can make a report unreliable. In `.tsx`, `.jsx` and `.js` files, JSX text and quoted attribute values are read as markup, never as code.

Each listed key shows its decoded spelling (a gettext `msgctxt` entry as `menu|Open`); `--json` also carries the `catalogKey` as the adapter spells it. A key used only outside the scanned source (a server template, a CMS) belongs in [`extract.unused.ignore`](https://verbatra.kreitz-webdev.de/docs/config-file.md#extract): a pattern matches the decoded key, and a matched key is listed under `ignored`, never dropped.

### Protected keys [#protected-keys]

<AvailableFrom version="0.12.0" />

Missing and changed keys that `translate` would leave for a person (a person wrote, imported or changed the value outside verbatra, or the key matches `pinnedKeys`) are listed again under `protected`; see [Protecting human translations](https://verbatra.kreitz-webdev.de/docs/protecting-human-translations.md). They stay in `add` and `re-translate`, since they are still pending. When `verbatra.provenance.json` is corrupt or from a newer verbatra, only pinned keys are listed.

### JSON output [#json-output]

Schema: [`diff-envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/diff-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 a `DiffSummary`. `hasPendingChanges` is true exactly when the command exits `1` for pending keys:

```ts
interface DiffSummary {
  hasPendingChanges: boolean;
  locales: LocaleDiff[];
  unused?: UnusedKeysReport;   // only with --unused
}

interface LocaleDiff {
  locale: string;
  missing: string[];           // would be added
  changed: string[];           // would be re-translated
  orphaned: string[];          // reported only
  emptySource?: string[];      // source value empty: never pending
  hasPendingChanges: boolean;  // missing.length > 0 || changed.length > 0
  changedOrigins?: Record<string, KeyOrigin>; // absent when the provenance file is unreadable
  protected?: string[];        // pending keys translate would leave alone
}
```

### Terminal feedback [#terminal-feedback]

<AvailableFrom version="0.12.0" />

`diff` shows one line while it works (with a running file count when `--unused` scans the source) and suggests the next command:

```text output
verbatra: diffing and scanning the source... done (0.8s)
next: verbatra translate (send the pending keys to your provider)
```

When every pending key is protected, it suggests `verbatra studio` instead; under `provider: { id: "none" }` it suggests `verbatra export`. These lines go to stderr, and `--quiet` drops them. See [Terminal output](https://verbatra.kreitz-webdev.de/docs/cli/output.md#terminal-output).

## Exit codes [#exit-codes]

| Code | Meaning |
| --- | --- |
| `0` | no locale has a key to add or re-translate, and no complete `--unused` scan found an unused key |
| `1` | at least one locale has a missing or changed key, or a `complete` `--unused` scan found an unused key |
| `2` | could not run: a whole-run error, an unconfigured `--locales` value (`UNKNOWN_LOCALE`), or a usage error such as an empty `--locales` list (`INVALID_LOCALES`) |

Orphaned, possibly dynamic and ignored keys, and an `unreliable` or `not-run` report, never cause `1` on their own. See [the exit-code contract](https://verbatra.kreitz-webdev.de/docs/cli/output.md#exit-codes).

## Related [#related]

- [`verbatra check`](https://verbatra.kreitz-webdev.de/docs/cli/check.md) gives the per-locale counts behind these lists.
- [`verbatra translate`](https://verbatra.kreitz-webdev.de/docs/cli/translate.md) applies the pending changes; `--prune` removes orphaned keys.
- [The config file](https://verbatra.kreitz-webdev.de/docs/config-file.md#extract) documents the `extract` block and `unused.ignore`.
- [CI and exit codes](https://verbatra.kreitz-webdev.de/docs/ci-and-exit-codes.md) shows `diff` in a pipeline.
