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.
function createDefaultRegistry(fs?: AdapterFs): AdapterRegistry;Returns: AdapterRegistry
Example: Register it and run
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.
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_ and INVALID_ from register, and ADAPTER_ 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.
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.
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.
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.
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.
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_, authKey, --auth-key, api_, deeplKey, DEEPL_, or DEEPL_
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_ 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_ stays readable. A long Slovak-like key reaches the threshold and is redacted,
though: sk-SK_ 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.
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.
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 });TranslationProvideris the one contract:id,kind(aProviderKind),supportsGlossary, andtranslateBatch(request). Treat every value in a request as untrusted data, read any API key from the environment rather than from the request, and throw aProviderErrorwithout secrets in it when a call fails.ProviderKindis"llm"or"machine-translation". Thekindon your provider object is descriptive only: the SDK decides plural generation and what the sensitive-data guard scans from the kind of the configured providerid, not from this field.TranslateRequestcarriessourceLocale,targetLocale, theentriesto translate, and the optionalglossary(the result ofglossaryForLocalefor the target locale),tone,maxLength, andpluralCategories. It also carriesextractPlaceholdersand, for some formats,comparePlaceholders, which your provider uses for the integrity check, plussignal, which aborts an in-flight call,onRepair, andforeignPlaceholders: per key, the tokens of another placeholder syntax to protect like placeholders.TranslateResultreturnsvaluesandintegritykeyed by entry key, plus optionalusage,notices, andreviewFlags. A key missing fromvaluescounts 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.ProviderRetryListeneris the type ofCreateProviderHooks.onRetry. It receives aProviderRetry,{ attempt, delayMs?, status? }, per retry, and each call becomes aprovider-retryprogress event.ProviderNetworkis whatCreateProviderContext.networkholds when the network policy restricts where a provider may connect: the effectivepolicy, theenvthe endpoint was resolved from, and optional testdeps. 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