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/sdkRequires 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? }.
localesrestricts the run to a subset of the configured target locales, exactly as oncheckanddiff. A locale that is not configured throwsUNKNOWN_LOCALEbefore anything is read or spent. Omit it to cover every target locale.dryRunreads, diffs, and reports without constructing or calling the provider and without writing anything.pruneremoves orphaned keys (target keys absent from source) from the written file and the lock.generatePluralssynthesizes 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.concurrencyruns up to that many target locales at once (default 1, strictly serial). A value below 1 throwsCONCURRENCY_INVALID; on a live run a value above 1 withmaxTokensconfigured throwsCONCURRENCY_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 beforetranslaterejects, so the rejection arrives after the slowest one rather than instantly.cachedefaults to true and reuses the local translation-memory cache (verbatra.cache.json, also exported asCACHE_FILE_NAME); set it to false to bypass the cache for the run (the CLI's--no-cache).onProgressis called as the run advances (per locale start and finish, and per provider sub-batch), andonLockWaitfires while a locale's write lock is contended. Both are notification callbacks; the SDK writes no output itself.lockAcquireTimeoutMsoverrides how long a contended write lock retries before failing withLOCK_CONTENDED.maxBatchSize,maxTokens, andbudgetBehaviorare 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
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
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
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
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
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
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, andPROVIDER_DEGRADED. A review flag is advisory and never withholds a key, so a key never appears in bothneedsReviewandintegrityMismatches. 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, andCACHE_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.usageis the sum over the locales. - budget appears only when
maxTokensis configured:{ maxTokens, behavior, supported, tokensUsed, exceeded }. Against a token-less provider or a dry run it is still present withsupported: 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_FAILEDonly 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(atranslaterun leaves them empty): a blank row whose key still needs a translation at import time (whether it was exported asnewor aschanged), a workbook row the reader could not parse, and a duplicated key whose first occurrence won. The optionallineon the last two is the file line the record starts on, present only for a delimited (csvortsv) 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.
| Code | When |
|---|---|
CONFIG_NOT_FOUND | no config was found by search, or an explicit configPath does not exist (thrown by loadConfig) |
CONFIG_INVALID | a config was found but is unparseable or fails validation, or its glossary file could not be resolved |
UNKNOWN_FORMAT | no adapter is registered for the configured format; thrown before any file is read |
UNKNOWN_LOCALE | a requested locale is not among the configured target locales |
UNKNOWN_KEY | a requested key is not in the source resource (keyValue, editEntry, retranslateEntry) |
PROVIDER_CONSTRUCTION_FAILED | the provider could not be built; wraps the provider's own error, including a missing API key |
SOURCE_UNREADABLE | the source locale file does not exist |
SOURCE_INVALID | the source locale file could not be read or parsed; wraps the adapter's read error |
LOCK_FILE_INVALID | the lock file is present but corrupt, oversized, or at an unsupported version |
LOCK_CONTENDED | a locale's write lock could not be acquired before its timeout; the message names the lock file's path |
GLOSSARY_NOT_FILE_BACKED | the config's glossary is inline or absent, so there is no glossary file to read or rewrite (readGlossaryFile, updateGlossaryTerm) |
GLOSSARY_UNWRITABLE | the glossary file could not be written (updateGlossaryTerm) |
LOCALE_LAYOUT_INVALID | files.pattern and files.localeStyle cannot be combined, or the style has no valid path spelling for a configured locale |
LOCALE_PATH_COLLISION | two configured locales resolve to the same absolute file path |
CONCURRENCY_INVALID | concurrency was set but is not an integer of at least 1; raised before any locale runs |
CONCURRENCY_BUDGET_CONFLICT | a live run set concurrency above 1 while maxTokens is configured; raised before any provider call (a dry run is exempt) |
TARGET_UNWRITABLE | a 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_FAILED | never 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.