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-timeoutbounds 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
translateorimport, and the second interrupt that force-stopswatch, 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_and nothing is written.CONTENDED
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_ to pt-BR, the next translate or watch run carries the old state
over once and reports
LOCALE_. 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_, 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_ 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.
| Origin | Written by |
|---|---|
machine | a provider answer in translate, watch or a single-key retranslate, with the provider's id and, when known, the model |
memory | an exact cache hit, including every value a human-only run (provider none) fills |
fuzzy | a fuzzy cache hit |
human | a person's edit through the SDK's editEntry, as Studio's edit dialog makes |
agent | an AI agent's edit through editEntry with actor: "agent" |
import | an accepted row from verbatra import |
unknown | a 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_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 (FILE_ INVALID 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
translaterun 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 withREVIEW_.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.