---
title: "verbatra translate"
description: "Translate every target locale once, sending only new and changed keys to your provider, then write the results and exit."
type: "reference"
---
# verbatra translate

Translate every target locale once, sending only new and changed keys to your provider, then write the results and exit.

Run one translation pass over every target locale, then exit. `translate` diffs the source against [the lock file](https://verbatra.kreitz-webdev.de/docs/the-lock-file.md), sends only new or changed strings to the provider, runs the [integrity checks](https://verbatra.kreitz-webdev.de/docs/translation-safety.md), writes the locale files, and updates the lock. An unchanged string is never sent twice. A real run also adds missing entries to an existing `.gitignore`, see [.gitignore entries](https://verbatra.kreitz-webdev.de/docs/cli.md#gitignore-entries).

## Synopsis [#synopsis]

```bash
verbatra translate [flags]
```

## Examples [#examples]

```bash
# translate once using the config it finds
verbatra translate

# preview the keys that would be pruned, spending nothing
verbatra translate --prune --dry-run

# one locale at a time, which is what a rate-limited free tier needs
verbatra translate --locales de

# size and price the run without spending anything
verbatra translate --estimate

# stop before the run passes 50000 tokens
verbatra translate --max-tokens 50000

# also retranslate stale keys a person wrote
verbatra translate --include-human
```

## Flags [#flags]

| Flag | Argument | Default | Effect |
| --- | --- | --- | --- |
| `--locales` | `<list>` | every configured target locale | translate only this comma-separated subset; an unconfigured locale fails with `UNKNOWN_LOCALE` and an empty list with `INVALID_LOCALES`, both exit `2` |
| `--dry-run` | none | off | report what a run would change without calling a provider or writing files; needs no API key |
| `--prune` | none | the config's `prune` option, otherwise off | remove orphaned keys (in a target file, absent from source) from the written file and the lock |
| `--lock-timeout` | `<seconds>` | `600` | wait this long for a held per-locale write lock; a whole number from `1` to `3600` |
| `--concurrency` | `<n>` | `1` | translate up to `n` target locales at once; a whole number from `1` to `100`, refused under a token budget |
| `--max-tokens` | `<n>` | the config's `maxTokens` | hard token ceiling for this run, a positive whole number (`INVALID_MAX_TOKENS` otherwise); the lower of this and the config's `maxTokens` applies |
| `--no-cache` | none | off (cache on) | bypass the [translation-memory cache](https://verbatra.kreitz-webdev.de/docs/the-cache.md) (`verbatra.cache.json`) for this run |
| `--include-human` | none | off | also retranslate stale keys a person wrote or imported, for this run only; [pinned keys](https://verbatra.kreitz-webdev.de/docs/protecting-human-translations.md#pinning-keys) stay untouched |
| `--estimate` | none | off | estimate what the run would send and cost, then exit; implies `--dry-run` |

Also takes the [global flags](https://verbatra.kreitz-webdev.de/docs/cli.md#global-flags) `--cwd`, `--config` and `--json`. Before it runs, `translate` loads the [environment files](https://verbatra.kreitz-webdev.de/docs/cli.md#environment-files).

## Behavior [#behavior]

<Callout type="warn">
A run without `--dry-run` or `--estimate` sends every new or changed string to the provider, and a hosted provider bills for it: size the run with `--estimate` and cap it with `--max-tokens`.
</Callout>

<Callout type="warn">
An API key sent to a self-hosted server over plain `http://` (not loopback) crosses the network unencrypted, see [Keys are optional](https://verbatra.kreitz-webdev.de/docs/providers.md#keys-are-optional).
</Callout>

### Dry run [#dry-run]

`--dry-run` constructs no provider, so it needs no API key and cannot spend anything. It reads the source and target files, diffs them against the lock file, and reports exactly what a real run would change. It writes nothing (no locale file, no lock update, no run-status snapshot), with a machine provider neither reads nor writes the cache (under provider `none` it reads the translation memory, see below), and takes no write lock, so it never waits on a run in progress.

<Callout>
By default a run deletes nothing: orphaned keys are reported and left in place. `--prune`, or `prune: true` in the config, deletes exactly those keys from the target files and the lock; `--prune --dry-run` previews them.
</Callout>

### Cost estimate [#cost-estimate]

<AvailableFrom version="0.11.0" />

`--estimate` adds a pre-run estimate to the dry run: how many keys would be sent, how many provider requests they would take, and the quantity the provider bills for (tokens for an LLM or an `openai-compatible` endpoint, source characters for DeepL, Google Cloud Translation or LibreTranslate; a self-hosted endpoint or server is not billed, so the estimate reports no API cost). A currency figure appears only when the config has a `rates` block covering the provider and model; otherwise the missing rate key is named instead of inventing `0.00`. See [Estimating cost](https://verbatra.kreitz-webdev.de/docs/estimating-cost.md).

### A subset of locales [#a-subset-of-locales]

<AvailableFrom version="0.9.0" />

`--locales de,fr` narrows the run to part of the configured targets. The other locales keep their files, lock entries, and cache entries as they were. Use it on a rate-limited free tier to translate one locale at a time, or to re-run a single locale that came out partial. The run-status snapshot then describes that subset only.

### Write locks and interrupts [#write-locks-and-interrupts]

A locale waits up to `--lock-timeout` for a contended [write lock](https://verbatra.kreitz-webdev.de/docs/the-lock-file.md#the-write-lock), then fails with `LOCK_CONTENDED` while the other locales still run. A lock left by a verbatra process that is no longer running on this machine is reclaimed at once. The timeout bounds only that wait before any provider call: recording a written file in the lock file always waits up to the ten-minute default, so a written file is never left unrecorded. A wait longer than a second prints a line on stderr naming the lock and its holder (a `lock-wait` record under `--json`), which `--quiet` does not drop. Ctrl-C or SIGTERM releases the run's locks before the command exits.

### Token ceiling [#token-ceiling]

<AvailableFrom version="0.12.0" />

`--max-tokens` is always a hard stop: a provider request that would pass the ceiling is withheld, whatever `budgetBehavior` says, and its keys are listed as `budgetWithheld`. It is counted like a configured [`maxTokens` budget](https://verbatra.kreitz-webdev.de/docs/config-file.md#maxtokens-and-budgetbehavior) and can tighten that budget but never loosen it. A run the ceiling cut short exits `1`. Run `--estimate` first to pick a figure.

### Parallel locales [#parallel-locales]

`--concurrency` is refused with exit `2` when a token budget applies (from `maxTokens` or `--max-tokens`), because which locale loses its remaining work would depend on how the locales interleave; a dry run is exempt. If a whole-run error, such as a corrupt lock file, surfaces while locales are running, no further locale starts, and the running ones finish and release their locks before the command exits `2`.

### Keys a person wrote [#keys-a-person-wrote]

<AvailableFrom version="0.12.0" />

A stale key whose value a person wrote, imported, or changed outside verbatra is kept by default: it is listed as `protected`, not sent to the provider, and stays stale for `verbatra check`. A stderr warning says how many were kept (pinned keys are not counted) and points to `--include-human`, which retranslates them for one run (`humanEdits: "overwrite"`) and so prints no warning. Protected keys do not change the exit code, except in a human-only project. See [Protecting human translations](https://verbatra.kreitz-webdev.de/docs/protecting-human-translations.md).

### Human-only projects [#human-only-projects]

<AvailableFrom version="0.12.0" />

In a [human-only](https://verbatra.kreitz-webdev.de/docs/human-only-workflow.md) project (`provider: { id: "none" }`) no provider is called. Keys the translation memory covers are written, and every other missing or changed key is listed as `unfilled` and exits `3`; a failed or partial locale still wins with `1`. A dry run reads the memory here, so its plan shows the same `cacheHits` and `unfilled` keys, and `--estimate` reports nothing billed; both exit `3` the same way. Without `--json`, a stderr warning says how many keys need a person, and a `next:` hint points to [`verbatra export`](https://verbatra.kreitz-webdev.de/docs/cli/export.md), where you hand the `unfilled` keys off.

### Failed and partial locales [#failed-and-partial-locales]

<AvailableFrom version="0.9.0" />

One locale failing is not fatal: the run finishes and exits `1`. A **partial** locale exits `1` too: its file was written but keys are still missing, typically because a provider sub-batch failed or the integrity gate refused a translation. The human output lists every refused key under `integrity-withheld` with its reason (`placeholder`, `markup`, `icu`, `degenerate`, or `empty`); with `--json`, each locale carries them in `integrityRefusals`. Keys the run flagged for review follow the locale's `N needs-review` count under a `needs-review:` line, each with its [review reasons](https://verbatra.kreitz-webdev.de/docs/translation-safety.md#review-flags); with `--json` they are in `needsReview`.

### Unsupported locales [#unsupported-locales]

<AvailableFrom version="0.12.0" />

A locale the configured DeepL or Google Cloud Translation provider does not support refuses the whole run with `LOCALE_UNSUPPORTED_BY_PROVIDER` and exit `2`, on a dry run and with `--estimate` too, before anything is spent. Leave it out with `--locales`, or see [Language support](https://verbatra.kreitz-webdev.de/docs/language-support.md#checking-support-before-a-run). A glossary or tone the provider cannot apply only adds a notice (`GLOSSARY_UNSUPPORTED_BY_PROVIDER`, `FORMALITY_UNSUPPORTED_BY_PROVIDER`), as do `LOCALE_UNVERIFIED_BY_PROVIDER` and, for an LLM, `LOCALE_NOT_WELL_TESTED`.

### JSON output [#json-output]

Schema: [`translate-envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/translate-envelope.json) validates the whole envelope; [Schemas](https://verbatra.kreitz-webdev.de/docs/cli/output.md#schemas) lists the others.

With `--json`, stdout carries one [envelope](https://verbatra.kreitz-webdev.de/docs/cli/output.md#json-output) whose `result` is a `RunSummary`; [RunSummary](https://verbatra.kreitz-webdev.de/docs/sdk/run-summary.md) lists every field, for the run and for each locale.

### Terminal feedback [#terminal-feedback]

<AvailableFrom version="0.12.0" />

`translate` names the locales and provider before the run, and prints the elapsed time, tokens used, and the next command afterwards. On an interactive terminal the progress is one spinner line; when stderr is piped, every step is a plain line:

```text output
verbatra: translating 2 locales with gemini/gemini-2.5-flash
verbatra: translating de
verbatra: de batch 1/1
verbatra: de: writing
verbatra: de done, 12 translated
verbatra: translating fr
verbatra: fr batch 1/1
verbatra: fr: writing
verbatra: fr done, 12 translated
verbatra: run finished, 2 locales processed
[ok] done in 4.2s, 1830 tokens
next: verbatra check (confirm every locale is in sync)
```

A dry run opens with `dry run over 2 locales, no provider call`, an estimate with `estimating 2 locales, no provider call`, and a human-only project with `filling 2 locales from the translation memory (provider none)`. A dry run or an estimate ends each locale with `would translate` instead. A locale that kept only some keys ends with `partly done`, one that failed with `failed`, and the closing line adds how many failed. A dry run closes with `dry run done in 4.2s, nothing written`. An estimate closes with `estimate done in 4.2s, nothing written` <AvailableFrom version="0.13.0" />. These lines go to stderr, and `--quiet` drops them. See [Terminal output](https://verbatra.kreitz-webdev.de/docs/cli/output.md#terminal-output).

## Exit codes [#exit-codes]

| Code | Meaning |
| --- | --- |
| `0` | every target locale came out complete |
| `1` | the run finished, but a locale failed or came out partial |
| `2` | could not run: a config, source, format, provider, unsupported-locale, or lock-file error, or a usage error |
| `3` | human-only project: the run (a dry run or `--estimate` included) finished cleanly, but keys are left for a human translation |
| `130` | interrupted with Ctrl-C (SIGINT) after releasing the write locks |
| `143` | stopped with SIGTERM after releasing the write locks |

On an interrupt, stderr ends with `verbatra: interrupted (SIGINT), released locks`, or under `--json` with the record `{"type":"interrupted","signal":"SIGINT","locksReleased":true}`. If releasing the locks fails or does not finish within 5 seconds, the line says so instead (`could not release locks: <reason>` or `lock release did not finish within 5s`), and the JSON record carries `"locksReleased":false` with `"reason":"failed"` (plus `message`) or `"reason":"timed-out"` (plus `deadlineMs`). See [the exit-code contract](https://verbatra.kreitz-webdev.de/docs/cli/output.md#exit-codes).

## Related [#related]

- [How it works](https://verbatra.kreitz-webdev.de/docs/how-it-works.md) explains the read, diff, translate, write pipeline.
- [The cache](https://verbatra.kreitz-webdev.de/docs/the-cache.md) explains what `--no-cache` bypasses.
- [Translation safety](https://verbatra.kreitz-webdev.de/docs/translation-safety.md) covers the integrity checks and the needs-review queue.
- [`verbatra diff`](https://verbatra.kreitz-webdev.de/docs/cli/diff.md) previews the same pending keys read-only.
- [CI and exit codes](https://verbatra.kreitz-webdev.de/docs/ci-and-exit-codes.md) shows how to branch on these codes in a pipeline.
