Skip to content
Skip to Content

Docs / Concepts

The lock file and provenance

What verbatra.lock.json and verbatra.provenance.json record, how drift is detected, how concurrent runs are serialized, and why you commit both.

verbatra.lock.json is the baseline every run diffs against, so a run can skip what is current and retranslate what drifted. Next to it, verbatra.provenance.json records who or what wrote each value.

What it records

For every target locale, one hash per translated key of the source entry behind the current translation, plus a version field: no translations, no secrets. The hash covers the source value plus its description, meaning, plural flag and placeholders, so a changed description marks the key stale too. It normalizes Unicode to NFC and line endings to LF, so re-saving a source file with CRLF endings changes nothing.

How drift is detected

A source key the target lacks is missing. For a key it has, the current source hash is compared with the recorded one: a difference means the source changed and the key is retranslated; a match means it is never sent, so a second run with no source edits costs nothing. The whole pipeline is on How a run works.

Protected keys

The exception is a changed key whose value a person wrote or imported: by default it is protected and stays stale instead of being retranslated.

Adopting an existing project

Locale files that already hold reviewed translations are not retranslated. On the first run there is no lock file, so nothing can be stale: a key the target has keeps its value, only keys it lacks are translated, and the run records every source hash as the baseline at no provider cost.

Adoption goes by key presence, never by value, which has two consequences:

  • An empty or untranslated value counts as translated. Delete the key (emptying it is not enough) and the next run translates it.
  • An outdated translation is recorded as current. Fix or delete it before the first run.

The same rule keeps hand-written plural forms, so generatePlurals is safe while adopting. To preview all of this, run verbatra diff or verbatra translate --dry-run.

How it is updated

  • A full run (translate, watch, or an import) replaces a locale's entries with the run's result.
  • A single-key action, such as a retranslate from Studio, merges only that key's entry.

Either way, a key withheld this run (a failed integrity check or provider call, an invalid-ICU source) keeps its previous hash, so the next run retries it. Orphaned keys get no entry, and sorted keys keep the diffs small.

The write lock

Two runs touching the same locale at once could both diff a stale baseline and pay twice, so every write holds a cross-process lock for that locale under .verbatra-local/. A second writer waits, then diffs against a baseline that includes the first writer's result. Different locales never block each other.

What the lock guarantees:

  • A timeout costs nothing. --lock-timeout bounds only the wait before any provider call, so a locale that gives up has spent and written nothing; recording a written file waits the full ten-minute default.
  • Abandoned locks do not block you. A lock whose process is gone from this machine is reclaimed at once, and one not refreshed for 30 seconds (holders refresh every 10) counts as abandoned.
  • An interrupt releases the locks. Ctrl-C or SIGTERM during translate or import, and the second interrupt that force-stops watch, release every lock before the process exits.
  • A lost lock stops the write. Each lock carries a random token, checked before every write; when another run took the lock over, the write stops with LOCK_CONTENDED and nothing is written.

What it does not cover:

  • A lock from another machine (a shared network drive) or from a verbatra older than 0.12.0 is never reclaimed automatically; Troubleshooting has the steps to clear it.
  • A long pause between check and write. A run paused right after the token check (a sleeping machine, a stopped process) can still write once when it resumes.
  • Hosts that share a name. Outside Linux, the host name is the only machine identity, so two containers with one host name sharing .verbatra-local/ can mistake each other's live lock for an abandoned one. Give each its own host name or working copy.

A respelled locale code

The lock file, the cache and the provenance file key their state by your config's locale code. When you respell one, say pt_BR to pt-BR, the next translate or watch run carries the old state over once and reports LOCALE_STATE_CARRIED_OVER. A source change made before the rename is still retranslated, and cached translations are still reused.

The move happens only when unambiguous: pt-BR has no state of its own, and the file holds state under exactly one underscore spelling of it (or its gettext spelling, such as sr@latin for sr-Latn), compared without case. State pt-BR already has is never overwritten. If the move cannot be written, that locale does not run and fails with LOCALE_STATE_NOT_CARRIED_OVER, so the next run tries again; the other locales run as usual.

Until then, every read treats the old state as pt-BR's without writing, so a stale key is reported as stale. A single-key write (an edit, a retranslate, a review decision) and verbatra import move the state first, the same way. The locale-state check of verbatra doctor names any state left behind.

Missing or corrupt

A missing lock file is a first run, not an error. One that is unparseable, structurally wrong, oversized or at an unsupported version fails the run with LOCK_FILE_INVALID instead of being overwritten, so you can restore it rather than lose the baseline.

Provenance: who wrote each value

verbatra.provenance.json records which write path produced each value now in the locale file, and whether anyone reviewed it. An older verbatra, such as one pinned in CI, ignores it.

OriginWritten by
machinea provider answer in translate, watch or a single-key retranslate, with the provider's id and, when known, the model
memoryan exact cache hit, including every value a human-only run (provider none) fills
fuzzya fuzzy cache hit
humana person's edit through the SDK's editEntry, as Studio's edit dialog makes
agentan AI agent's edit through editEntry with actor: "agent"
importan accepted row from verbatra import
unknowna review decision on a value that had no record, or an origin this release does not know

Two more are derived on read: unrecorded is a value with no record, as every key is after upgrading; external no longer matches its record's hash because something outside verbatra changed it, such as a translator's pull request, an older verbatra or a merge. The fields each read returns are in the SDK reference.

What the file guarantees:

  • Stable diffs. No translated text or timestamps, one sorted record per line, so a run that changes nothing leaves the file unchanged. In a merge conflict, take the side matching the locale file you keep: a mismatched record reads as external, which protects the value from retranslation.
  • Fresh records for new values. A write that changes a value replaces its record with a new, unreviewed one; a write that leaves value and source unchanged keeps it.
  • No silent overwrite. A corrupt file stops every write with PROVENANCE_FILE_INVALID before any locale file changes; reports leave their provenance fields out instead. A file from a newer verbatra is left untouched, and one that would grow too large to read back is not written (PROVENANCE_FILE_TOO_LARGE).

It is not tamper-proof; to turn it into a report, see Record machine-translation evidence.

Review decisions

A record can carry a review decision, approved or rejected, with an optional reviewer name. Studio's Review page, the SDK's review functions and the MCP review tools write them; the MCP tools relay a person's decision and require their name. A decision applies to the exact value the reviewer saw, and is refused if it changed.

  • An approval changes only the provenance file, and reads as unreviewed again once the source text changes.
  • A rejection removes the value and its lock entry, so the key reads as missing until the next translate run or a person writes a new value. The record keeps a hash of the removed text and the cache entry is dropped, so a run does not put the same text back. XLIFF cannot drop one translation, so a rejection there is refused with REVIEW_REJECT_UNSUPPORTED.

Only machine-class values enter the review queue and the --require-reviewed gate. The byReviewState counts in check and lock.state cover every value by its recorded decision, whatever its origin, so a value a person wrote or imported with no decision counts as unreviewed there without being in the queue; read them with byOrigin.

Decisions live in this committed file, so the review queue and verbatra check --require-reviewed see the same state everywhere, and the reviewer name is public; verbatra never fills it in.

Commit it

Commit verbatra.lock.json and verbatra.provenance.json with your locale files, and never hand-edit them: they are the baseline your team and CI diff against, and what a rollback restores.

Edit on GitHub

On this page