Reference / CLI
verbatra extract
Scan your application source for translation call sites and add the new keys to the source locale file.
Start from your code instead of a locale file. extract walks the source roots you configure, finds the translation call sites in them, and adds every key the source locale file does not have yet. It makes verbatra usable on a project that is not internationalized yet, and it spends nothing: no provider, no API key, no network request.
Synopsis
verbatra extract [flags]Examples
# add every new key found in your source to the source locale file
verbatra extract
# preview the keys that would be added, write nothing
verbatra extract --dry-run
# machine-readable result for a CI step
verbatra extract --jsonFlags
| Flag | Argument | Default | Effect |
|---|---|---|---|
--dry-run | none | off | report what would be added and write nothing |
Also takes the global flags --cwd, --config and --json; the extract roots resolve from the project root.
Behavior
Configuration
extract is driven by the extract block in your config. A run without one fails with EXTRACT_ and exit 2. For i18next-json, verbatra init writes the block as a comment in the generated config; uncomment it and adjust roots.
import { defineConfig } from "@verbatra/cli";
export default defineConfig({
sourceLocale: "en",
targetLocales: ["de", "fr"],
format: "i18next-json",
files: { pattern: "locales/{locale}.json" },
provider: { id: "gemini", options: { model: "gemini-2.5-flash", maxOutputTokens: 4096 } },
extract: {
framework: "i18next",
roots: ["src"],
},
});What it finds
The i18next framework reads the t(...), $t(...), and <object>.t(...) call shapes in .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, and .cts files, each also through an optional call and an explicit type-argument list. The key is the first argument; a default is the second argument or a defaultValue field on the options object:
t("nav.home"); // key only, written with an empty value
t("nav.away", "Away"); // positional default
t("nav.help", { defaultValue: "Help" }); // default on the options object
t?.("nav.back"); // optional call
t<string>("nav.next"); // explicit type argumentComments, strings, and regular expressions are understood, so a call site inside a comment or a quoted t( inside a string is not picked up. A key and a default must each be one complete literal: t("user." + id) is reported as dynamic, and a concatenated default adds the key with no default rather than a truncated one. A key with an empty path segment (t("user."), t(".lead"), t("a..b")) would land under an unnamed child your application never reads, so it is reported as dynamic too.
Namespaces are not resolved yet
A namespace-qualified key such as t("common:nav.home") is reported as dynamic and never written: one verbatra config addresses one catalog file, so there is nowhere for it to go that means what your i18next setup means. If your project names a namespace at every call site, the run adds nothing. extract is useful today on a project that keeps one namespace and writes its keys without the prefix.
What it writes
Only the source locale file, and only keys that are genuinely new:
- A key already in the catalog keeps its value exactly as it stands; a call-site default never overwrites an edit.
- A key the catalog holds only as plural forms counts as present:
t("cart.items", { count })adds nothing next tocart.items_andone cart.items_, and the same holds for a gettextother msgid_entry and an Androidplural <plurals>resource. In a format that does not mark plural forms, such as YAML, every key ending in an i18next plural suffix counts. - A catalog key no call site mentions is left alone.
extractnever deletes;verbatra diff --unusedlists those keys. - A run that finds nothing new writes nothing, so the file stays byte-identical.
- A target locale file is never written. Run
verbatra translateafterwards to fill in the new keys. A key added with an empty value is skipped until you write its source text, and the run names it withSOURCE_.VALUE_ EMPTY
xliff and apple-xcstrings catalogs are never created from nothing: create the source catalog first (the .xcstrings one in Xcode), and extract then appends to it.
What it reports instead of guessing
Everything the scan cannot resolve comes back as data, so one awkward file never aborts a run. None of these change the exit code.
| Reported as | When |
|---|---|
dynamic | the key is not one complete static string, or spells a path verbatra cannot address: a variable, a member expression, a template literal with an expression, a concatenation, a namespace-qualified key, or an empty path segment. It is never written. |
conflicts | one key appears at two call sites with different defaults. Neither value is written, so the catalog never depends on traversal order. |
withoutDefault | a key was added with an empty value because its call site had no default. Fill it in, then translate. |
diagnostics | a file or directory could not be read to its end: it vanished, is over the 2,000,000-byte limit, the scan failed on it, or an unterminated comment, template literal, JSX element or string abandoned the rest. What was read before that point is still reported. |
Scan boundaries
Nothing outside the configured roots is read. node_, .git, .next, .turbo, .verbatra, dist, build, and coverage are always skipped, extract.exclude adds your own directory names, and symbolic links are not followed. The result carries keys, values, and file and line locations only, never a source file's contents.
JSON output
Schema: extract-envelope.json validates the whole envelope; Schemas lists the others.
With --json, the envelope carries one result:
interface ExtractResult {
sourcePath: string; // the source locale file, relative to the project root
scannedFiles: number;
added: { key: string; value: string; file: string; line: number }[];
existingKeys: number; // keys found that were already in the catalog
withoutDefault: string[];
dynamic: { file: string; line: number }[];
conflicts: { key: string; values: string[]; locations: { file: string; line: number }[] }[];
diagnostics: { file: string; reason: "unreadable" | "too-large" | "unparseable" | "unreadable-directory" }[];
written: boolean; // false for a dry run and for a run that added nothing
dryRun: boolean;
}Terminal feedback
The summary goes to stdout. On stderr, a spinner line counts the scanned files; when keys were added, a hint suggests verbatra translate, or after --dry-run the same extract without the flag. --quiet drops the stderr lines:
verbatra: scanning the source... done (0.6s)
verbatra extract
42 files scanned, 18 keys already present
added 3 keys to locales/en.json
new keys (3):
nav.home src/components/Nav.tsx:14
nav.away src/components/Nav.tsx:15
cart.empty src/routes/cart.tsx:31
dynamic keys (1):
src/routes/product.tsx:88
next: verbatra translate (translate the new keys)See Terminal output for color, the spinner and the variables that control them.
Exit codes
| Code | Meaning |
|---|---|
0 | the scan ran, whatever it found |
2 | could not run: a usage error, no extract block, an unresolvable format, an unparseable source catalog, or a source catalog that could not be written |
There is no exit 1: dynamic call sites and conflicting defaults are findings, not failures. See Exit codes for the contract every command follows.
Related
- Extract keys from source code walks through the workflow from call site to translated key.
- The config file documents the
extractblock. verbatra translatefills in the target locales once the new keys exist.verbatra typesdeclares the keys for TypeScript.verbatra checkreports the drift the new keys create.- The SDK exposes the same capability as
extract().