SDK reference

Every @verbatra/sdk entry point, grouped by job, with the error model and the RunSummary anatomy.

@verbatra/sdk is the engine the verbatra command runs on. The CLI is a thin wrapper, so whatever the command line does, you can do in code. This page catalogs the whole public surface, grouped by the job each entry point does. For end-to-end examples, see SDK recipes.

Install

npm install --save-dev @verbatra/sdk
# pnpm
pnpm add -D @verbatra/sdk
# yarn
yarn add -D @verbatra/sdk

Requires Node.js >=22.14.0.

The pnpm line installs correctly but exits 1 with ERR_PNPM_IGNORED_BUILDS: pnpm gates the install scripts of the bundled Gemini SDK and its protobufjs, and leaves an unanswered pnpm-workspace.yaml behind that makes every later pnpm command in the project fail the same way. Run pnpm approve-builds once and approve neither entry, or see Troubleshooting for the non-interactive fix. There is no npx shortcut around this the way there is for the CLI, because you are importing a library, not running a binary.

API keys and the environment

The SDK never reads, holds, or accepts an API key. The provider reads its key from the environment (for example ANTHROPIC_API_KEY) when it is constructed. Unlike the CLI, the SDK does not load .env files: in your own script, set the variable yourself, for example with node --env-file=.env script.js.

Every entry point takes one input object whose first field is the validated config (from loadConfig), except where noted. Most also accept an optional second deps argument that injects a registry, provider builder, or file system; you can ignore it in normal use. See The dependency seam for what deps.fs reaches.

Run translations

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.

Input: { config, cwd?, locales?, dryRun?, prune?, generatePlurals?, concurrency?, cache?, onProgress?, onLockWait?, lockAcquireTimeoutMs? }.

  • locales restricts the run to a subset of the configured target locales, exactly as on check and diff. A locale that is not configured throws UNKNOWN_LOCALE before anything is read or spent. Omit it to cover every target locale.
  • dryRun reads, diffs, and reports without constructing or calling the provider and without writing anything.
  • prune removes orphaned keys (target keys absent from source) from the written file and the lock. generatePlurals synthesizes missing CLDR plural forms (i18next-JSON with an LLM provider only). Both default to off; when set, each overrides the matching config option for that run.
  • concurrency runs up to that many target locales at once (default 1, strictly serial). A value below 1 throws CONCURRENCY_INVALID; on a live run a value above 1 with maxTokens configured throws CONCURRENCY_BUDGET_CONFLICT (a dry run is exempt). A whole-run error raised once locales are already running stops any further locale from being started, and the locales in flight finish and release their write locks before translate rejects, so the rejection arrives after the slowest one rather than instantly.
  • cache defaults to true and reuses the local translation-memory cache (verbatra.cache.json, also exported as CACHE_FILE_NAME); set it to false to bypass the cache for the run (the CLI's --no-cache).
  • onProgress is called as the run advances (per locale start and finish, and per provider sub-batch), and onLockWait fires while a locale's write lock is contended. Both are notification callbacks; the SDK writes no output itself. lockAcquireTimeoutMs overrides how long a contended write lock retries before failing with LOCK_CONTENDED.
  • maxBatchSize, maxTokens, and budgetBehavior are config-only; there is no per-run override.

Returns a RunSummary (see its anatomy below). Whole-run problems throw an SdkError; a single locale failing never throws and lands as status: "failed" on that locale's entry while the run continues.

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.

Input: { config, cwd?, locales?, debounceMs?, onRun, concurrency?, cache?, onProgress?, onLockWait?, lockAcquireTimeoutMs? }. debounceMs defaults to 300. onRun is called once per run with a WatchRunResult: { status: "succeeded", summary } or { status: "failed", error: { code, message } }. 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. Three problems are refused at startup, before the watcher exists, so watch itself rejects rather than returning a controller: a concurrency that is not an integer of at least 1 throws CONCURRENCY_INVALID, a concurrency above 1 against a config that sets maxTokens throws CONCURRENCY_BUDGET_CONFLICT (there is no dry-run watch, so the budget conflict always applies), and a missing source file throws SOURCE_UNREADABLE. Every failure after startup is surfaced through onRun and watching continues.

Inspect state without writing

None of these call a provider, write a file, or mutate the lock.

doctor

Available from 0.9.0

This needs verbatra 0.9.0 or newer. Earlier releases do not have it, so check your installed version with verbatra --version and upgrade if it is older.

Validates the project setup and spends nothing: it constructs no provider, makes no network request, and never reads an API key value. Five checks run, each with its own pass, fail, or skipped verdict: the config loads and validates, the configured format resolves to an adapter, the configured provider ID resolves to a factory, the environment variable that provider reads its key from is set, and the source locale file can be read. Every check runs even when an earlier one failed, so one call reports every independent problem. The API key is checked by name only. The source-file check reads and parses the file rather than only probing for it, so a directory in its place, an empty file, and malformed content are all reported here.

Input: { cwd?, configPath? }. No loaded config needed: doctor loads it itself, so a project with no config at all still gets a report rather than a thrown error. Returns a DoctorResult whose ok is true exactly when no check failed. It throws CONFIG_NOT_FOUND only for an explicit configPath that does not exist.

check

Reports per-locale drift as counts: missing (in source, absent from target), stale (source changed since last translated), and upToDate. Use it as a CI gate.

Input: { config, cwd?, locales? }. Returns a CheckSummary whose inSync is true exactly when every checked locale has nothing missing and nothing stale.

diff

The detailed sibling of check: the same computation, but it returns the key lists per locale (missing, changed, orphaned) instead of counts. Orphaned keys are report-only and never flip hasPendingChanges, since a default run does not prune.

Input: { config, cwd?, locales? }. Returns a DiffSummary.

keyIntegrity

Reports, for each locale's changed keys, whether the current target value still matches the source's placeholders and whether it is valid ICU. Each entry carries key, hasPlaceholders, matches, the missing and extra placeholder tokens on a mismatch, and icuValid. Entries never carry a source or target string value. This is the per-key integrity report Studio surfaces.

Input: { config, cwd?, locales?, keys? }. keys narrows the check; only keys that are "changed" for a locale are ever checked. Returns one LocaleKeyIntegrity per checked locale.

lockState

Reports the lock file's existence, version, and per-locale drift (baseline key count plus missing, stale, and up-to-date counts). exists comes from an explicit probe, so "no lock file yet" and "an empty but present lock file" stay distinguishable. When the file is absent the result is { exists: false } and nothing else is read.

Input: { config, cwd?, locales? }. Returns a LockStateResult.

loadLockFile

Reads the lock file itself (verbatra.lock.json, also exported as LOCK_FILE_NAME) and returns its parsed shape: { version, locales }, where each locale maps keys to the source content hash they were last translated from. A missing file degrades to an empty lock, the same first-run behavior translate relies on; use lockState when you need to distinguish absence.

Input: { cwd? }. No config needed. Returns a LockFile.

runStatus

Reads the review-flag and token-usage snapshot the most recent non-dry-run translate or watch run persisted to .verbatra-local/run-status.json. Never throws: a missing, corrupt, or unrecognized file degrades to { available: false }. This file is best-effort telemetry, not a correctness baseline.

Input: { cwd? }. No config needed. Returns { available: false } or { available: true, version, generatedAt, usage?, budget?, locales }.

Single-key operations

These are the seams Verbatra Studio drives; use them to build your own review tooling. All three resolve locale and key fresh on every call and throw UNKNOWN_LOCALE or UNKNOWN_KEY when either does not exist.

keyValue

Reads one key's current source value and, when present, its current target value for one locale. Read-only.

Input: { config, cwd?, locale, key }. Returns { source, target? }; target is absent exactly when the key does not yet exist in that locale.

editEntry

Writes one human-typed correction for one key and locale. The candidate value passes the same integrity gate as a provider translation before anything reaches disk; on acceptance the locale file and the lock entry for that key are updated inside the locale's write lock. Never calls a provider.

Input: { config, cwd?, locale, key, value }. Returns a two-armed result: { accepted: true, value }, or { accepted: false, reason: "placeholder" | "icu" | "degenerate" | "empty", value } with nothing written. "empty" covers an empty or whitespace-only value for a source that has text: an edit cannot express clearing a key, so use the workbook's [[CLEAR]] sentinel for that.

retranslateEntry

Re-runs the provider for exactly one key and locale: a single-entry call through the same provider path translate uses, gated through the same integrity checks. On acceptance it writes the locale file and lock entry for that key only.

Input: { config, cwd?, locale, key }. Returns { accepted: true, value, reviewReasons } (the review reason codes, if any, that apply to the new value) or { accepted: false, reason: "placeholder" | "icu" | "degenerate" | "empty", value }. Unlike translate, a provider failure here throws: a ProviderError from @verbatra/ai-providers with a stable code such as RATE_LIMITED or AUTH_FAILED.

Bulk content reads

Available from 0.10.0

This needs verbatra 0.10.0 or newer. Earlier releases do not have it, so check your installed version with verbatra --version and upgrade if it is older.

localeValues

Reads every key's current source and target text, across every requested target locale, in one pass over the files already on disk. It is the bulk counterpart to keyValue: use it when a caller needs translation content in bulk, for instance to search or scan values rather than key names, since keyValue only answers for one key at a time. Read-only.

Input: { config, cwd?, locales? }; an omitted locales covers every configured target locale. Returns an array of { locale, values }, one entry per locale, where values maps each key to { source?, target? }. An absent target means the key has not been translated in that locale yet; an absent source means the key is orphaned, present in the target locale but no longer in the source.

Locale-file snapshots

The building blocks of a live-refresh watcher such as Studio's: capture a locale file's state, then count what changed since.

readLocaleFileSnapshot

Reads one locale file (the source locale or any target locale) and reduces it to a content hash per key. A file that does not exist yet reads as an empty snapshot rather than throwing.

Input: { config, locale, cwd? }. Returns { locale, hashes }.

diffLocaleSnapshots

Compares two snapshots of the same file, taken at different times, and counts the keys added, changed, and removed between them. A plain synchronous function: diffLocaleSnapshots(previous, current) returns { added, changed, removed }. Counts only, never key names.

Locale paths

createLocalePathResolver

Resolves the project's locale-to-path mapping in both directions, from files.pattern, the configured locales, and files.localeStyle. createLocalePathResolver(cwd, config) returns { pathFor, localeFor }: pathFor(locale) is the absolute path of one locale's file, and localeFor(path) is the locale that owns a path, or undefined for a path this project does not own. Every SDK entry point resolves paths through it, so a watcher or dashboard built on the SDK sees exactly the paths a run writes.

Every check runs when the resolver is created, before any file is read: a pattern and style that cannot be combined, or a locale the style has no correct spelling for, throws LOCALE_LAYOUT_INVALID, and two locales resolving to one path throws LOCALE_PATH_COLLISION.

The workbook pair

The translator handoff, as an Excel workbook or as delimited text; see Manual translation.

exportWorkbook

Writes the strings that still need translating (missing and changed keys per locale; add unchanged ones with includeUnchanged) into a styled .xlsx workbook. Rows carry the same review-flag signal a translate run computes. No provider call, no lock write.

Input: { config, cwd?, out?, locales?, includeUnchanged?, format? }. format is xlsx by default; csv and tsv write one <locale>.csv or <locale>.tsv per locale instead, so out names a directory for them (created if missing) and a file path for xlsx. out defaults to DEFAULT_WORKBOOK_PATH (verbatra-translations.xlsx) or DEFAULT_DELIMITED_PATH (verbatra-translations), both exported as constants. The accepted values are exported as EXCHANGE_FORMATS and the default as DEFAULT_EXCHANGE_FORMAT, so a tool that wraps the SDK can validate a format argument without hardcoding the list. Returns { path, locales }: the absolute path written and a per-locale row count.

importWorkbook

Reads a filled workbook back into the locale files, running the same source-drift and integrity checks as translate. Only accepted rows advance their lock baseline; a blank or rejected row keeps re-exporting until it is genuinely resolved.

Input: { config, workbook, cwd?, dryRun?, format? }. With csv or tsv, workbook is either one interchange file or the directory holding one per locale, and the locale comes from the file name. Returns the same RunSummary shape as translate (with needsReview always empty, since no provider is involved). A sheet for a locale that is not a configured target fails that locale with CONFIG_INVALID as data on the summary, not a throw.

Config

loadConfig

Finds, loads, and validates the project config, returning a VerbatraConfig. Options: { cwd?, configPath?, configOverride? }, with precedence configOverride (validate an in-memory object) over configPath (load one explicit file) over search. The search starts at cwd and covers verbatra.config.ts (also .js/.cjs), the .verbatrarc family (.json, .yaml, .yml, .js, .cjs, .ts), and a "verbatra" property in package.json. A glossary given as a file path is read and validated here, so downstream code always sees a plain record.

Throws CONFIG_NOT_FOUND when nothing is found, CONFIG_INVALID when a config is found but invalid. See The config file for the schema.

loadConfigWithMeta

The same load, plus provenance: returns { config, source, glossary }, where source says whether the config came from a search hit, an explicit path, or an in-memory override (with the absolute filepath when there is one), and glossary records whether the glossary was absent, inline, or resolved from a file. Use it when you need to display where the config came from.

readGlossaryFile

Available from 0.9.0

This needs verbatra 0.9.0 or newer. Earlier releases do not have it, so check your installed version with verbatra --version and upgrade if it is older.

Reads a file-backed glossary fresh from disk and returns it as a flat term map. Takes { glossary }, the GlossaryProvenance from loadConfigWithMeta, so the file it reads is always the one the config names; there is no path argument. Use it in a long-running tool that has to show the glossary as it is now rather than as it was when the config was loaded.

Throws GLOSSARY_NOT_FILE_BACKED when the glossary is inline or absent, and CONFIG_INVALID when the file is missing, oversized, not UTF-8, not valid JSON, or not a flat string map.

updateGlossaryTerm

Available from 0.9.0

This needs verbatra 0.9.0 or newer. Earlier releases do not have it, so check your installed version with verbatra --version and upgrade if it is older.

Adds, replaces, or removes exactly one term in a file-backed glossary and returns the glossary as it now stands. Takes { glossary, cwd?, term, translation }, where translation is the new text or null to remove the term. The rest of the file keeps its key order and its indentation, the write is atomic, and the whole read-modify-write is held under a project-wide glossary lock, so two concurrent edits are serialized rather than interleaved.

An inline glossary is refused with GLOSSARY_NOT_FILE_BACKED rather than converted: it lives inside an executable config module. A blank term or translation, and an edit whose result would exceed the 1 MiB glossary limit, are refused with CONFIG_INVALID; a failed write is GLOSSARY_UNWRITABLE.

defineConfig

An identity helper for authoring a typed verbatra.config.ts: it returns its argument unchanged, and exists purely for type inference and editor autocomplete, including completion of the selected provider's known model IDs. The model restriction is authoring-only; at runtime any non-empty string passes validation.

verbatraConfigSchema

The zod schema loadConfig validates with, exported so your own tooling can validate a config object the same way the SDK does. Unknown top-level keys are rejected, so a stray secret cannot hide in the config. The one exception is $schema, an optional string accepted so a JSON or YAML config can point an editor at the JSON Schema document the package ships as @verbatra/sdk/config-schema.json. It is ignored at runtime.

scaffoldingMetadata

Read-only metadata the CLI init command derives its prompts from: providerEnv (provider id to the environment variable its key is read from), scaffoldModels (a default scaffold model per LLM provider), providerTokenLimitKeys (the option key each LLM provider takes its output token limit under), and supportedFormats (the closed set of format ids). Read the token-limit key from providerTokenLimitKeys rather than assuming one: Anthropic calls it maxTokens and the others maxOutputTokens, the schema validates each provider's options strictly, and DeepL has no entry because it takes no token limit. The ScaffoldableProviderId type covers the four hosted providers; openai-compatible is excluded because it has no single required environment variable.

Secret redaction

Available from 0.10.0

This needs verbatra 0.10.0 or newer. Earlier releases do not have it, so check your installed version with verbatra --version and upgrade if it is older.

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 dependency seam

Available from 0.9.0

This needs verbatra 0.9.0 or newer. Earlier releases do not have it, so check your installed version with verbatra --version and upgrade if it is older.

deps.fs replaces the file-system port the SDK's own I/O travels through, typed as SdkFs. The seam is complete: the run-status file, the lock file, the config glossary, workbook and interchange I/O, and the locale files themselves all go through it, because the format adapters read and write through a port built from the same object. A whole run can therefore be held in memory, which is how the SDK's own tests avoid touching disk and how an embedding application can back part of a project with something other than a local disk.

import { translate, type SdkFs } from "@verbatra/sdk";

const summary = await translate({ config }, { fs: inMemoryFs satisfies SdkFs });

Two contract points an implementation has to honor. Reads are size-bounded, so readFileBounded and readBytesBounded take a byte limit and report missing or too-large as a state rather than throwing, which is what keeps a hostile or accidentally huge file from exhausting memory. Writes are expected to be atomic, so a crash mid-write never leaves a half-written file behind, and createExclusive must be atomic against other processes because it is the primitive behind the per-locale write lock. Directory creation is the caller's job.

The one thing deps.fs cannot reach is a caller-supplied deps.adapterRegistry. Those adapters were constructed by the caller, so their file access is whatever the caller wired into them. Supplying both means the caller owns that wiring.

The RunSummary anatomy

translate, importWorkbook, and each successful watch run resolve to a RunSummary:

interface RunSummary {
  dryRun: boolean;          // true when nothing was written and no provider was called
  locales: LocaleSummary[]; // one entry per target locale, in config order
  succeeded: string[];      // locales whose run succeeded
  partial: string[];        // locales written with keys still missing; the CLI exits 1 on these
  failed: string[];         // locales whose run failed
  usage?: UsageSummary;     // summed input/output tokens; absent when no call reported usage
  budget?: RunBudget;       // present only when maxTokens is configured
}

interface LocaleSummary {
  locale: string;
  status: "succeeded" | "partial" | "failed";
  translated: string[];          // keys translated this run (in dry-run, keys that would be)
  cacheHits: string[];           // keys served from the translation-memory cache this run
  unchanged: string[];           // keys already up to date
  orphaned: string[];            // target keys with no matching source key (always reported)
  pruned: string[];              // orphaned keys removed this run; empty unless pruning is on
  invalidIcuSource: string[];    // source keys skipped for invalid ICU
  integrityMismatches: string[]; // translations withheld for a placeholder mismatch
  providerFailures: string[];    // keys withheld because nothing was translated for them
  budgetWithheld: string[];      // keys never sent because a "stop" budget already tripped
  generated: string[];           // CLDR plural forms synthesized this run
  unfilled: string[];            // import only: blank rows whose key still needs a translation
  malformedRows: { row: number; line?: number; column: string }[]; // import only: rows the reader could not parse
  duplicateKeys: { key: string; row: number; line?: number }[];    // import only: later rows for a duplicated key
  notices: LocaleNotice[];       // provider notices and SDK notices for this locale
  needsReview: { key: string; reasons: string[] }[]; // accepted keys flagged for a second look
  usage?: UsageSummary;          // this locale's summed tokens; absent if nothing reported usage
  error?: { code: string; message: string }; // present only when status is "failed"
}

The parts worth knowing:

  • needsReview lists accepted, written keys the review heuristics flagged, each with its reason codes: LENGTH_RATIO_OUTLIER, EQUALS_SOURCE, GLOSSARY_TERM_MISSED, INTEGRITY_REORDERED, and PROVIDER_DEGRADED. A review flag is advisory and never withholds a key, so a key never appears in both needsReview and integrityMismatches. See Translation safety for what each code means and Review in Studio for working the queue.
  • notices live per locale, never at the top level. They cover provider notices (say, a DeepL degradation) and SDK notices with the codes PLURAL_CATEGORIES_INCOMPLETE, SUB_BATCH_FAILED, BLANK_ROW_BASELINE_RETAINED, BUDGET_TOKENS_EXCEEDED, and CACHE_VERSION_UNRECOGNIZED. The cache notice is run-wide (one file, shared by every locale), so it is attached to each locale in the run.
  • usage is undefined, never a fabricated zero, whenever nothing in that scope reported usage: a dry run never calls a provider, and DeepL never reports tokens. RunSummary.usage is the sum over the locales.
  • budget appears only when maxTokens is configured: { maxTokens, behavior, supported, tokensUsed, exceeded }. Against a token-less provider or a dry run it is still present with supported: false, so the guardrail is visibly inert rather than falsely tripped.
  • error.code on a failed locale is a preserved string (the underlying provider or adapter code, with LOCALE_FAILED only as a fallback), so do not treat it as a closed set.
  • cacheHits lists keys served from the translation-memory cache rather than the provider. unfilled, malformedRows, and duplicateKeys are populated only by importWorkbook (a translate run leaves them empty): a blank row whose key still needs a translation at import time (whether it was exported as new or as changed), a workbook row the reader could not parse, and a duplicated key whose first occurrence won. The optional line on the last two is the file line the record starts on, present only for a delimited (csv or tsv) import.

The error model

Whole-run failures throw an SdkError: one class, a stable code, and a secret-free message. Branch on the code, not the message. Per-locale failures, provider notices, and integrity findings are surfaced as data on the RunSummary, never thrown.

CodeWhen
CONFIG_NOT_FOUNDno config was found by search, or an explicit configPath does not exist (thrown by loadConfig)
CONFIG_INVALIDa config was found but is unparseable or fails validation, or its glossary file could not be resolved
UNKNOWN_FORMATno adapter is registered for the configured format; thrown before any file is read
UNKNOWN_LOCALEa requested locale is not among the configured target locales
UNKNOWN_KEYa requested key is not in the source resource (keyValue, editEntry, retranslateEntry)
PROVIDER_CONSTRUCTION_FAILEDthe provider could not be built; wraps the provider's own error, including a missing API key
SOURCE_UNREADABLEthe source locale file does not exist
SOURCE_INVALIDthe source locale file could not be read or parsed; wraps the adapter's read error
LOCK_FILE_INVALIDthe lock file is present but corrupt, oversized, or at an unsupported version
LOCK_CONTENDEDa locale's write lock could not be acquired before its timeout; the message names the lock file's path
GLOSSARY_NOT_FILE_BACKEDthe config's glossary is inline or absent, so there is no glossary file to read or rewrite (readGlossaryFile, updateGlossaryTerm)
GLOSSARY_UNWRITABLEthe glossary file could not be written (updateGlossaryTerm)
LOCALE_LAYOUT_INVALIDfiles.pattern and files.localeStyle cannot be combined, or the style has no valid path spelling for a configured locale
LOCALE_PATH_COLLISIONtwo configured locales resolve to the same absolute file path
CONCURRENCY_INVALIDconcurrency was set but is not an integer of at least 1; raised before any locale runs
CONCURRENCY_BUDGET_CONFLICTa live run set concurrency above 1 while maxTokens is configured; raised before any provider call (a dry run is exempt)
TARGET_UNWRITABLEa target locale file could not be written (its directory is not writable, does not exist, is read-only, or is out of space); the message names the target file and the file-system code, never the internal temporary file the atomic write uses
LOCALE_FAILEDnever thrown: the fallback code recorded on a failed locale's error

Two exceptions to the one-error-class rule: retranslateEntry re-throws the provider's own ProviderError when the single call fails, and runStatus never throws at all. For how these codes map to CLI exit codes, see CI and exit codes; for symptom-first help, see Troubleshooting.

Edit on GitHub