---
title: "Run translations"
description: "translate, watch and the entry points around a run, plus pseudolocales and typed catalogs, all callable from your own Node.js scripts."
type: "reference"
---
# Run translations

translate, watch and the entry points around a run, plus pseudolocales and typed catalogs, all callable from your own Node.js scripts.

## Translate and watch [#run-translations]

### translate [#translate]

The one-shot flow: read the source, diff each target locale against the lock baseline, send the
missing and changed keys to the provider, run the integrity checks, write the
locale files, and update the lock. Use it for scripts, build steps, and CI jobs.

```ts title="Signature"
function translate(
  input: TranslateInput,
  deps?: TranslateDeps,
): Promise<RunSummary>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `config` | `VerbatraConfig` | yes | The resolved project config, normally from loadConfig. |
| `cwd` | `string` | no | Directory the `files.pattern` is resolved against, and where the lock-file, the translation memory, and the run-status file live. Defaults to the project root of the config object loadConfig returned, else the process working directory; a copied or rebuilt config loses that root, so pass `cwd` from resolveProjectRoot. |
| `locales` | `readonly string[]` | no | Restrict the run to a subset of the configured target locales, which is how you translate one locale at a time against a rate-limited provider. Defaults to every configured target. The result keeps the configured order whatever order the subset is given in, and any locale named here that is not a configured target fails the whole run with `UNKNOWN_LOCALE` before anything is read or spent. An explicit empty array selects no locale at all rather than every locale. Locales left out are untouched: their files, their lock entries, and their translation-memory entries all stay as they were. The run-status file always describes the most recent run alone, so a subset run narrows it to that subset. |
| `dryRun` | `boolean` | no | Compute the whole run but write nothing and call no provider. The returned RunSummary reports what would have happened, which makes this safe to run in CI to preview work without spending anything. Defaults to false. In human-only mode (provider `none`) it reads the translation memory, so the plan matches a live run. |
| `estimate` | `boolean` | no | Compute a pre-run cost estimate and return it on RunSummary.estimate. Implies `dryRun`: an estimate constructs no provider, reads no API key, makes no network call, and writes no file, so it is safe to run anywhere. Defaults to false. The figure bounds the plan rather than the invoice: every provider call a live run schedules is counted, plural generation included, and the prompt is measured from the request payload that would be sent. Provider-side retries, a repair round, and a language that expands past the allowance can each push a live run above it; EstimateCaveatCode names them all. It carries a currency amount only when the config supplies a `rates` block covering the configured provider and model; otherwise it reports the quantity and says why the money is missing. See RunEstimate. |
| `prune` | `boolean` | no | Remove keys that no longer exist in the source. Defaults to the config's `prune`, then to false, so orphaned keys are reported but kept unless removal is asked for explicitly. |
| `generatePlurals` | `boolean` | no | Generate the plural categories a target language requires rather than translating each category separately. Defaults to the config's `generatePlurals`, then to false. Takes effect only for the `i18next-json` format with an LLM provider; any other format, a machine-translation provider, and provider `none` generate nothing. |
| `onLockWait` | `LockWaitListener` | no | Called while waiting on another process's write lock, so a CLI can explain a stall instead of appearing to hang. Never called for a lock this process holds itself, such as the lock-file guard a sibling locale holds on a concurrent run. Fires first once a wait has lasted a second. The SDK writes no output itself. |
| `onProgress` | `ProgressListener` | no | Called as locales and sub-batches start and finish, for progress reporting. The SDK writes no output itself. |
| `lockAcquireTimeoutMs` | `number` | no | How long, in milliseconds, to wait for a locale's write lock before that locale fails with `LOCK_CONTENDED`, and for the lock-file guard before a locale whose respelled state could not be moved fails with `LOCALE_STATE_NOT_CARRIED_OVER`. Both waits happen before any provider call. Defaults to ten minutes. It does not bound the lock-file guard a locale takes to record its result once its target file is written: that wait always allows the ten-minute default, so a written file is not left unrecorded. Not used on a dry run, which takes no lock. |
| `concurrency` | `number` | no | How many locales to run at once. Must be an integer of at least 1; defaults to 1. On a live run it cannot be combined with a token budget, configured or passed as `maxTokens`: the ceiling would still hold, but which locale loses its remaining work would depend on the order the locales interleave, so the run would not be reproducible. A whole-run error raised once locales are running starts no further locale, and the locales in flight finish and release their write locks before `translate` rejects, so the rejection arrives after the slowest one. |
| `cache` | `boolean` | no | Consult and update the translation memory, fuzzy reuse included. Defaults to true. Turning it off forces every key through the provider, which is what to do when you want to re-pay for a fresh translation. A dry run never reads the memory, whatever this says, except in human-only mode (provider `none`), where the memory is all a live run would use. The memory is the local CACHE_FILE_NAME file; `false` is the CLI's `--no-cache`. |
| `humanEdits` | `"overwrite" \| "protect" \| "suggest"` | no | Overrides the config's `humanEdits` for this run only (see HumanEditsPolicy). Set `overwrite` to retranslate stale keys a person wrote, as the CLI's `--include-human` flag does. Keys matching the config's `pinnedKeys` stay protected whatever this says. A dry run reads the provenance file too, unless this resolves to `overwrite`. |
| `maxTokens` | `number` | no | A token ceiling for this run alone, as the CLI's `--max-tokens` flag sets it. It is always a hard stop: a provider request that would pass it is withheld rather than sent, whatever the config's `budgetBehavior` says, and its keys are listed in LocaleSummary.budgetWithheld. When the config also sets `maxTokens`, the lower of the two applies, so this can tighten the configured budget but never loosen it. Must be a whole number of at least 1. Defaults to the config's `maxTokens` and `budgetBehavior` alone. |
| `signal` | `AbortSignal` | no | Cancels the run when aborted. No further locale starts, and a wait for a write lock stops. The locales running at the time send no further request, and an in-flight request is abandoned rather than counted as a provider failure, unless the provider reported a failure of its own. What already arrived is still written and recorded, the keys left over get no lock-file entry and stay pending for the next run, the run status is recorded, and every lock is released. The call then resolves rather than rejects: the result carries RunSummary.cancelled, each locale kept from starting is `failed` with the `RUN_CANCELLED` error code, and locales stopped while running carry a `RUN_CANCELLED` notice and are `partial`, or `failed` when no key landed and others were withheld. A dry run only stops early. Defaults to none. |

**Returns:** [`RunSummary`](https://verbatra.kreitz-webdev.de/docs/sdk/run-summary.md)

**Throws:** [`UNKNOWN_LOCALE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#unknown_locale), [`LOCALE_UNSUPPORTED_BY_PROVIDER`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_unsupported_by_provider), [`UNKNOWN_FORMAT`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#unknown_format), [`CONCURRENCY_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#concurrency_invalid), [`CONCURRENCY_BUDGET_CONFLICT`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#concurrency_budget_conflict), [`MAX_TOKENS_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#max_tokens_invalid), [`LOCK_TIMEOUT_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#lock_timeout_invalid), [`LOCALE_LAYOUT_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_layout_invalid), [`LOCALE_PATH_COLLISION`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_path_collision), [`SOURCE_UNREADABLE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_unreadable), [`SOURCE_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_invalid), [`PROVIDER_CONSTRUCTION_FAILED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#provider_construction_failed), [`NETWORK_POLICY_VIOLATION`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#network_policy_violation), [`CONFIG_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_invalid), [`LOCK_FILE_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#lock_file_invalid), [`PROVENANCE_FILE_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#provenance_file_invalid)

**Example:** [Translate and read the summary](https://verbatra.kreitz-webdev.de/docs/sdk-quickstart.md#translate-and-read-the-summary)

What the table does not cover:

- `cache` is the local [translation-memory cache](https://verbatra.kreitz-webdev.de/docs/the-cache.md) (`verbatra.cache.json`, also exported as `CACHE_FILE_NAME`).
- `humanEdits` (from 0.12.0): by default, stale keys a person wrote, imported, or changed outside
  verbatra are kept and listed in `LocaleSummary.protected`. See
  [Protecting human translations](https://verbatra.kreitz-webdev.de/docs/protecting-human-translations.md).
- `onProgress` receives the [progress events](#progress-events) below.
- `maxBatchSize` and `budgetBehavior` are config-only; there is no per-run override.

A whole-run problem throws an `SdkError`; a single locale failing never throws and lands as
`status: "failed"` on that locale's entry while the run continues. See
[the anatomy of `RunSummary`](https://verbatra.kreitz-webdev.de/docs/sdk/run-summary.md#the-runsummary-anatomy).

### Progress events [#progress-events]

<AvailableFrom version="0.12.0" />

`onProgress` receives a `ProgressEvent`, a union discriminated on `type`. A minor version may add types, so ignore one you do not recognize.

| `type` | When | Fields |
| --- | --- | --- |
| `locale-started` | a locale's work begins | `locale`, `localeIndex`, `totalLocales` |
| `locale-planned` | after the translation memory is consulted, before the first batch is sent; only when a provider is called | `locale`, `keys`, `batches`, `cacheHits` |
| `sub-batch` | just before each translation batch is attempted | `locale`, `batchIndex`, `totalBatches` |
| `provider-retry` | a built-in provider retries a request that failed with a retryable status | `attempt`, `delayMs?`, `status?` |
| `repair` | an LLM provider re-requests the keys its first response left out | `locale`, `keys` |
| `split-retry` | a batch whose output was cut off is retried in two halves | `locale`, `keys` |
| `batch-finished` | a batch has finished, whether it succeeded or not | `locale`, `batchIndex`, `totalBatches`, `durationMs`, `usage?` |
| `writing` | just before a locale's file is written | `locale` |
| `locale-finished` | a locale's work ends | `locale`, `status`, `translated`, `localeIndex`, `totalLocales` |
| `run-finished` | every locale has finished | `localesCompleted`, `localesFailed` |
| `change-detected` | `watch` only: a change to the source file has settled | `paths` |
| `idle` | `watch` only: a run finished and none is queued | none |

`provider-retry` names no locale, because one provider serves the whole run. Gemini reports the delay and the status, Anthropic, OpenAI, OpenCode and openai-compatible report the attempt as their client starts it, and DeepL, Google Cloud Translation and LibreTranslate report none. A dry run and an estimate emit only `locale-started`, `locale-finished` and `run-finished`. The CLI's `--json` progress records keep to the four original types: `locale-started`, `sub-batch`, `locale-finished` and `run-finished`.

`extract`, `diff` with `unused`, and `doctor` with `literals` take an `onProgress` of their own. It receives a `ScanProgressEvent`, `{ type: "files-scanned", scanned, total }`, once after each application source file the scan reads.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | `"files-scanned"` | yes | Discriminant, reserved so later scan events can join a union. |
| `scanned` | `number` | yes | How many files have been scanned so far. |
| `total` | `number` | yes | How many files the scan will read in total. |

A custom `CreateProvider` receives a third argument, `CreateProviderHooks`, whenever a run reports progress. Call its `onRetry` with `{ attempt, delayMs?, status? }` and your provider's retries show up as `provider-retry` events.

### resolveDryRun [#resolvedryrun]

<AvailableFrom version="0.11.0" />

Answers whether a set of run options resolves to a dry run. It takes the `dryRun` and `estimate`
you are about to hand `translate` and returns whether that run will construct a provider, write a
file, or spend anything. This is the one place the rule that `estimate` implies `dryRun` is
decided, so reach for it whenever something around a run has to know in advance rather than
restating the implication and drifting from it.

```ts title="Signature"
function resolveDryRun(input: {
  readonly dryRun?: boolean | undefined;
  readonly estimate?: boolean | undefined;
}): boolean;
```

**Returns:** `boolean`

```ts title="Example"
import { resolveDryRun, translate, type VerbatraConfig } from "@verbatra/sdk";

declare const config: VerbatraConfig;

const options = { dryRun: false, estimate: true };
resolveDryRun(options); // true

const summary = await translate({ config, ...options });
```

### watch [#watch]

Watches the source locale file and re-runs `translate` on every debounced change. It fires one
initial run immediately at startup, then one run per settled change. Runs are serialized: changes
during a run collapse into a single follow-up.

```ts title="Signature"
function watch(
  input: WatchInput,
  deps?: WatchDeps,
): Promise<WatchController>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `config` | `VerbatraConfig` | yes | The resolved project config, normally from loadConfig. |
| `cwd` | `string` | no | Directory the `files.pattern` is resolved against, and where each run's lock-file, translation memory, and run-status file live. Defaults to the project root of the config object loadConfig returned, else the process working directory; a copied or rebuilt config loses that root, so pass `cwd` from resolveProjectRoot. |
| `locales` | `readonly string[]` | no | Restrict every run of the session to a subset of the configured target locales. Validated once when watching starts, so an unconfigured locale throws `UNKNOWN_LOCALE` before any watching begins rather than failing on each run. An explicit empty array selects no locale at all. |
| `debounceMs` | `number` | no | How long to coalesce rapid source changes before running. Defaults to 300 milliseconds. |
| `onRun` | `(result: WatchRunResult) => void` | yes | Called after every run with its outcome. This is the only way to observe a watch session, since watch itself resolves as soon as watching starts. |
| `onReady` | `(() => void)` | no | Called once when the session's startup checks have passed and the watcher is attached, right before the initial run starts, so a caller can announce the session ahead of that run's output. |
| `onLockWait` | `LockWaitListener` | no | Called while waiting on another process's write lock, never for one this process holds. |
| `onProgress` | `ProgressListener` | no | Called as each run plans, sends and writes its locales, and as the watcher detects a change or goes idle, for progress reporting. |
| `lockAcquireTimeoutMs` | `number` | no | How long, in milliseconds, to wait for a locale's write lock, before any provider call, before that locale fails with `LOCK_CONTENDED` on the run's summary. Defaults to ten minutes. The lock-file guard a locale takes to record its result after writing its target file always allows the ten-minute default instead. |
| `concurrency` | `number` | no | How many locales to run at once. Defaults to 1. Validated once when watching starts, and applied to every run. |
| `cache` | `boolean` | no | Consult and update the translation memory, fuzzy reuse included. Defaults to true. |

**Returns:** `WatchController`

**Throws:** [`UNKNOWN_LOCALE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#unknown_locale), [`LOCALE_UNSUPPORTED_BY_PROVIDER`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_unsupported_by_provider), [`CONCURRENCY_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#concurrency_invalid), [`LOCK_TIMEOUT_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#lock_timeout_invalid), [`CONCURRENCY_BUDGET_CONFLICT`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#concurrency_budget_conflict), [`LOCALE_LAYOUT_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_layout_invalid), [`LOCALE_PATH_COLLISION`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_path_collision), [`SOURCE_UNREADABLE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_unreadable), [`NETWORK_POLICY_VIOLATION`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#network_policy_violation), [`PROVIDER_CONSTRUCTION_FAILED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#provider_construction_failed), [`CONFIG_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_invalid)

**Example:** [Watch in a long-running process](https://verbatra.kreitz-webdev.de/docs/programmatic-api.md#watch-in-a-long-running-process)

`debounceMs` defaults to 300. `onRun` is called once per run with a `WatchRunResult`:
`{ status: "succeeded", summary }` or
`{ status: "failed", error: { code, message, causeCode?, hint? } }`, where `causeCode` names the
coded error the failure wraps and `hint` is the next step `errorHint` gives. `onReady` is called
once, after the startup checks pass and right before the initial run, so you can announce the
session ahead of that run's output. If `onReady` throws, the watcher is closed and `watch` rejects
with that error before any run starts. The SDK does no logging; `onRun` is the only output.
`concurrency`, `cache`, `onProgress`, `onLockWait`, and `lockAcquireTimeoutMs` are passed through to
every run and behave exactly as on `translate`. `locales` narrows every run of the session to that
subset and is validated once at startup, not per cycle.

Returns a `WatchController` with one method, `stop()`, which closes the watcher and awaits the
in-flight run. Every code on the **Throws** line is refused at startup, before the watcher exists,
so `watch` itself rejects rather than returning a controller. Two of them weigh more on a session
than on one run: a `concurrency` above 1 against a config that sets `maxTokens` always throws
`CONCURRENCY_BUDGET_CONFLICT`, because there is no dry-run watch, and, from 0.12.0, a provider that
cannot be constructed, such as one whose API key variable is unset, throws
`PROVIDER_CONSTRUCTION_FAILED` once instead of failing every run. Every failure after startup is
surfaced through `onRun` and watching continues.

### releaseHeldLocks [#releaseheldlocks]

<AvailableFrom version="0.12.0" />

Deletes every write lock the current process holds, for a process about to exit without letting its
in-flight runs finish: the CLI calls it when an interrupt stops `translate` or `import`, and when a
second interrupt force-stops `watch`. The runs that took those locks lose their protection, so call
it only immediately before the process exits. It takes no arguments and rejects with an
`AggregateError` naming the lock paths only when a held lock could not be deleted, after attempting
every lock. A lock that a still-running operation is taking, or tries to take, while it runs is
refused with `RUN_CANCELLED` and leaves no file behind; it waits at most three seconds for such an
acquisition.

```ts title="Signature"
function releaseHeldLocks(): Promise<void>;
```

**Returns:** `void`

A lock left behind anyway, by a process killed outright, does not need it: a waiting run on the
same machine sees that the recorded process is gone and reclaims the lock itself. See
[the write lock](https://verbatra.kreitz-webdev.de/docs/the-lock-file.md#the-write-lock).

## Pseudolocalization [#pseudolocalization]

### pseudolocalize [#pseudolocalize]

<AvailableFrom version="0.11.0" />

Builds a fake locale from the source strings alone, so layout truncation and strings that were
never extracted for translation show up before any API key exists. Every value is accented,
expanded by roughly a third of its translatable length, and wrapped in `[` and `]` boundary
markers, while placeholders stay untouched in every syntax the adapters recognise, including ICU
plural and select arms, markup tags, and escape sequences. It constructs no provider, reads no API
key, and makes no network request.

```ts title="Signature"
function pseudolocalize(
  input: PseudolocalizeInput,
  deps?: PseudolocalizeDeps,
): Promise<PseudolocalizeResult>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `config` | `VerbatraConfig` | yes | The resolved project config, normally from loadConfig. |
| `cwd` | `string` | no | Directory the `files.pattern` is resolved against. Defaults to the project root of the config object loadConfig returned, else the process working directory; a copied or rebuilt config loses that root, so pass `cwd` from resolveProjectRoot. |
| `mode` | `"accented" \| "bidi"` | no | The transform to apply. Defaults to `accented`. `bidi` wraps every word of translatable text in a right-to-left mark and override (`U+200F U+202E` before, `U+202C U+200F` after), so it renders right to left while the stored letters keep their order; placeholders, ICU syntax, markup, digits and punctuation stay outside the override, and nothing is accented or padded. |
| `locale` | `string` | no | The pseudolocale's BCP-47 code. Defaults to `en-XA` in `accented` mode and to `ar-XB` in `bidi` mode, a code that resolves to right-to-left text direction, so an application that derives its layout direction from the locale flips without further setup. In `bidi` mode an `i18next-json` plural keyed by suffix also gets every CLDR category the pseudolocale's language needs and the source lacks (for `ar-XB`, `_zero`, `_two`, `_few` and `_many`), filled from the source's `other` form, so no count falls back to another language. It must not be the source locale or any configured target locale, compared case-insensitively, so a pseudolocale can never stand in for a real translation. |
| `out` | `string` | no | Directory the pseudolocale file is written under, relative to `cwd`. Defaults to `.verbatra-local/pseudo`, which `verbatra init` already adds to `.gitignore`. The configured `files.pattern` is expanded inside it, so the file keeps the layout the application expects. It must name a directory inside `cwd`: an absolute path, one that climbs out with `..`, and `cwd` itself are all refused, as is a directory under which the expanded pattern would place the pseudolocale file beside a configured locale file, so a generated pseudolocale never lands outside the project or beside the real translations where nothing ignores it. The same holds after symbolic links are resolved, so a linked directory cannot carry the file outside `cwd` or into the directory of a configured locale file. |

**Returns:** `PseudolocalizeResult`

**Throws:** [`PSEUDO_OUTPUT_CONFLICT`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#pseudo_output_conflict), [`UNKNOWN_FORMAT`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#unknown_format), [`LOCALE_LAYOUT_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_layout_invalid), [`LOCALE_PATH_COLLISION`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_path_collision), [`SOURCE_UNREADABLE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_unreadable), [`SOURCE_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_invalid), [`TARGET_UNWRITABLE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#target_unwritable)

**Example:** [Build a pseudolocale](https://verbatra.kreitz-webdev.de/docs/programmatic-api.md#build-a-pseudolocale)

Every generated value is held to the same integrity gate a provider translation must pass. One
that would not pass it is copied from the source verbatim and named in `copied` rather than written
in a broken form, so the entry is still present and still renders.

<AvailableFrom version="0.12.0" />

`mode` picks the transform from `PSEUDO_MODES`: `accented`, the default, or `bidi`, which instead
wraps every word of translatable text in a right-to-left mark and override and leaves
placeholders, ICU syntax, markup, digits and punctuation outside it, accenting nothing. In `bidi`
mode `locale` defaults to `ar-XB`, a code that resolves to right-to-left direction; see
[`verbatra pseudo`](https://verbatra.kreitz-webdev.de/docs/cli/pseudo.md) for how its Arabic plural rules meet your source's plural
forms.

`locale` defaults to `en-XA` and `out` to
`.verbatra-local/pseudo`, which `verbatra init` already adds to `.gitignore`; the configured
`files.pattern` is expanded inside it, so the file keeps the layout the application expects. A
pseudolocale that names a configured locale, or that would land on a configured locale file, is
refused with `PSEUDO_OUTPUT_CONFLICT` before anything is read or written, so it can neither
overwrite a real translation nor stand in for one; so is an `out` that is absolute, climbs out of
`cwd`, or names `cwd` itself, such as `.`, and, when `deps.fs` implements `realpath` as the
default does, one that does any of this, lands in the directory of a configured locale file, or
lands on another file the project depends on, once symbolic links are resolved. Because the file lives outside `files.pattern`,
`translate` never spends on it and `check` and `diff` never report it as drifted.

Returns a `PseudolocalizeResult`: the `locale`, the `mode`, the absolute `path`, how many `entries` were
processed, how many were `transformed`, the keys that were `copied`, and whether anything was
`written`. The transform is deterministic, so a second run over unchanged source rewrites nothing
and reports `written` as false. For `xliff` and `apple-xcstrings`, whose writers only patch a
document that already exists, the output is re-copied from the source on every write, so a key
added to the source after the first run still reaches the pseudolocale, and a copied XLIFF has its
`target-language` rewritten to the pseudolocale.

## Typed catalogs [#typed-catalogs]

### generateTypes [#generatetypes]

<AvailableFrom version="0.11.0" />

Declares a project's source catalog as TypeScript: a union of every key it holds and, per key, the
arguments its message interpolates. Type a translation function against the result and a misspelled
key or a missing interpolation argument becomes a compile error instead of a runtime lookup failure.
It spends nothing: no provider is constructed, no API key environment variable is read, and no
network request is made.

```ts title="Signature"
function generateTypes(
  input: GenerateTypesInput,
  deps?: GenerateTypesDeps,
): Promise<GenerateTypesResult>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `config` | `VerbatraConfig` | yes | The resolved project config, normally from loadConfig. |
| `cwd` | `string` | no | Directory the `files.pattern` and the output path are resolved against. Defaults to the project root of the config object loadConfig returned, else the process working directory; a copied or rebuilt config loses that root, so pass `cwd` from resolveProjectRoot. |
| `out` | `string` | no | Where to write the declaration, relative to `cwd`. Defaults to DEFAULT_TYPES_PATH. Refused with `TYPES_OUTPUT_CONFLICT`, before anything is read or written, when it names no file, is absolute, climbs out of `cwd`, does not end in `.ts`, `.mts` or `.cts`, or names a configured locale file, the lock file, the provenance file, the translation-memory cache, a file verbatra searches for its configuration, the GenerateTypesInput.configPath file, or the GenerateTypesInput.glossaryPath file. When the file-system port implements `realpath`, the same checks run again after symbolic links are resolved. Names are compared case-insensitively. A generating run also refuses to replace an existing file there unless that file begins with the header line verbatra writes, after any leading byte order mark and blank lines, and refuses one too large to verify. |
| `configPath` | `string` | no | The configuration file `config` was loaded from, absolute or relative to `cwd`. It is refused as the output path even when its name is not one verbatra searches for. |
| `glossaryPath` | `string` | no | The glossary file the config names, absolute or relative to `cwd`, normally the `path` of a file-backed LoadedConfig.glossary. It is refused as the output path. |
| `check` | `boolean` | no | Compare instead of writing. The run reports whether the file on disk matches what a fresh generation would produce and leaves every file untouched. |

**Returns:** `GenerateTypesResult`

**Throws:** [`TYPES_OUTPUT_CONFLICT`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#types_output_conflict), [`TYPES_UNWRITABLE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#types_unwritable), [`UNKNOWN_FORMAT`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#unknown_format), [`LOCALE_LAYOUT_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_layout_invalid), [`LOCALE_PATH_COLLISION`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#locale_path_collision), [`SOURCE_UNREADABLE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_unreadable), [`SOURCE_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#source_invalid)

**Example:** [Generate a typed catalog](https://verbatra.kreitz-webdev.de/docs/programmatic-api.md#generate-a-typed-catalog)

It reads one file (the source locale catalog) and writes one file (the declaration); a target locale
file is never touched. The keys are exactly what the format adapter already produced, in document
order, so two runs over an unchanged catalog write byte-identical bytes. Arguments come from the
placeholder tokens the adapter extracted, except in the ICU message formats (`next-intl-json` and
`arb`), where each message is read with the same ICU parser the adapter uses. Every key is emitted
as a quoted string literal, so one carrying a dot, a reserved word, a leading digit, a quote, or
nothing at all is declared verbatim rather than dropped or re-split, and a key the adapter reported
in `excludedLeafPaths` is never declared at all.

What it declines to claim matters as much as what it declares. A message with no placeholders gets a
shape that makes passing any argument a type error; one whose placeholders name their arguments gets
an object shape; one whose placeholders are numbered or anonymous gets a readonly tuple. An ICU
argument only some `select` or `plural` branches use is declared as an optional member. Argument
types come only from what the catalog records: `number` for an ICU `number`, `plural` or
`selectordinal` argument and for a numeric formatter or conversion, `Date | number` for an ICU
`date` or `time` argument and an i18next `datetime` formatter, `string` for an ICU `select` argument
and a printf string conversion, and a `string | number` alias everywhere else, never a permissive
`any`. An i18next unescaped interpolation (`{{- name}}`) is declared by the name behind its prefix.
A message the adapter reported as invalid, one that appears to both name and number its arguments,
and one whose placeholder index is out of range are declared with their arguments reported in
`unresolved` rather than silently declared as taking none. Plural siblings are never collapsed: each
is declared on its own, and the ones the adapter marked plural are listed.

`out` defaults to
`DEFAULT_TYPES_PATH` (`verbatra-types.d.ts`). Before anything is read or written,
`TYPES_OUTPUT_CONFLICT` refuses an `out` that names no file (including one ending in a path
separator), is absolute, climbs out of `cwd` with `..`, names `cwd` itself (`.`), or does not end
in `.ts`, `.mts` or `.cts`,
and one naming a configured locale file, the lock file, the provenance file, the translation-memory cache, a file
verbatra searches for its configuration, `configPath`, the configuration file `config` was loaded
from, or `glossaryPath`, the glossary file it names; names are compared case-insensitively, and when
`deps.fs` implements `realpath`, as the default does, the same checks run again on where the path
lands once symbolic links are resolved. A generating run also refuses, with the same code, to
replace an existing file that does not begin with the header line verbatra writes, and leaves that
file untouched. The full list is on the [`verbatra types`](https://verbatra.kreitz-webdev.de/docs/cli/types.md) page. With `check` set
nothing is written at all, nothing is refused for its content, and the run only reports whether the
committed file still matches, which is the shape a CI gate wants.

Returns a `GenerateTypesResult`: the absolute `path`, the `sourcePath` it was built from, how many
`keys` were declared and how many of those take at least one argument verbatra could determine
(`withArguments`, which never counts an `unresolved` key), the `unresolved` keys and the `excluded`
and `plural` ones, whether anything was `written`, whether the file on disk was `stale` when the run
started, whether it was `missing` altogether (a missing file is also `stale`), and whether this was a
`check` run.
