Reference / SDK
SDK reference
Install @verbatra/sdk and find the reference page for each entry point. Files that do not live on disk reach the SDK through your own file system.
@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.
Start here
New to the SDK? Begin with one of these:
SDK quickstart
SDK recipes
Reference pages
These pages catalog the whole public surface, grouped by the job each entry point does.
Run translations
translate, Progress events, resolveDryRun, watch, releaseHeldLocks, pseudolocalize, generateTypesInspect a project
doctor, dataFlow, dataFlowManifestSchema, extract, check, checkFile, diff, keyIntegrity, localeIntegrity, lockState, loadLockFile, loadProvenance, runStatus, reviewQueue, provenanceReport, provenanceReportPage, previewIcuMessageEdit
editEntry, previewValueReplace, applyValueReplaceReview and retranslate
keyValue, keyContext, localeHistory, localeGitStatus, localeGitDiff, retranslateEntry, approveEntry, rejectEntry, createValueMarker, localeValues, localeValuesPage, approveEntries, rejectEntries, approveLocale, retranslateEntries, BatchInterruptedErrorKey history
keyHistory, runHistoryHandoff and exchange
readLocaleFileSnapshot, diffLocaleSnapshots, createLocalePathResolver, localeWatchPaths, exportWorkbook, importWorkbook, importTmx, tmxErrorLocation, exportTmxConfig and glossary
loadConfig, loadConfigWithMeta, configCandidatePaths, resolveProjectRoot, readGlossaryFile, updateGlossaryTerm, glossaryForLocale, glossaryHits, glossaryDraftCheck, normalizeGlossary, sharedGlossaryTranslations, redactGlossary, readCurrentGlossary, editConfiguredGlossaryTerm, setKeyLengthLimit, defineConfig, verbatraConfigSchema, scaffoldingMetadata, detectProject, isMachineTranslationEnabled, assertMachineTranslationEnabled, assertTargetLocaleAdapters, providers, redaction
createDefaultRegistry, AdapterRegistry, createFlatFileAdapter, createTreeFileAdapter, nodeAdapterFs, isCustomFormatId, redact, declareProviderKeyEnvVarRunSummary
budgetStandingJSON Schemas
runSummarySchema, checkSummarySchema, checkFileSummarySchema, diffSummarySchema, doctorResultSchema, provenanceReportResultSchema, pseudolocalizeResultSchema, generateTypesResultSchema, extractResultSchema, exportWorkbookResultSchema, importTmxResultSchema, exportTmxResultSchema, runHistoryResultSchema, localeSummarySchema, keyProvenanceSchema, provenanceSummarySchema, keyOriginSchema, progressEventSchema, lockWaitEventSchema, renderJsonSchemas, renderOutputJsonSchema, jsonSchemaUrlErrors
SdkError, ProviderError, AdapterError, projectRelativeMessage, errorHintInstall
npm install --save-dev @verbatra/sdkRequires Node.js ^22.22.0 || >=24.
With pnpm, pnpm add --save-dev @verbatra/sdk installs correctly but exits 1 with ERR_: 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_) 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.
The dependency seam
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 glossary file tools, 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.
Prop
Type
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. Before it writes a locale file, the run-status file, or an output file such
as a workbook, the SDK calls the optional mkdir with the parent directory, so writeFile and
writeBytes need not create one; leave mkdir out when the store has no directories.
createExclusive gets no such call and creates a missing parent itself. The other required
members are fileExists and deleteFile, which succeeds when the file is already gone.
readDirectory is optional, so an implementation written before source extraction existed keeps
compiling. extract is the only entry point that needs it, and reports its absence as
EXTRACT_. It lists one directory's immediate entries as { name, kind }, where
kind is "file", "directory", or "other". A symbolic link belongs in "other": the scan
never follows one, which is what keeps it inside the roots it was given.
realpath is optional too. It resolves every symbolic link in an existing path, and the output
guards of generateTypes, exportTmx,
exportWorkbook, and pseudolocalize use it to see where an
output path really lands, so a link cannot carry a written file outside cwd or onto a file the
project depends on. Without it, those guards compare paths as written. A link at the output path
whose target does not exist cannot be resolved, so the guards assume writeFile and writeBytes
replace a link there rather than writing through it, as the default atomic write does.
rename is optional as well.
It moves a file to a new name in the same directory in one atomic step, replacing any file already
there, and rejects with an error whose code is ENOENT when the source is missing. The write lock
uses it to remove a lock or reclaim guard left by a process that is gone, and to release its own
lock: the file is moved aside under a unique <name>.<pid>.<tag>.<uuid>.stale name and deleted only
when it is still the record the lock expected. Anything else, such as a lock another process took in
the meantime, is put back with createExclusive. A rename that fails with EPERM, EBUSY, or
EACCES, as Windows reports for a file another process has open, counts as "not reclaimed this
poll", and the lock keeps polling until its timeout. When the rename that releases a lock fails for
any reason, the release falls back to reading the lock file and deleting it while it still holds
the holder's token, and retries an EPERM, EBUSY, or EACCES failure a few times, a poll apart.
A record that should have been put back but cannot be, because a third process took the path in
the meantime, is deleted, since its holder's token check fails from then on. Without rename, the lock reads the file and
deletes it when unchanged, which leaves a wider window in which a lock another process just took is
deleted.
Moving a file aside and putting it back is two steps, so in rare interleavings two processes can
briefly both believe they hold the same lock. Every lock therefore records a random ownership
token, and before each write the lock protects (the locale file, the lock file, the provenance
file, the glossary) the holder checks that the lock file still carries its token. When it does not,
the operation stops with LOCK_ and writes nothing. A lock file found missing or holding
another token on release is reported the same way when the operation succeeded, since the other
process may have changed the same files since; an operation that failed throws its own error. The
check and the write are still two steps, and nothing bounds the time between them: a holder paused
right after the check (a sleeping machine, a stopped process, a debugger, a clock step, or a long
block of its event loop) stops its heartbeat, so once its lock is three intervals old (30 seconds
by default) another process on the same machine takes it over, and the paused holder can still
write once when it resumes. The token check relies on createExclusive creating a file that
readFileBounded reads back, so a custom implementation must keep that promise.
touch and mtimeMs are optional too.
touch(path) sets an existing file's modification time to now, and mtimeMs(path) returns it in
milliseconds, or undefined when the file is missing. With touch, a lock holder records a
heartbeat interval of 10 seconds in its lock and refreshes the file's modification time at that
interval while it holds the lock. It touches the lock file right after creating it too, so the
file's age is measured by this process's clock rather than the file server's. With mtimeMs, a waiting process treats a lock on its own
machine whose recorded process is still running but whose file has not been refreshed for three of
the recorded intervals (30 seconds by default) as abandoned. That covers a lock record that outlived
its hold, for example one put back for a long-lived Studio, watch, or MCP server process that had
already released it. A reclaim guard records no heartbeat: it is cleared once its process is
provably gone and it has been seen unchanged across two polls. Without these members, a lock is judged abandoned
only when its recorded process is provably gone.
A process that crashes while it has a lock moved aside can leave a .stale file in
.verbatra-local/locks/. Such files are safe to delete by hand, and the next lock acquisition
deletes them once the process that moved them is gone or, when touch is implemented, once they
are older than the stale threshold, counted from the move. A file system without touch keeps
them until their mover is gone.
deps.fs cannot reach 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.
Config loading sits outside the seam as well. loadConfig and loadConfigWithMeta take no deps:
they read the config file itself from the real disk, and a glossary file the config names through
their own fs option, so the deps.fs a later flow receives reaches neither read. To keep a whole
project in memory, pass the same SdkFs as loadConfig({ fs }), and hand the config in as
configOverride rather than as a file. The flows resolve paths against their own cwd, so pass it
there too.
The example keeps a whole project in memory with only the required members. Under the provider
none the run needs no key and leaves greeting for a person to translate.
import { check, loadConfig, type SdkFs, translate } from "@verbatra/sdk";
const files = new Map<string, Uint8Array>();
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function bounded(path: string, maxBytes: number) {
const bytes = files.get(path);
if (bytes === undefined) return { kind: "missing" } as const;
if (bytes.byteLength > maxBytes) return { kind: "too-large" } as const;
return { kind: "ok", bytes } as const;
}
const memoryFs: SdkFs = {
fileExists: async (path) => files.has(path),
readFileBounded: async (path, maxBytes) => {
const read = bounded(path, maxBytes);
return read.kind === "ok" ? { kind: "ok", content: decoder.decode(read.bytes) } : read;
},
readBytesBounded: async (path, maxBytes) => bounded(path, maxBytes),
writeFile: async (path, data) => {
files.set(path, encoder.encode(data));
},
writeBytes: async (path, data) => {
files.set(path, data);
},
createExclusive: async (path, data) => {
if (files.has(path)) return false;
files.set(path, encoder.encode(data));
return true;
},
deleteFile: async (path) => {
files.delete(path);
},
};
files.set("/project/locales/en.json", encoder.encode('{ "greeting": "Hello, {{name}}!" }'));
const config = await loadConfig({
cwd: "/project",
fs: memoryFs,
configOverride: {
sourceLocale: "en",
targetLocales: ["de"],
format: "i18next-json",
files: { pattern: "locales/{locale}.json" },
provider: { id: "none" },
},
});
const summary = await translate({ config, cwd: "/project" }, { fs: memoryFs });
const status = await check({ config, cwd: "/project" }, { fs: memoryFs });
console.log(summary.locales[0]?.unfilled, status.inSync); // [ 'greeting' ] false
console.log([...files.keys()]); // the source, verbatra.lock.json, .verbatra-local/run-status.json