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

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

Requires Node.js >=22.14.0.

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 for tests; you can ignore it in normal use.

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 placeholder and ICU integrity checks, write the locale files, and update the lock. Use it for scripts, build steps, and CI jobs.

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

  • 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).
  • cache defaults to true and reuses the local translation-memory cache; 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?, 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 (a budgeted watch with concurrency above 1 fails each run with CONCURRENCY_BUDGET_CONFLICT).

Returns a WatchController with one method, stop(), which closes the watcher and awaits the in-flight run. A missing source file throws SOURCE_UNREADABLE at startup; every failure after that is surfaced through onRun and watching continues.

Inspect state without writing

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

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 placeholder and ICU 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", value } with nothing written.

retranslateEntry

Re-runs the provider for exactly one key and locale: a single-entry call through the same provider registry 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", 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.

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.

The workbook pair

The Excel handoff for human translators; 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? }. out defaults to DEFAULT_WORKBOOK_PATH (verbatra-translations.xlsx), exported as a constant. 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, placeholder, and ICU 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? }. 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.

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.

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), and supportedFormats (the closed set of format ids). The ScaffoldableProviderId type covers the four hosted providers; openai-compatible is excluded because it has no single required environment variable.

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
  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" | "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: changed rows the translator left blank
  malformedRows: { row: number; column: string }[];  // import only: rows the reader could not parse
  duplicateKeys: { key: string; row: 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, and BUDGET_TOKENS_EXCEEDED.
  • 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 changed row, a workbook row the reader could not parse, and a duplicated key whose first occurrence won.

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
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)
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