Skip to content
Skip to Content

Reference / CLI

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, then stops: no provider call, no file written, no lock file update, no API key needed.

Synopsis

verbatra diff [flags]

Examples

# 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

FlagArgumentDefaultEffect
--locales<list>all configured targetsdiff only this comma-separated subset; an unconfigured locale fails with UNKNOWN_LOCALE and an empty list with INVALID_LOCALES, both exit 2
--unusednoneoffalso list the source-locale keys no call site under the extract roots names; see Unused source keys

Also takes the global flags --cwd, --config and --json.

Behavior

Pending and orphaned keys

Each locale lists four kinds of key:

ListMeaning
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
orphanedin 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 removes them. Where verbatra check gives the counts, diff gives the key lists.

Unused source keys

--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 with the reader verbatra extract 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):

ReasonstatusWhen
dynamic-keysunreliablea key with no static part: t(key), a template literal with no static head, or a concatenation
dynamic-key-prefixunreliablea keyPrefix or getFixedT prefix that is not a static string
trans-without-keyunreliablea Trans element without a static i18nKey
aliased-translate-functionunreliablet assigned to anything but a plain variable, such as this.tr = t
translate-function-escapesunreliablea 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-sourceunreliablea 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-scannedunreliable.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-scanunreliablea 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_CONFIGUREDnot-runno extract block
EXTRACT_FS_UNSUPPORTEDnot-runan SDK caller's file system cannot list directories
FRAMEWORK_NOT_MODELEDnot-runthe format is next-intl-json, vue-i18n-json, or ngx-translate-json, whose runtimes the scan does not model
NO_SOURCE_FILESnot-runthere is no source file under the roots
NO_REFERENCES_FOUNDnot-runthe 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: a pattern matches the decoded key, and a matched key is listed under ignored, never dropped.

Protected keys

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

Schema: diff-envelope.json validates the whole envelope; Schemas lists the others.

--json prints one envelope whose result is a DiffSummary. hasPendingChanges is true exactly when the command exits 1 for pending keys:

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

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

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.

Exit codes

CodeMeaning
0no locale has a key to add or re-translate, and no complete --unused scan found an unused key
1at least one locale has a missing or changed key, or a complete --unused scan found an unused key
2could 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.

Edit on GitHub

On this page