Core conceptsHow it works

How it works

The translation pipeline end to end, from reading the source locale to updating the lock file.

A run is a pipeline: read the source locale, diff each target against the lock file, send only what needs work to the provider, check what comes back, write, and record the result. This page walks that pipeline so you know exactly what happens when you run verbatra translate.

verbatra translate
en.json
de.json
"cart.checkout": "Checkout"
"cart.checkout": "Zur Kasse"
"cart.empty": "Empty"
"cart.empty": "Leer"
"cart.total": "Total"
"cart.total": "Gesamt"
Only the changed key is sent to the provider. Current keys are left untouched.

A run starts from your validated config; see Config file for where it is found and what it contains. Everything below is the same whether the run came from the CLI, the SDK, or watch.

Read the source locale

The format adapter reads the source locale file into a neutral set of keyed entries, so the rest of the pipeline works the same for every format. A missing source file fails the run with SOURCE_UNREADABLE; a file the adapter cannot parse fails with SOURCE_INVALID. While reading, the adapter also flags source keys whose values are invalid for the format's message syntax (invalid ICU): those keys ride along but are set aside instead of being sent to the provider in a broken state.

Diff against the lock baseline

This diff exists so a run is incremental: the provider is called only for strings that actually need work. verbatra reads the existing target locale file (an absent file counts as empty) and diffs the source against it, using the lock file as the baseline of what was translated before. Every source key lands in one of four buckets:

  • missing: the target does not have the key yet.
  • changed: the source value no longer matches the hash recorded in the lock.
  • unchanged: the source value still matches the recorded hash.
  • orphaned: the key is in the target but no longer in the source.

Only missing and changed keys become translation candidates. Unchanged keys are left alone. Orphaned keys are reported and, by default, left in place; with --prune (or prune: true in the config) exactly those keys are removed from the target file and the lock, and nothing else.

Batch to the provider

Candidates go to your configured provider in sequential sub-batches of at most maxBatchSize (default 50), so one oversized request cannot take down a whole locale. Each sub-batch carries the entries plus your optional glossary and tone. If a sub-batch call fails (a rate limit, a timeout, a revoked key), its keys are withheld for this run and picked up again next run while the other sub-batches keep making progress. See Providers for how each provider applies the inputs.

Validate the response

Provider output is never trusted as free text. For the LLM providers (Anthropic, OpenAI, Gemini, and openai-compatible), the response is schema-bound data, validated before anything else happens: a response that is malformed, or that contains a key that was never requested, fails the sub-batch, and a requested key that comes back missing or duplicated is re-requested once; if it is still unresolved after that, the key is treated as not translated this run and retried on the next one. DeepL is a machine-translation API without this response layer, but its results go through the same integrity checks below.

Check placeholder and ICU integrity

A translation that passed schema validation can still be broken, so verbatra recomputes its own accept-or-reject decision from the candidate value itself: it never trusts the provider's report. Each accepted candidate must carry the same placeholders as its source, and for the ICU formats it must still be a valid message. A candidate that fails either check is withheld instead of written, surfaces on the run summary, and is retried next run. See Translation safety for the full safety model.

Write through the adapter

Accepted translations are merged into the target and written by the format adapter, atomically, in document key order: a key already in the target keeps its position, and a new key appends where the source puts it. An interrupted write never leaves a half-finished locale file, and a formatter or code reviewer sees stable, minimal diffs.

Update the lock

After a locale is written, its lock entries are refreshed with the current source hashes, with one deliberate exception: a key withheld this run keeps its prior hash, so the next run still sees it as needing work. Each locale's write happens under a cross-process write lock, so two overlapping runs never diff against a stale baseline or pay for the same provider call twice. The details live in The lock file.

One locale at a time, failures as data

Each target locale runs on its own: one locale failing does not stop the others. The run returns a summary per locale (translated, unchanged, orphaned, withheld, flagged for review, notices), and a dry run (--dry-run) produces the same summary shape after the diff step, without constructing a provider or writing anything.

Edit on GitHub