---
title: "verbatra extract"
description: "Scan your application source for translation call sites and add the new keys to the source locale file."
type: "reference"
---
# verbatra extract

Scan your application source for translation call sites and add the new keys to the source locale file.

<AvailableFrom version="0.11.0" />

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 [#synopsis]

```bash
verbatra extract [flags]
```

## Examples [#examples]

```bash
# 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 --json
```

## Flags [#flags]

| Flag | Argument | Default | Effect |
| --- | --- | --- | --- |
| `--dry-run` | none | off | report what would be added and write nothing |

Also takes the [global flags](https://verbatra.kreitz-webdev.de/docs/cli.md#global-flags) `--cwd`, `--config` and `--json`; the `extract` roots resolve from the [project root](https://verbatra.kreitz-webdev.de/docs/config-file.md#the-working-directory-and-env).

## Behavior [#behavior]

### Configuration [#configuration]

`extract` is driven by the [`extract` block](https://verbatra.kreitz-webdev.de/docs/config-file.md#extract) in your config. A run without one fails with `EXTRACT_NOT_CONFIGURED` and exit `2`. For `i18next-json`, `verbatra init` writes the block as a comment in the generated config; uncomment it and adjust `roots`.

```ts title="verbatra.config.ts"
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 [#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:

```ts
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 argument
```

Comments, 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 [#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 [#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 to `cart.items_one` and `cart.items_other`, and the same holds for a gettext `msgid_plural` entry and an Android `<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. `extract` never deletes; [`verbatra diff --unused`](https://verbatra.kreitz-webdev.de/docs/cli/diff.md#unused-source-keys) lists 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 translate`](https://verbatra.kreitz-webdev.de/docs/cli/translate.md) afterwards 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 with [`SOURCE_VALUE_EMPTY`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_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 [#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 [#scan-boundaries]

Nothing outside the configured `roots` is read. `node_modules`, `.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 [#json-output]

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

With `--json`, the [envelope](https://verbatra.kreitz-webdev.de/docs/cli/output.md#json-output) carries one result:

```ts
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 [#terminal-feedback]

<AvailableFrom version="0.12.0" />

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:

```text output
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](https://verbatra.kreitz-webdev.de/docs/cli/output.md#terminal-output) for color, the spinner and the variables that control them.

## Exit codes [#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](https://verbatra.kreitz-webdev.de/docs/cli/output.md#exit-codes) for the contract every command follows.

## Related [#related]

- [Extract keys from source code](https://verbatra.kreitz-webdev.de/docs/extract-keys.md) walks through the workflow from call site to translated key.
- [The config file](https://verbatra.kreitz-webdev.de/docs/config-file.md#extract) documents the `extract` block.
- [`verbatra translate`](https://verbatra.kreitz-webdev.de/docs/cli/translate.md) fills in the target locales once the new keys exist.
- [`verbatra types`](https://verbatra.kreitz-webdev.de/docs/cli/types.md) declares the keys for TypeScript.
- [`verbatra check`](https://verbatra.kreitz-webdev.de/docs/cli/check.md) reports the drift the new keys create.
- [The SDK](https://verbatra.kreitz-webdev.de/docs/sdk/inspect.md#extract) exposes the same capability as `extract()`.
