Skip to content

Reference / SDK

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 Available from 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 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

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 covers register and resolve.

Signature
function createDefaultRegistry(fs?: AdapterFs): AdapterRegistry;

Returns: AdapterRegistry

Example: Register it and run

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

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.

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

resolve takes these options:

Prop

Type

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 and INVALID_FORMAT_ID from register, and ADAPTER_FAILED from resolve when a custom: adapter's own canHandle throws during detection

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.

Signature
function createFlatFileAdapter(
  options: FlatFileAdapterOptions,
): FormatAdapter;

Prop

Type

Returns: FormatAdapter

Example: Build the adapter

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

Prop

Type

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.

Signature
function createTreeFileAdapter(
  options: TreeFileAdapterOptions,
): FormatAdapter;

Prop

Type

Returns: FormatAdapter

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.

Signature
const nodeAdapterFs: AdapterFs;

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.

Signature
function isCustomFormatId(
  format: string,
): format is CustomFormatId;

Returns: format is CustomFormatId

Secret redaction Available from 0.10.0

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.

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 Available from 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.

Signature
function declareProviderKeyEnvVar(
  provider: ProviderConfig,
): void;

Returns: void

Custom providers Available from 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.

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 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.
  • 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

Edit on GitHub

On this page