Skip to content
Skip to Content

Reference / CLI

verbatra translate

Translate every target locale once, sending only new and changed keys to your provider, then write the results and exit.

Run one translation pass over every target locale, then exit. translate diffs the source against the lock file, sends only new or changed strings to the provider, runs the integrity checks, writes the locale files, and updates the lock. An unchanged string is never sent twice. A real run also adds missing entries to an existing .gitignore, see .gitignore entries.

Synopsis

verbatra translate [flags]

Examples

# translate once using the config it finds
verbatra translate

# preview the keys that would be pruned, spending nothing
verbatra translate --prune --dry-run

# one locale at a time, which is what a rate-limited free tier needs
verbatra translate --locales de

# size and price the run without spending anything
verbatra translate --estimate

# stop before the run passes 50000 tokens
verbatra translate --max-tokens 50000

# also retranslate stale keys a person wrote
verbatra translate --include-human

Flags

FlagArgumentDefaultEffect
--locales<list>every configured target localetranslate only this comma-separated subset; an unconfigured locale fails with UNKNOWN_LOCALE and an empty list with INVALID_LOCALES, both exit 2
--dry-runnoneoffreport what a run would change without calling a provider or writing files; needs no API key
--prunenonethe config's prune option, otherwise offremove orphaned keys (in a target file, absent from source) from the written file and the lock
--lock-timeout<seconds>600wait this long for a held per-locale write lock; a whole number from 1 to 3600
--concurrency<n>1translate up to n target locales at once; a whole number from 1 to 100, refused under a token budget
--max-tokens<n>the config's maxTokenshard token ceiling for this run, a positive whole number (INVALID_MAX_TOKENS otherwise); the lower of this and the config's maxTokens applies
--no-cachenoneoff (cache on)bypass the translation-memory cache (verbatra.cache.json) for this run
--include-humannoneoffalso retranslate stale keys a person wrote or imported, for this run only; pinned keys stay untouched
--estimatenoneoffestimate what the run would send and cost, then exit; implies --dry-run

Also takes the global flags --cwd, --config and --json. Before it runs, translate loads the environment files.

Behavior

Warning

A run without --dry-run or --estimate sends every new or changed string to the provider, and a hosted provider bills for it: size the run with --estimate and cap it with --max-tokens.

Warning

An API key sent to a self-hosted server over plain http:// (not loopback) crosses the network unencrypted, see Keys are optional.

Dry run

--dry-run constructs no provider, so it needs no API key and cannot spend anything. It reads the source and target files, diffs them against the lock file, and reports exactly what a real run would change. It writes nothing (no locale file, no lock update, no run-status snapshot), with a machine provider neither reads nor writes the cache (under provider none it reads the translation memory, see below), and takes no write lock, so it never waits on a run in progress.

Note

By default a run deletes nothing: orphaned keys are reported and left in place. --prune, or prune: true in the config, deletes exactly those keys from the target files and the lock; --prune --dry-run previews them.

Cost estimate

--estimate adds a pre-run estimate to the dry run: how many keys would be sent, how many provider requests they would take, and the quantity the provider bills for (tokens for an LLM or an openai-compatible endpoint, source characters for DeepL, Google Cloud Translation or LibreTranslate; a self-hosted endpoint or server is not billed, so the estimate reports no API cost). A currency figure appears only when the config has a rates block covering the provider and model; otherwise the missing rate key is named instead of inventing 0.00. See Estimating cost.

A subset of locales

--locales de,fr narrows the run to part of the configured targets. The other locales keep their files, lock entries, and cache entries as they were. Use it on a rate-limited free tier to translate one locale at a time, or to re-run a single locale that came out partial. The run-status snapshot then describes that subset only.

Write locks and interrupts

A locale waits up to --lock-timeout for a contended write lock, then fails with LOCK_CONTENDED while the other locales still run. A lock left by a verbatra process that is no longer running on this machine is reclaimed at once. The timeout bounds only that wait before any provider call: recording a written file in the lock file always waits up to the ten-minute default, so a written file is never left unrecorded. A wait longer than a second prints a line on stderr naming the lock and its holder (a lock-wait record under --json), which --quiet does not drop. Ctrl-C or SIGTERM releases the run's locks before the command exits.

Token ceiling

--max-tokens is always a hard stop: a provider request that would pass the ceiling is withheld, whatever budgetBehavior says, and its keys are listed as budgetWithheld. It is counted like a configured maxTokens budget and can tighten that budget but never loosen it. A run the ceiling cut short exits 1. Run --estimate first to pick a figure.

Parallel locales

--concurrency is refused with exit 2 when a token budget applies (from maxTokens or --max-tokens), because which locale loses its remaining work would depend on how the locales interleave; a dry run is exempt. If a whole-run error, such as a corrupt lock file, surfaces while locales are running, no further locale starts, and the running ones finish and release their locks before the command exits 2.

Keys a person wrote

A stale key whose value a person wrote, imported, or changed outside verbatra is kept by default: it is listed as protected, not sent to the provider, and stays stale for verbatra check. A stderr warning says how many were kept (pinned keys are not counted) and points to --include-human, which retranslates them for one run (humanEdits: "overwrite") and so prints no warning. Protected keys do not change the exit code, except in a human-only project. See Protecting human translations.

Human-only projects

In a human-only project (provider: { id: "none" }) no provider is called. Keys the translation memory covers are written, and every other missing or changed key is listed as unfilled and exits 3; a failed or partial locale still wins with 1. A dry run reads the memory here, so its plan shows the same cacheHits and unfilled keys, and --estimate reports nothing billed; both exit 3 the same way. Without --json, a stderr warning says how many keys need a person, and a next: hint points to verbatra export, where you hand the unfilled keys off.

Failed and partial locales

One locale failing is not fatal: the run finishes and exits 1. A partial locale exits 1 too: its file was written but keys are still missing, typically because a provider sub-batch failed or the integrity gate refused a translation. The human output lists every refused key under integrity-withheld with its reason (placeholder, markup, icu, degenerate, or empty); with --json, each locale carries them in integrityRefusals. Keys the run flagged for review follow the locale's N needs-review count under a needs-review: line, each with its review reasons; with --json they are in needsReview.

Unsupported locales

A locale the configured DeepL or Google Cloud Translation provider does not support refuses the whole run with LOCALE_UNSUPPORTED_BY_PROVIDER and exit 2, on a dry run and with --estimate too, before anything is spent. Leave it out with --locales, or see Language support. A glossary or tone the provider cannot apply only adds a notice (GLOSSARY_UNSUPPORTED_BY_PROVIDER, FORMALITY_UNSUPPORTED_BY_PROVIDER), as do LOCALE_UNVERIFIED_BY_PROVIDER and, for an LLM, LOCALE_NOT_WELL_TESTED.

JSON output

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

With --json, stdout carries one envelope whose result is a RunSummary; RunSummary lists every field, for the run and for each locale.

Terminal feedback

translate names the locales and provider before the run, and prints the elapsed time, tokens used, and the next command afterwards. On an interactive terminal the progress is one spinner line; when stderr is piped, every step is a plain line:

Output
verbatra: translating 2 locales with gemini/gemini-2.5-flash
verbatra: translating de
verbatra: de batch 1/1
verbatra: de: writing
verbatra: de done, 12 translated
verbatra: translating fr
verbatra: fr batch 1/1
verbatra: fr: writing
verbatra: fr done, 12 translated
verbatra: run finished, 2 locales processed
[ok] done in 4.2s, 1830 tokens
next: verbatra check (confirm every locale is in sync)

A dry run opens with dry run over 2 locales, no provider call, an estimate with estimating 2 locales, no provider call, and a human-only project with filling 2 locales from the translation memory (provider none). A dry run or an estimate ends each locale with would translate instead. A locale that kept only some keys ends with partly done, one that failed with failed, and the closing line adds how many failed. A dry run closes with dry run done in 4.2s, nothing written. An estimate closes with estimate done in 4.2s, nothing written new. These lines go to stderr, and --quiet drops them. See Terminal output.

Exit codes

CodeMeaning
0every target locale came out complete
1the run finished, but a locale failed or came out partial
2could not run: a config, source, format, provider, unsupported-locale, or lock-file error, or a usage error
3human-only project: the run (a dry run or --estimate included) finished cleanly, but keys are left for a human translation
130interrupted with Ctrl-C (SIGINT) after releasing the write locks
143stopped with SIGTERM after releasing the write locks

On an interrupt, stderr ends with verbatra: interrupted (SIGINT), released locks, 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). See the exit-code contract.

Edit on GitHub

On this page