---
title: "verbatra doctor"
description: "Check whether a project is set up correctly and get every problem in one report, without calling a provider or reading an API key."
type: "reference"
---
# verbatra doctor

Check whether a project is set up correctly and get every problem in one report, without calling a provider or reading an API key.

<AvailableFrom version="0.9.0" />

Check whether a project is set up correctly and get every problem in one report. `doctor` constructs no provider, writes no file, and reads an API key value only for `--live`, which sends it in one request for the provider's language list (LibreTranslate gets none). Run it after [`verbatra init`](https://verbatra.kreitz-webdev.de/docs/cli/init.md) or when another command fails.

## Synopsis [#synopsis]

```bash
verbatra doctor [flags]
```

## Examples [#examples]

```bash
# report every setup problem at once
verbatra doctor

# validate a project in another directory, with an explicit config
verbatra doctor --cwd apps/web --config verbatra.config.ts

# machine-readable report for a CI preflight step
verbatra doctor --json

# list hardcoded user-facing strings, with no key set
verbatra doctor --literals

# what the provider supports for each target locale
verbatra doctor --locales

# the same, against the provider's current language list
verbatra doctor --live

# a data-flow manifest to attach to a DPA or NDA review
verbatra doctor --data-flow --json
```

## Flags [#flags]

| Flag | Argument | Default | Effect |
| --- | --- | --- | --- |
| `--literals` | none | off | scan the [`extract`](https://verbatra.kreitz-webdev.de/docs/config-file.md#extract) source roots for hardcoded user-facing strings instead of checking the setup (see [Untranslated literals](#untranslated-literals)) |
| `--locales` | none | off | after the checks, list per target locale the code sent, provider support, glossary and formality (see [Locale support](#locale-support)) |
| `--live` | none | off | fetch the provider's current language list first, using no translation quota; implies `--locales` |
| `--data-flow` | none | off | describe what is sent to which host and what is written locally instead of checking the setup (see [Data flow](#data-flow)) |

Also takes the [global flags](https://verbatra.kreitz-webdev.de/docs/cli.md#global-flags) `--cwd`, `--config` and `--json`. `--locales` and `--live` cannot be combined with `--literals` (`INVALID_OPTION`, exit `2`), and `--data-flow` with none of the three.

## Behavior [#behavior]

### What it checks [#what-it-checks]

<AvailableFrom version="0.12.0" />

A setup run reports these checks, in this order. Plural rules, Plural completeness, Locale codes, Locale state and Length limits are informational and never fail: each prints `[warn]` when it names something and `[ok  ]` otherwise.

| Check | Fails when |
| --- | --- |
| Configuration | no config file is found, or it fails validation |
| Format adapter | the configured `format` resolves to no file adapter |
| Provider | the configured `provider.id` resolves to no provider factory |
| API key environment variable | the variable that provider reads its key from is not set |
| Network policy | the effective [network policy](https://verbatra.kreitz-webdev.de/docs/network-policy.md) refuses the provider's host or a proxy it would use |
| Source locale file | the source locale file is missing at its resolved path, is not a regular file, or does not parse under the configured format |
| Plural rules | never; names the ICU and CLDR versions in use, and warns when a target locale has no ICU plural rules |
| Plural completeness | never; warns and names each target plural that lacks a CLDR category its language uses, judging a plural the target has no forms for by what a run would write there (a blank source form counts as absent, and plural generation only where it actually runs), deliberately stricter than the i18next-only notice of `translate` (at most ten; [`verbatra check`](https://verbatra.kreitz-webdev.de/docs/cli/check.md#plural-categories) lists the gaps in plurals a target already holds); skipped for a format that does not store plural forms by CLDR category |
| Locale codes | never; warns and names every valid but non-canonical code, such as `en-us` or `iw`, with the canonical form (see [Locale codes](https://verbatra.kreitz-webdev.de/docs/config-file.md#locale-codes)) |
| Locale state | never; warns and names every locale the lock file, cache or provenance file holds state for that the config does not list, and whether `translate` [carries it over](https://verbatra.kreitz-webdev.de/docs/the-lock-file.md#a-respelled-locale-code) |
| Length limits | never; warns when `verbatra.limits.json` cannot be used (a run ignores it and a limit cannot be set until it is fixed), names a limit for a key the source no longer has or one the config's `maxLength` overrides, and says so when the source could not be read to check the keys; passes when the file is absent ([length limits](https://verbatra.kreitz-webdev.de/docs/config-file.md#maxlength)) <AvailableFrom version="0.13.0" /> |
| Locale support | the provider does not support the source or a target locale; warns when a locale carries a warning (see [Locale support](#locale-support)) |

Every check runs even when an earlier one failed. The one exception is the config: when it cannot be loaded, the checks that need it print as `[skip]` (`"status": "skipped"` in JSON), as does Plural completeness for a format without CLDR plural forms. A check that found something worth attention without failing prints `[warn]` (`"status": "warn"` in JSON) and never changes the exit code. A failed check also carries a `fix`, one imperative next step printed as a `fix:` line and sent as `"fix"` in JSON, so an agent can act without parsing `detail`.

A run with two problems:

```text output
verbatra doctor
  [ok  ] Configuration: Loaded /app/verbatra.config.ts.
  [ok  ] Format adapter: Format "i18next-json" resolves to an adapter.
  [ok  ] Provider: Provider "anthropic" resolves to a factory.
  [fail] API key environment variable: The ANTHROPIC_API_KEY environment variable is not set.
         fix: Set ANTHROPIC_API_KEY in the environment, or, with the CLI, in a .env file in the project directory.
  [ok  ] Network policy: any host (config: unset; VERBATRA_NETWORK_POLICY: unset). Provider "anthropic" connects to api.anthropic.com: permitted.
  [fail] Source locale file: The source locale file was not found at /app/locales/en.json.
         fix: Create the source locale file, or fix `files.pattern` and `sourceLocale` in the config so they point at it.
  ...
  [warn] Locale codes: "iw" is canonically "he". Consider the canonical form in your config; file names follow the configured code and are never renamed for you.
  [ok  ] Locale support: Provider "anthropic" is an LLM and accepts any locale (well-tested list of 2026-09-28).
2 problems found (run verbatra doctor again after fixing them)
```

### Keys and files [#keys-and-files]

- The API key is checked **by name only**: `doctor` asks whether the variable is set, never what it holds. See [Providers](https://verbatra.kreitz-webdev.de/docs/providers.md) for each provider's variable.
- `openai-compatible` falls back to a placeholder key, so it fails only when the config names its own `provider.options.apiKeyEnvVar` and that variable is unset. `libretranslate` never fails the check, since its key is optional.
- With [`provider: { id: "none" }`](https://verbatra.kreitz-webdev.de/docs/human-only-workflow.md), the Provider, API key and Network policy checks pass and Locale support is skipped.
- A missing **target** file is fine: `translate` creates it. The **source** file is parsed, so a directory, an empty file or malformed content fails it.
- `doctor` loads the [environment files](https://verbatra.kreitz-webdev.de/docs/cli.md#environment-files) first, so a key in a dotenv file counts as set; with `--literals` it loads neither.

### Locale support [#locale-support]

<AvailableFrom version="0.12.0" />

The Locale support check asks whether the provider can translate each configured locale. DeepL and Google Cloud Translation are judged against a dated language table verbatra ships; an LLM provider accepts any locale. It fails on exactly the locale [`verbatra translate`](https://verbatra.kreitz-webdev.de/docs/cli/translate.md) would refuse with `LOCALE_UNSUPPORTED_BY_PROVIDER`, and otherwise prints `[warn]` when a locale carries a warning or `--live` could not fetch the list. [Language support](https://verbatra.kreitz-webdev.de/docs/language-support.md#checking-support-before-a-run) explains the verdicts.

`--locales` prints the verdict per locale after the checks:

```text output
  locale support (deepl, language table of 2026-09-28, static)
    en  sent as EN  source, supported
    de  sent as DE  supported  glossary: yes  formality: yes
    sv  sent as SV  supported  glossary: yes  formality: no
      warning [FORMALITY_UNSUPPORTED_BY_PROVIDER] Provider "deepl" has no formality control for "sv", so the configured formal tone is not applied and the provider's default register is used.
    chr  sent as CHR  unsupported  glossary: no  formality: no
```

Each line names the locale, the code the provider receives (after [`localeMap`](https://verbatra.kreitz-webdev.de/docs/providers.md#locale-codes-sent-to-the-provider), marked `(localeMap)`), the verdict, and whether a glossary and a `formal` or `informal` tone apply. A warning is advisory.

`--live` judges against the provider's current list instead (DeepL `/v3/languages`, Cloud Translation `languages`, LibreTranslate `/languages`). For LibreTranslate, which ships no table, it is the only way to get a verdict other than `unverified`, so Locale support shows `[warn]` until `--live` runs. The request is sent only when the key variable is set (LibreTranslate needs none) and the [network policy](https://verbatra.kreitz-webdev.de/docs/network-policy.md) permits the host; otherwise, or on failure, the shipped table is used and a `live language list` line says why.

### Untranslated literals [#untranslated-literals]

<AvailableFrom version="0.11.0" />

`--literals` finds user-facing strings that never reached a catalog. It walks the source roots of your [`extract` block](https://verbatra.kreitz-webdev.de/docs/config-file.md#extract), reads and never writes your source, constructs no provider, reads no key and loads no `.env` file. Only two checks run: Configuration and Untranslated literals. Each finding names file, line and column, with whitespace collapsed, JSX character references decoded, and at most 80 characters:

```text output
  [fail] Untranslated literals: Scanned 42 source files: 2 untranslated literals found (1 suppressed).
    src/components/Header.tsx:12:9  "Welcome back"
    src/pages/settings.tsx:40:22  "Save changes"
    suppressed (directive) src/pages/legal.tsx:8:5  "Acme Inc."
```

Reported: JSX text in `.tsx`, `.jsx` and `.js` files; user-facing JSX attributes (`alt`, `title`, `placeholder`, `label`, `aria-label` and the other text-carrying `aria-*`); and any other string of two or more words.

Never reported:

- a string passed to `t(...)`, `$t(...)`, `i18n.t(...)` or a renamed `t` from `useTranslation`, or rendered inside `<Trans>` or `<Translation>`;
- object keys, import specifiers, class names, CSS values, test ids, `data-*` and id or keyword attributes (`aria-describedby`, `aria-labelledby`, `aria-controls`, `rel`, `sandbox`, `allow`, `autoComplete`, `referrerPolicy`, `crossOrigin`), URLs, and strings without letters;
- literals in a type position (the value before `as` or `satisfies` is still checked), log messages, error messages (`new ValidationError(...)`, `throw Error(...)`), comparison operands, and template literals with an expression;
- direct arguments of `describe` (zod), `query`, `execute`, `prepare`, `format`, `parse`, `z.enum`, `setItem`, `getItem` and `removeItem`, and the first argument of `on`, `off`, `once`, `emit`, `addEventListener` and a member `get` or `set`; copy nested deeper, and messages passed to functions like `setError`, are still reported;
- anything in test, story, declaration or config files.

To hold one literal back, put `// verbatra-ignore-next-line` above it (`{/* verbatra-ignore-next-line */}` in JSX) or `// verbatra-ignore-line` at the end of its line. A next-line directive targets the next line holding code and, on a JSX opening tag, all of that tag's attributes. For a string that is fine everywhere, list it under `extract.literals.ignore` with references decoded (`Tom & Jerry`). Suppressed literals are still listed with their reason.

The check fails on a finding and on a file it could not open, read to the end, or considered too large (the scan carries on). A project without an `extract` block fails it.

### Data flow [#data-flow]

<AvailableFrom version="0.12.0" />

`--data-flow` describes where this project's data goes instead of checking the setup, as evidence for a DPA or NDA review; [Data handling](https://verbatra.kreitz-webdev.de/docs/data-handling.md) explains each category. It reads no API key and makes no network request. Only two checks run: Configuration and Data flow. Data flow never fails: it prints `[warn]` when the [network policy](https://verbatra.kreitz-webdev.de/docs/network-policy.md) refuses a host or holds an invalid value, or when the source file cannot be read for the counts.

```text output
  [ok  ] Data flow: Provider "deepl" sends source-text, placeholder-markers, language-codes to api.deepl.com (permitted), api-free.deepl.com (permitted).
  data flow (manifest version 1)
    provider     deepl (machine-translation)
    network      any host (no policy set)
    destination  api.deepl.com  default  paid-key  before-construction  permitted
    destination  api-free.deepl.com  default  free-key  before-construction  permitted
    sent         source-text, placeholder-markers, language-codes
    api key      required
    placeholders masked
    sensitive    off
    counts       3 source keys, 0 with a description or meaning, 1 with placeholders it cannot mask (withheld)
    locale       de  sent as DE  glossary terms: 0
    local        lock  verbatra.lock.json  (no source text, translations or personal data)
    local        provenance  verbatra.provenance.json  (reviewer names)
    local        cache  verbatra.cache.json  (source text, translations, gitignored by init)
    local        local-state  .verbatra-local  (source text, host name and process ID of a run holding a write lock, gitignored by init)
    request      language-list  verbatra doctor --live  sends the API key
    request      dns-lookup  each connection to a destination host
    agent        mcp  verbatra mcp  redactable
    agent        studio-agent-tools  verbatra studio --expose-agent-tools
```

The manifest names the provider; each destination host, where it comes from (built in, `provider.options.baseUrl` or a `*_BASE_URL` variable) and the policy verdict (`permitted`, `deferred`, `refused` or `invalid-policy`); the kinds of data sent, with the number of source keys, of keys carrying a description or meaning, and of keys whose placeholders the provider cannot mask (an estimate over the source keys: a run sends only missing or changed keys and may withhold more under `sensitiveData: redact`); the code and glossary term count per target locale; the local files with their project-relative paths, and whether they hold source text, translations or personal data; and the other requests and agent surfaces. DeepL is listed with both hosts, because the key that picks one is never read. A proxy in effect is named by its variable and host, never its credentials, and the fields list only what this config can send. With `provider: { id: "none" }` it states that nothing is sent. It names hosts, data categories and conditions, never a key value or file contents.

With `--json` the manifest is under `result.dataFlow`. Its `version` field changes only when a field is renamed or removed; new fields can appear without a bump, so ignore the ones you do not know. The SDK's [`dataFlowManifestSchema`](https://verbatra.kreitz-webdev.de/docs/sdk/inspect.md#dataflowmanifestschema) validates it.

### JSON output [#json-output]

Schema: [`doctor-envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/doctor-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 the report under `result`. `ok` is true exactly when the command exits `0`:

```ts
interface DoctorResult {
  ok: boolean;                 // true exactly when the command exits 0
  checks: DoctorCheck[];       // one per check, always in the same order
  literals?: LiteralScan;      // only with --literals: findings, suppressed, diagnostics
  dataFlow?: DataFlowManifest; // only with --data-flow: what goes where, version 1
  locales?: LocaleCapabilityReport; // per-locale provider support, with or without --locales
}

interface DoctorCheck {
  id:
    | "config"
    | "format-adapter"
    | "provider"
    | "api-key"
    | "network-policy"
    | "source-file"
    | "plural-rules"
    | "plural-completeness"
    | "locale-codes"
    | "locale-state"
    | "limits" // 0.13.0
    | "locales"
    | "untranslated-literals"
    | "data-flow";
  title: string;               // stable, human-readable name
  status: "pass" | "warn" | "fail" | "skipped";  // only "fail" makes ok false
  detail: string;              // why it reached that verdict; never an API key value
  fix?: string;                // the next step, only on a failed check
}

interface LocaleCapabilityReport {
  provider: string;
  coverage: "listed" | "open";       // "open" for an LLM provider
  tableVersion: string;              // the table's date (YYYY-MM-DD)
  tableOrigin: "static" | "live";    // "live" only when --live fetched the list
  live?: { status: "refreshed" | "skipped" | "failed"; detail: string }; // only with --live
  source: {
    locale: string;
    providerCode: string;
    mapped: boolean;
    support: "supported" | "unverified" | "unsupported";
    warnings: { code: string; message: string }[];
  };
  locales: {
    locale: string;
    providerCode: string;            // the code the provider receives
    mapped: boolean;                 // true when localeMap set providerCode
    support: "supported" | "unverified" | "unsupported";
    glossary: boolean;
    formality: boolean;
    warnings: { code: string; message: string }[];
  }[];
}
```

`result.locales` is absent when the config could not be loaded, for an unknown provider, and for `provider: { id: "none" }`.

### Terminal feedback [#terminal-feedback]

<AvailableFrom version="0.12.0" />

While it runs, `doctor` shows one stderr line, which `--quiet` drops: `checking the setup`, `checking the setup and fetching the provider's language list` under `--live`, `describing the data flow` under `--data-flow`, or `scanning the source for literals` with a file count under `--literals`. The report stays on stdout. See [Terminal output](https://verbatra.kreitz-webdev.de/docs/cli/output.md#terminal-output).

## Exit codes [#exit-codes]

| Code | Meaning |
| --- | --- |
| `0` | no check failed (a `warn` still exits `0`) |
| `1` | at least one check failed, including an unsupported locale; with `--literals`, a literal was found or a file could not be scanned |
| `2` | could not run: a usage error, or an explicit `--config` path that does not exist |

A config the search does not find, or one that fails validation, is a failed check (exit `1`), because reporting it is the command's job. See [the exit-code contract](https://verbatra.kreitz-webdev.de/docs/cli/output.md#exit-codes).

## Related [#related]

- [`verbatra init`](https://verbatra.kreitz-webdev.de/docs/cli/init.md) scaffolds the config and `.env.example` that `doctor` validates.
- [`verbatra check`](https://verbatra.kreitz-webdev.de/docs/cli/check.md) is the drift gate to run once the setup is sound.
- [The config file](https://verbatra.kreitz-webdev.de/docs/config-file.md) documents every key `doctor` validates.
- [Providers](https://verbatra.kreitz-webdev.de/docs/providers.md) lists the API key variable each provider reads.
- [Language support](https://verbatra.kreitz-webdev.de/docs/language-support.md#checking-support-before-a-run) lists the language tables the Locale support check uses.
