---
title: "Adapters, providers, redaction"
description: "Build a format adapter or a translation provider that verbatra does not ship, and redact secrets from any text you pass to the SDK."
type: "reference"
---
# Adapters, providers, redaction

Build a format adapter or a translation provider that verbatra does not ship, and redact secrets from any text you pass to the SDK.

## Format adapters [#format-adapters]

<AvailableFrom version="0.11.0" />

Everything needed to build a format adapter for a format verbatra does not ship, and to register it alongside the built-in ones. A format from outside verbatra is named with a `custom:` identifier, such as `custom:toml`, which the config accepts exactly like a built-in name. See [Custom format adapters](https://verbatra.kreitz-webdev.de/docs/custom-format-adapters.md) for the full walkthrough, including what you are trusting when you install a third-party adapter: it is fully trusted code, and verbatra does not sandbox it.

### createDefaultRegistry [#createdefaultregistry]

Builds an `AdapterRegistry` holding every adapter verbatra ships, in detection order. Start here rather than from an empty registry, so your own format joins the built-in ones instead of replacing them, then pass the result as `deps.adapterRegistry` to any flow. [`AdapterRegistry`](#adapterregistry) covers `register` and `resolve`.

```ts title="Signature"
function createDefaultRegistry(fs?: AdapterFs): AdapterRegistry;
```

**Returns:** `AdapterRegistry`

**Example:** [Register it and run](https://verbatra.kreitz-webdev.de/docs/custom-format-adapters.md#register-it-and-run)

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

declare const config: VerbatraConfig;
declare const tomlAdapter: FormatAdapter;

const adapterRegistry = createDefaultRegistry().register(tomlAdapter);
const summary = await translate({ config }, { adapterRegistry });
```

### AdapterRegistry [#adapterregistry]

Holds the registered adapters and resolves one for a file. `new AdapterRegistry()` starts empty, so
it reads only the formats you register; `createDefaultRegistry` is the usual start.
`register(adapter)` appends an adapter to the detection order and returns the registry, so
registrations chain. It refuses a format the registry already holds and a malformed `custom:`
identifier, and wraps a `custom:` adapter so that an unexpected throw from it surfaces as an
`AdapterError` naming the format. `resolve(filePath, options)` picks the adapter for one file: by
`options.format` when given, otherwise by detection, which `options.sample`, a leading content
sample, can aid.

```ts title="Signature"
class AdapterRegistry {
  register(adapter: FormatAdapter): this;
  resolve(filePath: string, options?: ResolveOptions): AdapterResolution;
}
```

`resolve` takes these options:

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `sample` | `string` | no | A leading content sample to aid detection. Ignored when `format` is given. |
| `format` | `FormatId` | no | Bypass detection and select this format explicitly. |

**Returns:** `register` returns the registry. `resolve` returns an `AdapterResolution`, discriminated
on `status`: `resolved` carries the `adapter`, `no-match` the `filePath` and the `triedFormats`, and
`ambiguous` the `filePath` and the competing `candidates`, which you settle by passing one as
`format`. An unresolvable file is a status, never an exception.

**Throws:** `AdapterError` [`DUPLICATE_FORMAT`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#duplicate_format) and [`INVALID_FORMAT_ID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#invalid_format_id) from `register`, and [`ADAPTER_FAILED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#adapter_failed) from `resolve` when a `custom:` adapter's own `canHandle` throws during detection

### createFlatFileAdapter [#createflatfileadapter]

Builds a `FormatAdapter` for a flat key/value format, one where every entry is addressed by a single key with no nesting. You supply the format's own `parseEntries`, `serializeEntries` and `extractPlaceholders`; the factory supplies the size-bounded read, the atomic write, extension detection and the structured error handling. `parseEntries` also receives the locale the file is read as, for a format whose one file holds both source and target text, and `serializeEntries` receives the caller's write context, whose optional `sourcePath` names the source-locale file, and, from 0.12.0, the locale being written as its fifth argument, for a format that records its locale inside the file. Give it a `sniff` whenever your extension is a generic one, or your adapter claims every file that shares it and detection reports an ambiguity instead of choosing. The optional hooks are `validateMessage`, `computeInvalidIcuKeys`, `comparePlaceholders` and, from 0.12.0, `compareBranchArms`; `fs` replaces the file-system port, `nodeAdapterFs` by default.

```ts title="Signature"
function createFlatFileAdapter(
  options: FlatFileAdapterOptions,
): FormatAdapter;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `format` | `FormatId` | yes | The format this adapter claims: a built-in name, or a `custom:` identifier of your own. |
| `extensions` | `readonly string[]` | yes | The file extensions this format owns, each including the leading dot. Matched against a path's extension case-insensitively. |
| `sniff` | `Sniff` | no | Optional content check, consulted when a sample is available, so this adapter does not claim every file that merely shares one of its extensions. |
| `parseEntries` | `function` | yes | Parse one file's text into entries keyed by entry key, in document order. Also receives the locale the file is read as, for a format whose one file carries both source and target text and has to know which side to read. Throw an `AdapterError` for content this format cannot represent; anything else thrown is reported as a structural failure. |
| `serializeEntries` | `function` | yes | Render entries back to the format's text, preserving key order. Receives the destination path, the port, the caller's write context and the locale being written, for a format that has to consult the existing file, or the source-locale file, to write in place, or that records the locale inside the file. |
| `extractPlaceholders` | `ExtractPlaceholders` | yes | Find this format's placeholder tokens in one value. |
| `validateMessage` | `ValidateMessage` | no | Optional message-syntax check applied to a candidate value before it is written. |
| `computeInvalidIcuKeys` | `ComputeInvalidIcuKeys` | no | Optional per-file report of the keys whose values are invalid for this format's message syntax. |
| `comparePlaceholders` | `ComparePlaceholders` | no | Optional whole-value placeholder comparison, for a format whose structure a flat token list would lose. Omitted adapters are compared by `extractPlaceholders` plus a flat multiset check. |
| `compareBranchArms` | `CompareBranchArms` | no | Optional check of a translation's plural and select branch arms against its source. |
| `fs` | `AdapterFs` | no | The file-system port to read and write through. Defaults to `nodeAdapterFs`. |

**Returns:** `FormatAdapter`

**Example:** [Build the adapter](https://verbatra.kreitz-webdev.de/docs/custom-format-adapters.md#build-the-adapter)

A `FormatAdapter` reads a file into a `LocaleResource`, the neutral form every adapter and flow shares:

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `locale` | `string` | yes | The locale these entries belong to (for example, "en" or "de"). |
| `namespace` | `string` | yes | The namespace these entries belong to. Every built-in adapter sets it to the locale file's base name without its extension; an adapter you write may choose another value. |
| `format` | `FormatId` | yes | The source format the resource came from, retained for round-trip fidelity on write. |
| `entries` | `object` | yes | Entries addressable by key, in the order the source file defined them. |

### createTreeFileAdapter [#createtreefileadapter]

Builds a `FormatAdapter` for a nested-tree format, one whose entries live at paths through nested objects. It takes `parse` and `serialize` over a tree plus a `deriveEntry` that reports each leaf's placeholders and whether it is a plural form, and `extractPlaceholders` for one value, and handles flattening and unflattening itself. Like the flat factory it takes `format`, `extensions` and an optional `sniff`. It also carries the optional hooks a structured format needs: `validateTree`, `validateMessage`, `computeInvalidIcuKeys`, `comparePlaceholders`, `compareBranchArms` (from 0.12.0), `deriveDescriptions`, `buildWriteTree` and `keyMode`, and `fs` replaces the file-system port, `nodeAdapterFs` by default.

```ts title="Signature"
function createTreeFileAdapter(
  options: TreeFileAdapterOptions,
): FormatAdapter;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `format` | `FormatId` | yes | The format this adapter claims: a built-in name, or a `custom:` identifier of your own. |
| `extensions` | `readonly string[]` | yes | The file extensions this format owns, each including the leading dot. Matched against a path's extension case-insensitively. |
| `sniff` | `Sniff` | no | Optional content check, consulted when a sample is available, so this adapter does not claim every file that merely shares one of its extensions. |
| `parse` | `(content: string) => JsonRecord` | yes | Parse one file's text into a nested tree, preserving key order at every level. Throw an `AdapterError` for content this format cannot represent; anything else thrown is reported as a structural failure. |
| `serialize` | `(tree: OrderedRecord) => string` | yes | Render a nested tree back to the format's text, preserving key order at every level. |
| `deriveEntry` | `DeriveEntry` | yes | Derive the placeholder tokens and the plural flag for one leaf. |
| `extractPlaceholders` | `ExtractPlaceholders` | yes | Find this format's placeholder tokens in one value. |
| `computeInvalidIcuKeys` | `ComputeInvalidIcuKeys` | no | Optional per-file report of the keys whose values are invalid for this format's message syntax. |
| `validateMessage` | `ValidateMessage` | no | Optional message-syntax check applied to a candidate value before it is written. |
| `validateTree` | `ValidateTree` | no | Optional structural check on the parsed tree, throwing an `AdapterError` for a shape this format forbids. |
| `buildWriteTree` | `BuildWriteTree` | no | Optional override for how the tree to write is assembled, for a format that patches the existing document. |
| `keyMode` | `KeyMode` | no | How a dotted key is addressed. Defaults to `"literal-leaf"`. |
| `comparePlaceholders` | `ComparePlaceholders` | no | Optional whole-value placeholder comparison, for a format whose structure flattening would lose. |
| `compareBranchArms` | `CompareBranchArms` | no | Optional check of a translation's plural and select branch arms against its source. |
| `deriveDescriptions` | `DeriveDescriptions` | no | Optional extraction of per-key translator descriptions from the raw file text. |
| `fs` | `AdapterFs` | no | The file-system port to read and write through. Defaults to `nodeAdapterFs`. |

**Returns:** `FormatAdapter`

### nodeAdapterFs [#nodeadapterfs]

The file-system port, typed `AdapterFs`, that every adapter factory uses when none is supplied: a size-bounded read that reports a missing or oversized file as a state rather than throwing, and a write that goes through a temporary file and a rename so an interrupted write never leaves a half-finished locale file. Read and write through this port rather than reaching for `node:fs` in an adapter of your own. It is a convention rather than a boundary, and verbatra cannot enforce it on code it does not own.

```ts title="Signature"
const nodeAdapterFs: AdapterFs;
```

### isCustomFormatId [#iscustomformatid]

Narrows a format identifier to a third-party one: true for a well-formed `custom:` identifier, false for every built-in format name. Useful when your own tooling has to tell the two apart, for example to decide whether a registry has to be supplied before a config can run.

```ts title="Signature"
function isCustomFormatId(
  format: string,
): format is CustomFormatId;
```

**Returns:** `format is CustomFormatId`

## Secret redaction [#secret-redaction]

<AvailableFrom version="0.10.0" />

### redact [#redact]

Scrubs provider API key shapes and the exact current value of any configured provider environment
variable out of a string, replacing each match with `[REDACTED]`. `@verbatra/studio` and
`@verbatra/mcp` both apply it to every value they return to a caller that they did not themselves
generate, such as a glossary term, a file path, or an upstream error message, so a key already
present in your environment or written into a project file can never reach a browser tab, an
agent, or a log line. The `verbatra` CLI runs everything it writes to stdout and stderr, the
`--json` envelope included, through it as a last step.

```ts title="Signature"
function redact(text: string): string;
```

**Returns:** `string`

That includes the variable an `openai-compatible` provider names through `apiKeyEnvVar`. It is
declared when `loadConfig` loads a config naming it or when that provider is built, and from then on
its value is scrubbed exactly like the built-in ones. Declarations are never removed. A value
shorter than eight characters is not scrubbed by value, so a short variable can never wipe
unrelated text.

A DeepL key is recognized by its shape alone only when it carries the `:fx` suffix of a free key,
or when a bare UUID sits in a key context: after a `DeepL-Auth-Key` header, or after a name such as
`auth_key`, `authKey`, `--auth-key`, `api_key`, `deeplKey`, `DEEPL_API_KEY`, or `DEEPL_AUTH_KEY`
followed by `:`, `=`, a URL-encoded `%3D`, or a space, quoted or not, also inside JSON that was
serialized into a string. Any other UUID, such as one in a file path or a request id, stays
readable. A Pro key held in `DEEPL_API_KEY` is still scrubbed wherever it appears, by its value.

An `sk-` token is redacted once at least 32 letters and digits follow `sk-`, counted across its `-`
and `_` segments, whatever its prefix. That one rule covers every OpenAI and Anthropic key shape;
a known prefix such as `sk-proj-` or `sk-ant-` does not redact a shorter token on its own. The token
counts only when no letter or digit directly precedes it: a JSON escape such as `\n` or `\u0007`, a
percent-encoded character such as `%3D`, an ANSI CSI sequence such as `\x1b[31m`, `\x1b[2K` or
`\x1b[?25h`, an ANSI charset designation such as `\x1b(B`, or either ANSI form JSON-escaped as
`\u001b` is a boundary too. A Slovak locale path or key such as `locales/sk-SK.json` or
`sk-banner_headline` stays readable. A long Slovak-like key reaches the threshold and is redacted,
though: `sk-SK_settings_notifications_email_digest` holds 34 letters and digits after `sk-`, just
like a long camelCase key such as `sk-onboardingWelcomeScreenPrimaryButton`.

A Gemini key is recognized by its shape alone: `AIza` followed by 35 letters, digits, `_` or `-`.

### declareProviderKeyEnvVar [#declareproviderkeyenvvar]

<AvailableFrom version="0.12.0" />

Declares the variable a `provider` block names as its key source, so `redact` scrubs its value.
Today only an `openai-compatible` provider with `apiKeyEnvVar` names one; any other provider is a
no-op. `loadConfig` and building the provider already do this, and Studio and the MCP server also
do it for the config they receive, so call it only when a config reaches your own code some other
way. The declaration is process-wide and shared by every copy of the SDK loaded in the process.

```ts title="Signature"
function declareProviderKeyEnvVar(
  provider: ProviderConfig,
): void;
```

**Returns:** `void`

## Custom providers [#custom-providers]

<AvailableFrom version="0.12.0" />

A provider the SDK does not ship plugs in through `deps.createProvider`, a `CreateProvider` that
returns a `TranslationProvider`. The SDK exports the whole contract as types, so such a provider
type-checks against the same interface the built-in ones implement. That includes the types its
fields use: `Tone`, `PluralCategories`, `PlaceholderExtractor` and `PlaceholderComparator` in the
request, `Usage`, `ProviderNotice`, `ProviderNoticeCode` and `ReviewFlag` in the result, and
`NetworkPolicy` and `NetworkRule` in `ProviderNetwork`.

```ts title="Example"
import {
  translate,
  type CreateProvider,
  type TranslateRequest,
  type TranslateResult,
  type TranslationProvider,
  type VerbatraConfig,
} from "@verbatra/sdk";

declare const config: VerbatraConfig;
declare function callInHouseService(
  locale: string,
  entries: TranslateRequest["entries"],
): Promise<ReadonlyMap<string, string>>;

const provider: TranslationProvider = {
  id: "in-house",
  kind: "machine-translation",
  supportsGlossary: false,
  async translateBatch(request: TranslateRequest): Promise<TranslateResult> {
    const values = await callInHouseService(request.targetLocale, request.entries);
    return { values, integrity: new Map(), notices: [] };
  },
};

const createProvider: CreateProvider = () => provider;
const summary = await translate({ config }, { createProvider });
```

- `TranslationProvider` is the one contract: `id`, `kind` (a `ProviderKind`), `supportsGlossary`,
  and `translateBatch(request)`. Treat every value in a request as untrusted data, read any API key
  from the environment rather than from the request, and throw a `ProviderError` without secrets in
  it when a call fails.
- `ProviderKind` is `"llm"` or `"machine-translation"`. The `kind` on your provider object is
  descriptive only: the SDK decides plural generation and what the sensitive-data guard scans from
  the kind of the configured provider `id`, not from this field.
- `TranslateRequest` carries `sourceLocale`, `targetLocale`, the `entries` to translate, and the
  optional `glossary` (the result of [`glossaryForLocale`](https://verbatra.kreitz-webdev.de/docs/sdk/config.md#glossaryforlocale) for the target
  locale), `tone`, `maxLength`, and `pluralCategories`. It also carries `extractPlaceholders` and,
  for some formats, `comparePlaceholders`, which your provider uses for the integrity check, plus
  `signal`, which aborts an in-flight call, `onRepair`, and `foreignPlaceholders`:
  per key, the tokens of another placeholder syntax to protect like placeholders.
- `TranslateResult` returns `values` and `integrity` keyed by entry key, plus optional `usage`,
  `notices`, and `reviewFlags`. A key missing from `values` counts as not translated, so a provider
  can leave out a value it cannot translate safely. The SDK runs its own integrity gate on every
  returned value either way.
- `ProviderRetryListener` is the type of `CreateProviderHooks.onRetry`. It receives a
  `ProviderRetry`, `{ attempt, delayMs?, status? }`, per retry, and each call becomes a
  `provider-retry` [progress event](https://verbatra.kreitz-webdev.de/docs/sdk/run.md#progress-events).
- `ProviderNetwork` is what `CreateProviderContext.network` holds when the network policy restricts
  where a provider may connect: the effective `policy`, the `env` the endpoint was resolved from,
  and optional test `deps`. A custom provider may ignore it, but then only the check the SDK runs
  before calling the factory applies to its requests.

**Example:** [Test your integration without a key](https://verbatra.kreitz-webdev.de/docs/programmatic-api.md#test-your-integration-without-a-key)
