Skip to content

Reference / CLI

Exit codes and JSON output

The exit-code contract, the JSON envelope every --json command prints, the codes the CLI raises itself, and how verbatra writes to stderr and the terminal.

Everything a script or CI step can rely on across commands. Each command page adds only what is specific to that command.

Exit codes

The exit code is the contract your CI step branches on:

CodeMeaning
0success, per command as listed below the table
1it ran, but the result is not clean: translate or import finished with at least one failed or partial locale, check found a locale out of sync (or, with --qa, a committed translation with an integrity error, or with a review warning or an incomplete plural under --strict, or, with --require-reviewed, a machine-written translation nobody approved, or, with --sensitive, content that looks sensitive in the source file or glossary), check --file found a syntax or integrity error in the file (or, under --strict, a review warning or an incomplete plural), diff found a missing or changed key (or, with --unused, a complete scan found an unused source key), report provenance found verbatra.provenance.json corrupt or from a newer verbatra, doctor found at least one failed check, types --check found the committed declaration out of date, or studio or mcp failed while shutting its server down
2could not run: a whole-run error (bad config, unreadable source, corrupt lock file, a locale the provider does not support), a usage error (an empty --locales list, INVALID_LOCALES, or an invalid --debounce or --port), an unconfigured --locales value (UNKNOWN_LOCALE), init missing a flag it needs, finding several formats or file patterns that fit, refusing to overwrite an existing config, or unable to scaffold a valid config, watch failing to start or to stop, studio unable to load the config, import @verbatra/studio, or start its server, or mcp given a --config path that does not exist, or unable to import @verbatra/mcp, start its server, or honor --redact-values with the installed @verbatra/mcp (a missing or invalid config found by the search starts it unconfigured)
3translate (a dry run and --estimate included) in a human-only project (provider: { id: "none" }) finished cleanly, but left keys for a human translation; a failed or partial locale still exits 1
130translate or import was interrupted with Ctrl-C (SIGINT) after releasing its write locks, or watch, studio or mcp was force-stopped by a second interrupt
143translate or import was stopped with SIGTERM after releasing its write locks

Exit 0 means, per command:

  • translate or import: every locale finished complete
  • check: every locale is in sync
  • diff: no pending changes
  • report: it printed its report
  • doctor: no setup problem
  • extract: it completed its scan
  • pseudo: it generated its pseudolocale
  • types: it generated its declaration
  • export: it wrote its workbook
  • tmx: it imported or exported its translation memory
  • init: it scaffolded the project or found it already up to date
  • watch, studio or mcp: it stopped cleanly
  • --help or --version: it printed the help or the version

On 130 and 143, stderr ends with verbatra: interrupted (SIGINT), released locks (or SIGTERM), or under --json with the record {"type":"interrupted","signal":"SIGINT","locksReleased":true}. If releasing the locks fails or does not finish within 5 seconds, the line says so instead (could not release locks: <reason> or lock release did not finish within 5s), and the JSON record carries "locksReleased":false with "reason":"failed" (plus message) or "reason":"timed-out" (plus deadlineMs).

Some edge cases worth knowing:

Available from 0.9.0
  • A partial locale counts as failure. It means the file was written but some keys are still missing, usually because a provider sub-batch failed or the integrity gate refused a translation. It exits 1 exactly like a failed locale. Read partial on the summary to tell the two apart. A failed sub-batch leaves a SUB_BATCH_FAILED notice on the locale naming the provider's error code, or PROVIDER_CALL_FAILED when the failure was not a structured provider error.
  • A locale the provider does not support is a whole-run error too, never one failed locale. translate (a dry run and --estimate included), watch at startup, and a single-key retranslation refuse it with LOCALE_UNSUPPORTED_BY_PROVIDER and exit 2 before any provider is constructed or anything is spent, so no locale of the run has started and nothing was written. It is judged against the language table verbatra ships for DeepL and Google Cloud Translation; an LLM provider accepts any locale. Leave the locale out with --locales to translate the others, and run verbatra doctor --locales to see why. See Language support.
  • A corrupt lock file is a whole-run error in translate and import alike, never one failed locale. It is a single shared file, so a lock file that turns corrupt while the run is under way stops the run with 2 rather than carrying on through the remaining locales. Whatever was written before the abort stays on disk; fix the lock file and re-run the command.
  • A single interrupt is a clean stop, and all three long-running commands (watch, studio and mcp) exit 0 on it. Do not assume they behave alike beyond that: if the stop itself fails, watch exits 2 while studio and mcp exit 1.
  • A failed run during watch shows up as a record on the output stream, never as a non-zero exit code.
  • export has no per-locale failure mode: it exits 0 or 2, never 1.
  • doctor reads a broken config differently from every other command: a config the search does not find, or one that fails validation, is a failed check and exit 1, because reporting exactly that is the command's job. It exits 2 only when it cannot run at all, such as an explicit --config path that does not exist.
  • One failure mode sits outside the contract: a parse failure that is not a usage error is re-thrown, and the binary does not catch it, so Node's default handling of an unhandled rejection applies instead of any of the six codes.
  • A protected key (a stale value a person wrote, imported, or changed outside verbatra, or a pinned key) does not change the exit code of translate in a project with a provider: it exits 0 and says on stderr how many keys it left (pinned keys are not counted). check still exits 1 for it, because the key stays stale until a person resolves it. In a human-only project it counts toward exit 3.

JSON output

Every command except studio and mcp accepts --json for machine-readable output on stdout: translate, watch, check, diff, report, export, import, tmx, doctor, pseudo, types, extract, and init. Every record is one line, and every record has the same envelope around it, so you branch on one field and never have to guess which command's payload you are holding:

type Envelope<TResult> =
  | { ok: true; version: 1; command: string; result: TResult }
  | {
      ok: false;
      version: 1;
      command: string | null;
      code: string;
      message: string;
      causeCode?: string; // the code of the error this one wraps, when it has one
      candidates?: string[]; // the values to choose from, on an error that has them
      missing?: string[]; // the flags still to pass, on an init usage error
      hint?: string; // the next step that resolves the error, when there is one
    };

version is the version of this envelope shape, not the package version. It is an integer so you compare it with === rather than range-parsing it, and it changes only when an existing field changes meaning or disappears. New fields can appear without a bump, so ignore the ones you do not recognize.

A run that fails as a whole writes exactly one ok: false record to stdout and exits 2. Its code is the same stable error code the stderr line carries, so it is what you branch on:

{ "ok": false, "version": 1, "command": "translate", "code": "CONFIG_INVALID", "message": "..." }

command is null only when the failure happened before a subcommand was resolved. candidates appears only on an error that asks you to choose, such as init's FORMAT_AMBIGUOUS, and lists the values to choose from. missing appears on an init usage error that leaves flags unanswered and names each flag still to pass. causeCode appears when the error wraps another coded error, such as a PROVIDER_CONSTRUCTION_FAILED caused by MISSING_API_KEY; the stderr line then ends with (cause: MISSING_API_KEY).

Available from 0.12.0

hint appears when the error has a next step: one short imperative sentence that resolves it, such as "Set GEMINI_API_KEY in the environment, or, with the CLI, in a .env file in the project directory." for a missing key or "Run `verbatra check --help` to see the options and arguments it accepts." for a USAGE_ERROR. Act on it instead of parsing message. It names variables, flags, and paths, never a key value. Every SDK, provider, and format adapter code has one (the SDK exports the same text as errorHint), and so does every CLI error code except CLI_ERROR. Without --json, the same text follows the error line on stderr as next: ..., unless --quiet is set. mcp --json is the exception: it is refused with one USAGE_ERROR line on stderr, with no envelope and no next:. Startup errors of studio and mcp get no next: line either.

The result of a successful envelope is different for each command and is described under JSON output on its page: extract, translate, watch, check, diff, report, doctor, pseudo, types, export, import, tmx, init.

Schemas Available from 0.12.0

Every record --json prints has a JSON Schema (draft 2020-12). The schemas ship inside the npm packages and are served at stable URLs, so a script, an agent or an editor can validate output instead of reading prose. Every output schema allows properties it does not list, as the envelope contract above promises; keep it that way if you copy one. The config schema is the deliberate exception: it describes what you write, so it rejects an unknown key to catch a typo.

DocumentURLFile in the package
Any envelope, success or errorenvelope.json@verbatra/cli/schemas/envelope.json
One command's envelope, with its resulthttps://verbatra.kreitz-webdev.de/schema/v1/<command>-envelope.json@verbatra/cli/schemas/<command>-envelope.json
The error envelopeerror-envelope.json@verbatra/cli/schemas/error-envelope.json
The init result alone, without the envelopeinit-result.json@verbatra/cli/schemas/init-result.json
A JSON line on stderr: progress, lock wait, interruptstderr-record.json@verbatra/cli/schemas/stderr-record.json
The config fileconfig.json@verbatra/sdk/schemas/config.json, or @verbatra/sdk/config-schema.json: the same schema without $id and title

Each command page links its envelope under JSON output, and https://verbatra.kreitz-webdev.de/schema/v1 lists every document with its title. A command envelope points at its result schema by URL in $ref, so a validator loads it over the network, or from node_modules/@verbatra/sdk/dist/schemas when you register those files first.

v1 tracks envelope version: 1 and always serves the latest release. An added field stays in v1; a change that breaks a payload raises the envelope version and adds /schema/v2/. The config schema uses the same URL scheme but follows the config's own changes. The URL always serves the latest release while a copy from npm stays at its version, so an older copy can reject a record from a newer CLI where a schema lists closed values, such as a stderr record's type or an envelope's command.

CLI error codes Available from 0.12.0

The CLI raises these codes itself. The same list ships as CLI_ERROR_CODES (with the type CliErrorCode) from @verbatra/cli, so a script can check a code against it. Any other code comes from the SDK, a provider, or a format adapter. What each code means, which commands raise it, and its next step are on Error codes.

Errors on stderr

Every error goes to stderr as one structured line (verbatra: error [CODE] message) in both modes. Since 0.12.0 that includes an argument-parsing error (an unknown option or command, a missing argument): it prints verbatra: error [USAGE_ERROR] unknown option '--nope' and exits 2, where it printed commander's own error: unknown option '--nope' with no code before. init follows the same grammar since 0.12.0; before that, a missing provider printed verbatra: --provider is required (...) with no code. Since 0.12.0, a hint follows the line as next: ... without --json, and under --json a parsing error writes the USAGE_ERROR envelope with its hint to stdout; mcp keeps stdout for the protocol, so it refuses --json on stderr only. The startup of studio and mcp is the exception to the structured line: when @verbatra/studio or @verbatra/mcp is not installed, stderr carries the install instruction alone, with no verbatra: prefix. Under --json, the message of a parsing error is commander's own text, error: prefix included. Without --json, a failing run still writes nothing at all to stdout. Progress and lock-wait records always go to stderr, so stdout carries nothing but envelopes.

Terminal output Available from 0.12.0

Results go to stdout. Progress, notices, hints, warnings and errors go to stderr, and verbatra adapts them to where stderr is going:

  • On an interactive terminal, fixed labels such as error, [ok] and next: are colored, and a step that takes longer than 300 ms shows an ASCII spinner (| / - \).
  • When stderr is piped or redirected, CI is set to anything but empty, 0 or false, or TERM is dumb, the output is plain, append-only lines with no color and no animation.
  • The status labels of the doctor report on stdout ([ok ], [warn], [fail], [skip]) are colored when stdout itself is an interactive terminal, so a redirected report stays plain unless FORCE_COLOR turns color on.
  • --json never adds color or animation, and stdout carries nothing but envelopes. Plain verbatra: warning lines can still reach stderr, so a consumer parses only the stderr lines that start with {. See JSON output.
Flag or variableEffect
-q, --quietprint only results, warnings and errors: drops progress lines, next: hints and informational stderr lines, while the result summary, including its notices, stays. It works before or after the command name
--no-colornever color the output
NO_COLOR, NODE_DISABLE_COLORS, VERBATRA_NO_COLORany non-empty value turns color off; VERBATRA_NO_COLOR also overrides FORCE_COLOR
FORCE_COLOR1, 2, 3, true or an empty value turns color on even when stderr or stdout is not a terminal, and any other value, 0 included, turns it off. Once set, it wins over NO_COLOR, NODE_DISABLE_COLORS, CI and TERM=dumb; --no-color and VERBATRA_NO_COLOR still win
VERBATRA_NO_SPINNERany non-empty value replaces the spinner with static label... done (1.2s) lines, for screen readers and terminal recordings

The output samples on the command pages show the plain form. An interactive terminal shows a running step as a spinner line that ends in [ok] and the elapsed time.

Status words are plain ASCII ([ok], [warn], [fail], [skip]), so the output reads the same in every terminal and log. Every line passes through the same key redaction as the rest of the output, and only the fixed labels are ever colored, never a key, path or message.

Edit on GitHub

On this page