---
title: "The lock file and provenance"
description: "What verbatra.lock.json and verbatra.provenance.json record, how drift is detected, how concurrent runs are serialized, and why you commit both."
type: "concept"
---
# 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`](#provenance-who-wrote-each-value)
records who or what wrote each value.

## What it records [#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 [#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](https://verbatra.kreitz-webdev.de/docs/how-it-works.md).

### Protected keys [#protected-keys]

<AvailableFrom version="0.12.0" />

The exception is a changed key whose value a person wrote or imported: by default it is
[protected](https://verbatra.kreitz-webdev.de/docs/protecting-human-translations.md) and stays stale instead of being retranslated.

## Adopting an existing project [#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`](https://verbatra.kreitz-webdev.de/docs/cli/diff.md) or `verbatra translate --dry-run`.

## How it is updated [#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 [#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`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#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](https://verbatra.kreitz-webdev.de/docs/troubleshooting.md) 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 [#a-respelled-locale-code]

<AvailableFrom version="0.12.0" />

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`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#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`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#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`](https://verbatra.kreitz-webdev.de/docs/cli/doctor.md) names any state left behind.

## Missing or corrupt [#missing-or-corrupt]

A missing lock file is a [first run](#adopting-an-existing-project), not an error. One that is
unparseable, structurally wrong, oversized or at an unsupported version fails the run
with [`LOCK_FILE_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#lock_file_invalid) instead of being overwritten, so you
can restore it rather than lose the baseline.

## Provenance: who wrote each value [#provenance-who-wrote-each-value]

<AvailableFrom version="0.12.0" />

`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](https://verbatra.kreitz-webdev.de/docs/the-cache.md#fuzzy-reuse) 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](https://verbatra.kreitz-webdev.de/docs/sdk/inspect.md#loadprovenance).

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`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#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`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#provenance_file_too_large)).

It is not tamper-proof; to turn it into a report, see
[Record machine-translation evidence](https://verbatra.kreitz-webdev.de/docs/provenance-evidence.md).

### Review decisions [#review-decisions]

<AvailableFrom version="0.12.0" />

A record can carry a review decision, `approved` or `rejected`, with an optional `reviewer` name.
[Studio's Review page](https://verbatra.kreitz-webdev.de/docs/review-in-studio.md), the [SDK's review functions](https://verbatra.kreitz-webdev.de/docs/sdk/review.md) and the
[MCP review tools](https://verbatra.kreitz-webdev.de/docs/cli/mcp.md) 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`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#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`](https://verbatra.kreitz-webdev.de/docs/cli/check.md#require-reviewed-translations) see the same
state everywhere, and the `reviewer` name is public; verbatra never fills it in.

## Commit it [#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](https://verbatra.kreitz-webdev.de/docs/recovery-and-rollback.md) restores.
