Skip to content
Skip to Content

Docs / Concepts

How a run works

The translation pipeline end to end, from reading the source locale and finding changed keys to writing the translations and 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. The pipeline is the same whether the run came from the CLI, the SDK, or watch.

verbatra translate
en.json
de.json
"cart.checkout": "Checkout"
"cart.checkout": ""
"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.

Read the source locale

The format adapter reads the source file into a neutral set of keyed entries, so every later step works the same for every format. A source key whose value is invalid for the format's message syntax (invalid ICU) is set aside and reported rather than sent to the provider in a broken state. A source file that is missing or cannot be parsed fails the run; see SOURCE_UNREADABLE and SOURCE_INVALID.

Diff against the lock baseline

The diff is what makes a run incremental. verbatra reads the existing target file (an absent file counts as empty, though xliff and apple-xcstrings writers refuse a missing target) and compares each source key against the lock file, the record of what was translated before:

BucketMeaningWhat the run does
missingthe target does not have the keytranslates it
changedthe source no longer matches the recorded hashtranslates it
unchangedthe source still matches the recorded hashleaves it alone
orphanedthe key is in the target but not in the sourcereports it; removes it only with --prune or prune: true

Held-back keys

Two kinds of candidate are held back. A changed key whose current value a person wrote or imported is protected by default (humanEdits), and a key matching pinnedKeys is never machine-translated. Both stay stale until a person resolves them; see Protecting human translations.

Batch to the provider

Every candidate is first looked up in the translation cache. A hit is reused and never reaches the provider. Of the misses, keys with the same source content are grouped, so that text costs one request per locale rather than one per key.

The groups go to your provider in sequential sub-batches of at most maxBatchSize (default 50), each carrying the entries, the glossary terms of that locale and your tone. If a sub-batch fails (a rate limit, a timeout, a revoked key), its keys are withheld for this run while the other sub-batches keep going. Providers describes how each provider applies these inputs.

Validate the response

Provider output is data, never free text. For the LLM providers the response must match a fixed schema:

  • A malformed response, or one that contains a key that was never requested, fails the whole sub-batch.
  • A requested key that comes back missing or duplicated is requested once more.
  • A key still unresolved after that is not translated this run.

The machine-translation providers have no response schema, but their results go through the same integrity gate.

Run the integrity gate

A schema-valid translation can still be broken, so verbatra decides from the candidate value itself whether to accept it and never trusts the provider's own report. A candidate that loses a placeholder, breaks an ICU message or its markup, comes back empty, or degenerates is withheld instead of written. The checks are on Translation safety.

Write through the adapter

Accepted translations are merged into the target and written by the format adapter, atomically and in a stable key order: a key already in the target keeps its position, and new keys are appended after the existing ones, in source order. An interrupted write never leaves a half-written file, and reviewers see small, stable diffs.

Update the lock

After a locale is written, its lock entries are refreshed with the current source hashes. A key withheld this run keeps its previous hash, so the next run still sees it as needing work. Each locale is written under a cross-process write lock, so two overlapping runs never diff against a stale baseline or pay twice for the same call.

One locale at a time, failures as data

Each target locale runs on its own, and one locale failing does not stop the others. The run returns a summary per locale: translated, unchanged, orphaned, withheld, flagged for review, and notices. What each field holds is on RunSummary.

What this design guarantees:

  • A second run with no source edits calls the provider for nothing.
  • Nothing a check rejected is ever written, and nothing withheld is recorded as done.
  • A dry run (--dry-run) stops after the diff and returns the same summary shape, without building a provider or writing a file.

What it does not do: it never judges whether a translation is good. That is what review flags and a person are for.

Edit on GitHub

On this page