Skip to content
Skip to Content

Reference / CLI

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.

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 or when another command fails.

Synopsis

verbatra doctor [flags]

Examples

# 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

FlagArgumentDefaultEffect
--literalsnoneoffscan the extract source roots for hardcoded user-facing strings instead of checking the setup (see Untranslated literals)
--localesnoneoffafter the checks, list per target locale the code sent, provider support, glossary and formality (see Locale support)
--livenoneofffetch the provider's current language list first, using no translation quota; implies --locales
--data-flownoneoffdescribe what is sent to which host and what is written locally instead of checking the setup (see Data flow)

Also takes the 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

What it checks

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.

CheckFails when
Configurationno config file is found, or it fails validation
Format adapterthe configured format resolves to no file adapter
Providerthe configured provider.id resolves to no provider factory
API key environment variablethe variable that provider reads its key from is not set
Network policythe effective network policy refuses the provider's host or a proxy it would use
Source locale filethe source locale file is missing at its resolved path, is not a regular file, or does not parse under the configured format
Plural rulesnever; names the ICU and CLDR versions in use, and warns when a target locale has no ICU plural rules
Plural completenessnever; 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 lists the gaps in plurals a target already holds); skipped for a format that does not store plural forms by CLDR category
Locale codesnever; warns and names every valid but non-canonical code, such as en-us or iw, with the canonical form (see Locale codes)
Locale statenever; 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
Length limitsnever; 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) new
Locale supportthe provider does not support the source or a target locale; warns when a locale carries a warning (see 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:

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

  • The API key is checked by name only: doctor asks whether the variable is set, never what it holds. See Providers 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" }, 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 first, so a key in a dotenv file counts as set; with --literals it loads neither.

Locale support

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 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 explains the verdicts.

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

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, 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 permits the host; otherwise, or on failure, the shipped table is used and a live language list line says why.

Untranslated literals

--literals finds user-facing strings that never reached a catalog. It walks the source roots of your extract block, 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:

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 describes where this project's data goes instead of checking the setup, as evidence for a DPA or NDA review; Data handling 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 refuses a host or holds an invalid value, or when the source file cannot be read for the counts.

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

JSON output

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

With --json, the envelope carries the report under result. ok is true exactly when the command exits 0:

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

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.

Exit codes

CodeMeaning
0no check failed (a warn still exits 0)
1at least one check failed, including an unsupported locale; with --literals, a literal was found or a file could not be scanned
2could 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.

Edit on GitHub

On this page