@verbatra/sdk 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,15 +9,15 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="https://www.npmjs.com/package/@verbatra/sdk"><img src="https://img.shields.io/npm/v/@verbatra/sdk?label=%40verbatra%2Fsdk" alt="@verbatra/sdk npm version" /></a>
13
- <a href="https://github.com/verbatra/verbatra/actions/workflows/ci.yml"><img src="https://github.com/verbatra/verbatra/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI" /></a>
14
- <a href="https://codecov.io/gh/verbatra/verbatra"><img src="https://codecov.io/gh/verbatra/verbatra/graph/badge.svg" alt="Coverage" /></a>
15
- <a href="https://github.com/verbatra/verbatra/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT" /></a>
12
+ <a href="https://www.npmjs.com/package/@verbatra/sdk"><img src="https://img.shields.io/npm/v/%40verbatra%2Fsdk?label=%40verbatra%2Fsdk&amp;color=7b1fa2&amp;labelColor=0b0b12" alt="@verbatra/sdk npm version" /></a>
13
+ <a href="https://www.npmjs.com/package/@verbatra/sdk"><img src="https://img.shields.io/npm/types/%40verbatra%2Fsdk?color=7b1fa2&amp;labelColor=0b0b12" alt="Ships TypeScript types" /></a>
14
+ <a href="https://github.com/verbatra/verbatra/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/verbatra/verbatra/ci.yml?branch=main&amp;label=CI&amp;labelColor=0b0b12" alt="CI status on main" /></a>
15
+ <a href="https://github.com/verbatra/verbatra/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?color=7b1fa2&amp;labelColor=0b0b12" alt="License: MIT" /></a>
16
16
  </p>
17
17
 
18
18
  ## Description
19
19
 
20
- `@verbatra/sdk` is the engine behind verbatra: load and validate a config, run the one-shot translate flow over every target locale, watch the source and re-translate on each change, check or diff your locales without writing, validate the whole project setup before you spend anything, or export and import a translator handoff for manual translation. The [`@verbatra/cli`](https://github.com/verbatra/verbatra/tree/main/packages/cli) command is a thin wrapper over this package.
20
+ `@verbatra/sdk` is the engine behind verbatra: load and validate a config, translate every target locale once, watch the source and re-translate on each change, check or diff your locales without writing, validate the whole project setup before you spend anything, extract keys from your application source, or hand strings off to a human translator and read them back. Each run diffs your source against the lock file and sends only what changed, and every candidate value, whatever produced it, passes one integrity gate before it is written. [`@verbatra/cli`](https://www.npmjs.com/package/@verbatra/cli) is a thin wrapper over this package.
21
21
 
22
22
  ## Requirements
23
23
 
@@ -73,153 +73,97 @@ export default defineConfig({
73
73
  });
74
74
  ```
75
75
 
76
- `files.pattern` must contain the `{locale}` token, `targetLocales` must not include `sourceLocale`, and `targetLocales` must not list the same locale twice, compared case-insensitively (two such entries would collide as one Excel worksheet on export); all three are enforced when the config is validated. The supported `format` values are `i18next-json`, `vue-i18n-json`, `next-intl-json`, `ngx-translate-json`, `xliff`, `yaml`, `arb`, `properties`, `apple-strings`, `apple-xcstrings`, `android-xml`, and `gettext-po`. JSON-family, YAML, and ARB files round-trip in exact document key order: integer-like keys keep their position, new keys append in source-document order, and a YAML composite key (a map or sequence used as a mapping key) fails with a structured error; a `.properties` write preserves the destination file's existing line endings. The optional `glossary` (a term map, inline or a path to a JSON file of the same shape) and `tone` (`"formal"`, `"informal"`, or `"neutral"`) refine the output. The optional `prune` boolean (off by default) opts in to removing orphaned keys (present in a target file but absent from the source) from the written target files and the lock; the `translate --prune` flag overrides it per run. The optional `generatePlurals` boolean (off by default) opts in to synthesizing the CLDR plural forms a richer target language requires but the source lacks (i18next-JSON projects translated by an LLM provider only; DeepL, non-i18next formats, and unknown languages fall back to the per-locale plural warning and never fail); a per-run `generatePlurals` override on `translate` takes precedence, and generated keys are reported separately from translated keys on the summary. The optional `maxBatchSize` (a positive integer, 50 when absent) caps how many entries go into a single provider request, so a large locale is split into sequential sub-batches and one oversized request cannot sink the whole locale. The optional `maxTokens` sets a whole-run ceiling on input plus output tokens across every provider call, and `budgetBehavior` decides what happens once it is reached: `"warn"` (the default) flags it and lets the run continue, `"stop"` withholds every not-yet-attempted key for the rest of the run so it retries next time. Both are config-only, have no CLI flag, and never change an exit code; against a token-less provider such as DeepL, which reports no usage, the budget stays inert rather than tripping falsely.
76
+ `files.pattern` must contain the `{locale}` token, and `targetLocales` must neither include `sourceLocale` nor list one locale twice. Beyond the required keys, the config carries optional `glossary` and `tone`, opt-in `prune` and `generatePlurals`, per-key `maxLength` review budgets, and the `maxTokens`/`budgetBehavior` run budget. Every key, every default, and every provider's option shape is documented on the [Configuration page](https://verbatra.kreitz-webdev.de/docs/config-file); the providers and their key variables are on the [Providers page](https://verbatra.kreitz-webdev.de/docs/providers).
77
77
 
78
- Anthropic takes `{ model, maxTokens }`; OpenAI and Gemini take `{ model, maxOutputTokens }`; `openai-compatible` takes the same pair plus a `baseUrl` (for a local or self-hosted server such as LM Studio, Ollama, or vLLM) and an optional `apiKeyEnvVar`; DeepL takes `{}` (with an optional `glossaryId`); Google Cloud Translation (Basic, v2) also takes `{}`, with no glossary or model. Every provider additionally accepts an optional `requestTimeoutMs`, a positive-integer per-request timeout in milliseconds that bounds each outbound call. API keys are never part of the config. Each provider reads its own environment variable (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPL_API_KEY`, `GOOGLE_TRANSLATE_API_KEY`; `openai-compatible` resolves its key from `apiKeyEnvVar`, then `OPENAI_COMPATIBLE_API_KEY`, then falls back to a keyless placeholder).
78
+ API keys are never part of the config. Each provider reads its own environment variable, and the SDK never accepts one as an argument.
79
79
 
80
80
  ## API reference
81
81
 
82
- ### `defineConfig(config)`
82
+ Every flow takes an `input` object and an optional `deps` object for injecting the file system, the provider factory, or an adapter registry of your own; the two config helpers below take neither. The signatures below are the ones the package publishes in `dist/index.d.ts`.
83
83
 
84
- Returns the config unchanged. It exists purely for type inference and editor autocomplete when authoring a code-defined config. For Anthropic, OpenAI, and Gemini the `model` field is restricted to that provider's known model IDs (sourced from its own SDK), so a model from another provider is a type error at authoring time; the runtime still validates `model` only as a non-empty string, so an unlisted model runs even though the editor flags it. DeepL has no `model` field, and `openai-compatible`'s model is whatever the local server exposes, so neither is restricted.
84
+ ### `defineConfig(config): VerbatraConfigInput`
85
85
 
86
- ### `loadConfig(options?): Promise<VerbatraConfig>`
87
-
88
- Discovers and validates the configuration. With no arguments it searches upward from the current working directory; `options` accepts `cwd`, an explicit `configPath`, an in-memory `configOverride`, and an `fs` seam. Precedence is `configOverride`, then `configPath`, then the search. Resolves to the validated `VerbatraConfig`, and throws an `SdkError` if no config is found (`CONFIG_NOT_FOUND`) or it fails validation (`CONFIG_INVALID`). A `glossary` given as a file path is read and validated here, so every downstream call receives a plain term map.
89
-
90
- ### `translate(input): Promise<RunSummary>`
91
-
92
- Runs the one-shot read, diff, translate, write flow over every target locale. `input` is a `TranslateInput`: `{ config, cwd?, locales?, dryRun?, prune?, generatePlurals?, cache?, concurrency?, lockAcquireTimeoutMs?, onProgress?, onLockWait? }`. With `dryRun: true` it reads, diffs, and reports without calling the provider or writing anything. `locales` narrows the run to a subset of the configured target locales, exactly as it does on `check` and `diff`, and a locale that is not configured raises `UNKNOWN_LOCALE` before anything is read or spent. `prune` and `generatePlurals` each override the matching config option for this run.
93
-
94
- `cache` (on by default, and ignored on a dry run) toggles the local content-addressed translation-memory cache in `verbatra.cache.json`: a key whose source content is unchanged, including under a renamed key or shared with another key, is served from the cache instead of being re-sent to the provider. A cached value is re-checked against that key's own current source first, and one that fails the gate falls through to the provider rather than being written. `concurrency` (defaults to 1, strictly serial) is how many target locales may run at once; it must be an integer of at least 1, and on a live run a value above 1 is refused when the config sets `maxTokens`, because concurrent locales would overshoot the budget nondeterministically. `lockAcquireTimeoutMs` overrides how long a locale's write lock keeps retrying before it fails. `onProgress` receives a structured event once per locale before and after it runs, once per provider sub-batch, and once when the locale loop ends; `onLockWait` fires while a locale's write lock is blocked on another process holding it. The SDK writes to no stream, so these callbacks are the only progress signal.
95
-
96
- Resolves to a `RunSummary`: `dryRun`, `locales`, `succeeded`, `partial`, `failed`, plus `usage` when any provider call reported tokens and `budget` when `maxTokens` is configured. A locale's `status` is `"succeeded"` when nothing was withheld (a no-op with no candidate keys included), `"partial"` when it accepted at least one key and withheld at least one, and `"failed"` when it withheld keys and accepted none, or threw. Each `LocaleSummary` carries `locale`, `status`, `translated`, `unchanged`, `orphaned`, `pruned`, `invalidIcuSource`, `cacheHits`, `integrityMismatches`, `providerFailures`, `budgetWithheld`, `generated`, `notices`, `needsReview`, `unfilled`, `malformedRows`, `duplicateKeys`, an optional `usage`, and an optional `error` on a locale that threw. `cacheHits` are keys served from the translation-memory cache rather than the provider. `integrityMismatches` is a translation that came back and was withheld by verbatra's integrity gate, whose rejection reasons are the exported `IntegrityGateReason` union. `providerFailures` is a key withheld because nothing was translated for it (the provider call failed, or the response was still missing that key), with any secret-free failure code and message reported in `notices`. `budgetWithheld` is a candidate never sent because a `maxTokens` budget in `"stop"` mode had already tripped. `needsReview` flags accepted keys the review heuristics want a human to look at, and never withholds anything. `unfilled`, `malformedRows`, and `duplicateKeys` are populated only by `importWorkbook`, which returns this same shape. Every withheld key keeps its prior lock hash and is retried next run.
97
-
98
- Whole-run failures throw an `SdkError`: an unknown format, a `locales` entry that is not a configured target, provider construction (including a missing API key), an unreadable or invalid source file, a corrupt lock file, an invalid `concurrency`, or the concurrency-and-budget conflict above. A per-locale failure never throws; it is isolated on that locale's summary. On a non-dry run the flow also writes `.verbatra-local/run-status.json` (best-effort, read back through `runStatus`).
99
-
100
- ```ts
101
- const preview = await translate({ config, dryRun: true });
102
- ```
86
+ Identity helper that types a config literal against the provider you name in `provider.id`. See [Configuration](https://verbatra.kreitz-webdev.de/docs/config-file).
103
87
 
104
- ### `watch(input): Promise<WatchController>`
105
-
106
- Watches the source file and re-runs the translate flow on each debounced change. `input` is `{ config, cwd?, locales?, debounceMs?, onRun, cache?, concurrency?, lockAcquireTimeoutMs?, onLockWait?, onProgress? }`; `debounceMs` defaults to 300, `locales` narrows every run of the session to a subset of the configured target locales and is validated once at startup rather than per cycle, and the last five are passed straight through to every run. One run starts immediately at startup, before any change arrives. Runs are serialized, so changes during a run collapse into a single follow-up.
107
-
108
- `onRun` receives a `WatchRunResult` per run: `{ status: "succeeded", summary }` or `{ status: "failed", error }` with a secret-free `{ code, message }`, so a failing run is reported and watching continues. `watch` itself throws only at startup: `UNKNOWN_LOCALE` for a `locales` entry that is not a configured target, `CONCURRENCY_INVALID` or `CONCURRENCY_BUDGET_CONFLICT` for a `concurrency` no cycle could honor (resolved once, before the watcher exists, rather than failing every cycle), and `SOURCE_UNREADABLE` when the source locale file is absent. Resolves to a `WatchController` whose `stop()` closes the watcher and awaits the in-flight run.
109
-
110
- ```ts
111
- import { loadConfig, watch } from "@verbatra/sdk";
112
-
113
- const config = await loadConfig();
114
- const controller = await watch({
115
- config,
116
- onRun: (result) => console.log(result.status),
117
- });
118
-
119
- // Stop cleanly on Ctrl-C.
120
- process.on("SIGINT", () => void controller.stop());
121
- ```
88
+ ### `loadConfig(options?): Promise<VerbatraConfig>`
122
89
 
123
- ### `check(input): Promise<CheckSummary>`
90
+ Discover, load, and validate the project config, searching upward from the working directory. Throws an `SdkError` when no config is found or validation fails.
124
91
 
125
- Reports per-locale drift without calling a provider, writing any file, or touching the lock. `input` is `{ config, cwd?, locales? }`, where `locales` narrows the check to a subset of target locales (defaults to all configured). Resolves to a `CheckSummary` whose `locales` lists one `LocaleCheckSummary` each (counts only: `missing`, `stale`, `upToDate`, and a per-locale `inSync`); the top-level `inSync` is true only when every checked locale is in sync.
92
+ ### `translate(input, deps?): Promise<RunSummary>`
126
93
 
127
- ```ts
128
- import { check, loadConfig } from "@verbatra/sdk";
94
+ Translate every configured target locale once. The summary splits locales into succeeded, partial, and failed, and carries the run's notices, review flags, token usage, and budget standing. See [`verbatra translate`](https://verbatra.kreitz-webdev.de/docs/cli/translate).
129
95
 
130
- const config = await loadConfig();
131
- const summary = await check({ config });
132
-
133
- if (!summary.inSync) {
134
- console.log("Locales are out of sync; run verbatra translate.");
135
- }
136
- ```
96
+ ### `watch(input, deps?): Promise<WatchController>`
137
97
 
138
- ### `diff(input): Promise<DiffSummary>`
98
+ Watch the source locale file and re-translate on every change until the returned controller is stopped. Each run gets a fresh budget. See [`verbatra watch`](https://verbatra.kreitz-webdev.de/docs/cli/watch).
139
99
 
140
- Lists the keys a run would touch, without writing anything. `input` is the same `{ config, cwd?, locales? }` shape as `check`. Resolves to a `DiffSummary` whose `locales` lists one `LocaleDiff` each, with the key arrays `missing` (would be added), `changed` (would be re-translated), and `orphaned` (present in the target but absent from the source), plus a per-locale `hasPendingChanges` driven by `missing` and `changed` only, since a default run does not prune. The top-level `hasPendingChanges` is true when any checked locale has some.
141
-
142
- ```ts
143
- import { diff, loadConfig } from "@verbatra/sdk";
144
-
145
- const config = await loadConfig();
146
- const summary = await diff({ config });
147
- ```
100
+ ### `check(input, deps?): Promise<CheckSummary>`
148
101
 
149
- ### `doctor(input?): Promise<DoctorResult>`
102
+ Count missing, stale, and up-to-date keys per locale. Calls no provider and writes nothing. See [`verbatra check`](https://verbatra.kreitz-webdev.de/docs/cli/check).
150
103
 
151
- Validates the project setup and spends nothing: no provider is constructed, no network request is made, no file is written, and no API key value is ever read. `input` is `{ cwd?, configPath? }`, and it is optional as a whole because `doctor` loads the config itself, so a project with no config at all still gets a report rather than a thrown error. Five checks run, one per `DoctorCheckId`: `"config"` (a config was found and validates), `"format-adapter"` (the configured `format` resolves to an adapter), `"provider"` (the configured `provider.id` resolves to a factory), `"api-key"` (the environment variable that provider reads its key from is set), and `"source-file"` (the source locale file exists at its resolved path). Every check runs even when an earlier one failed, so one call reports every independent problem; when the config itself cannot be loaded, the four checks that depend on it report `"skipped"` instead of a verdict they could not reach. Resolves to a `DoctorResult` whose `ok` is true only when no check failed, and whose `checks` carry an `id`, a stable `title`, a `status` of `"pass"`, `"fail"`, or `"skipped"`, and a `detail` naming the variable or path behind the verdict but never a key value. Throws `CONFIG_NOT_FOUND` only for an explicit `configPath` that does not exist; a config that is merely absent from the search is a failed check instead.
104
+ ### `diff(input, deps?): Promise<DiffSummary>`
152
105
 
153
- ```ts
154
- import { doctor } from "@verbatra/sdk";
106
+ Name the keys that would be added, re-translated, or orphaned per locale. Calls no provider and writes nothing. See [`verbatra diff`](https://verbatra.kreitz-webdev.de/docs/cli/diff).
155
107
 
156
- const report = await doctor();
108
+ ### `doctor(input?, deps?): Promise<DoctorResult>`
157
109
 
158
- for (const entry of report.checks) {
159
- console.log(`${entry.status}: ${entry.title} - ${entry.detail}`);
160
- }
110
+ Validate the config, the format adapter, the provider, its key variable, and the source locale file in one pass, reporting every problem at once. Reads no key value. See [`verbatra doctor`](https://verbatra.kreitz-webdev.de/docs/cli/doctor).
161
111
 
162
- process.exitCode = report.ok ? 0 : 1;
163
- ```
112
+ ### `extract(input, deps?): Promise<ExtractResult>`
164
113
 
165
- ### `exportWorkbook(input): Promise<ExportWorkbookResult>`
114
+ Scan your application source for translation call sites and add the new keys to the source locale file. See [`verbatra extract`](https://verbatra.kreitz-webdev.de/docs/cli/extract).
166
115
 
167
- Exports the strings that need translating into a handoff for a human translator. `input` is `{ config, cwd?, out?, locales?, includeUnchanged?, format? }`. By default it writes the missing and changed strings for every target locale; `locales` narrows which target locales are exported, and `includeUnchanged: true` also exports already up-to-date strings. No provider is called and no lock file is written.
116
+ ### `generateTypes(input, deps?): Promise<GenerateTypesResult>`
168
117
 
169
- `format` is an `ExchangeFormat`, one of the exported `EXCHANGE_FORMATS` and defaulting to `DEFAULT_EXCHANGE_FORMAT` (`"xlsx"`). With `"xlsx"` it writes one styled workbook with a sheet per locale to `DEFAULT_WORKBOOK_PATH` (`verbatra-translations.xlsx`). With `"csv"` or `"tsv"` it writes one plain `<locale>.csv` or `<locale>.tsv` per exported locale into a directory, `DEFAULT_DELIMITED_PATH` (`verbatra-translations`) by default, creating it if it is missing, and it also leaves a hidden manifest there naming the locales it wrote, which is what lets a later import reject a file left over from a wider export. `out` overrides the default: a file path for `"xlsx"`, a directory for the delimited formats. Resolves to an `ExportWorkbookResult` with the absolute `path` written (the shared directory when one file per locale was written) and a per-locale row count.
118
+ Generate a TypeScript declaration of every source catalog key and the arguments its message interpolates. See [`verbatra types`](https://verbatra.kreitz-webdev.de/docs/cli/types).
170
119
 
171
- ### `importWorkbook(input): Promise<RunSummary>`
120
+ ### `pseudolocalize(input, deps?): Promise<PseudolocalizeResult>`
172
121
 
173
- Imports a filled handoff back into the locale files, gating every row through the same integrity gate as `translate` on top of a fresh source-drift check. `input` is `{ config, workbook, cwd?, dryRun?, format? }`. With `dryRun: true` it validates and reports without writing locale files or updating the lock.
122
+ Build a pseudolocale from the source strings, accented, expanded, and bracketed, without calling a provider. See [`verbatra pseudo`](https://verbatra.kreitz-webdev.de/docs/cli/pseudo).
174
123
 
175
- `format` takes the same `ExchangeFormat` values as `exportWorkbook` and must match how the handoff was written. For `"xlsx"`, `workbook` is the workbook file and the locale comes from the sheet name. For `"csv"` and `"tsv"` the locale comes from the file name instead, and `workbook` is tried as a single `<locale>.csv` file first, so one locale can be imported on its own; if no file exists there it is read as the directory the per-locale files were written into. A file in that directory that the most recent export's manifest does not list is refused as a leftover rather than applied, and reported as that locale's failure with `HANDOFF_FILE_STALE`; a directory with no readable manifest is read as it always was, with every file present imported. Resolves to a `RunSummary`, the same shape `translate` returns: a row the translator left blank whose key still needs a translation is reported in that locale's `unfilled` (nothing is written and the prior lock baseline is kept), an unreadable row in `malformedRows`, a repeated key in `duplicateKeys` (the first occurrence wins), and a configured target locale whose sheet or delimited file is missing from the handoff is reported as that locale's failed summary rather than silently dropped. `unfilled`, `malformedRows`, and `duplicateKeys` do not feed a locale's `status`, so a partly filled sheet still imports the rows it has. A key is cleared by filling its Translation cell with the `[[CLEAR]]` sentinel; an ordinary blank never clears a value.
124
+ ### `exportWorkbook(input, deps?): Promise<ExportWorkbookResult>`
176
125
 
177
- ```ts
178
- import { exportWorkbook, importWorkbook, loadConfig } from "@verbatra/sdk";
126
+ Write the strings that need translating to a translator handoff: a styled Excel workbook, or one CSV or TSV file per locale. See [Manual translation](https://verbatra.kreitz-webdev.de/docs/manual-translation).
179
127
 
180
- const config = await loadConfig();
128
+ ### `importWorkbook(input, deps?): Promise<RunSummary>`
181
129
 
182
- // Export the strings that need translating to an Excel workbook.
183
- const { path } = await exportWorkbook({ config });
130
+ Read a filled handoff back into the locale files, through the same integrity gate a translate run applies, returning the same `RunSummary` shape.
184
131
 
185
- // ...a human fills the Translation column, then import the file back.
186
- const summary = await importWorkbook({ config, workbook: path });
187
- ```
132
+ ### `exportTmx(input, deps?)` and `importTmx(input, deps?)`
188
133
 
189
- See [Manual translation](https://verbatra.kreitz-webdev.de/docs/manual-translation) for the full round-trip and the workbook layout.
134
+ Move the whole translation memory out as a TMX file any other translation tool can read, or read one in. See [`verbatra tmx`](https://verbatra.kreitz-webdev.de/docs/cli/tmx).
190
135
 
191
136
  ### More entry points
192
137
 
193
- Beyond the flows above, the SDK exports the building blocks Verbatra Studio and other tooling sit on. Most are a one-call read or a locked single-key write:
194
-
195
- - `keyIntegrity` reports, per changed key, whether its placeholders still match the source (with the missing and extra tokens on a mismatch) and whether the current target value is still valid ICU.
196
- - `lockState` reports the lock file's existence, version, and per-locale drift; `loadLockFile` reads the lock file itself.
197
- - `runStatus` reads the persisted review-flag and token-usage snapshot the last non-dry `translate` or `watch` run left behind; it never throws, and a missing, corrupt, or unrecognized file simply reports as unavailable.
198
- - `keyValue` reads one key's current source and target values.
199
- - `editEntry` saves a manually edited translation for one key, and `retranslateEntry` re-runs the provider for one key; both run the candidate through the same integrity gate as a full run (a rejection names an `IntegrityGateReason` and writes nothing) and hold the same per-locale write lock.
200
- - `readLocaleFileSnapshot` and `diffLocaleSnapshots` snapshot one locale file as per-key content hashes and compare two snapshots, the primitives behind live-refresh watching.
201
- - `loadConfigWithMeta` is `loadConfig` plus config-source and glossary provenance.
202
- - `readGlossaryFile` reads a file-backed glossary fresh from disk, under the same validation `loadConfig` applies, for a long-running tool that has to show the glossary as it is now rather than as it was when the config was loaded. `updateGlossaryTerm` adds, replaces, or removes exactly one term (`translation: null` removes it) and returns the glossary as it now stands, keeping the file's existing key order and indentation. Both take the `GlossaryProvenance` from `loadConfigWithMeta` rather than a path, so the file they touch is always the one the config names. A write runs under a project-wide glossary lock and replaces the file atomically. Only a file-backed glossary can be read or changed this way: a glossary written inline in the config module is refused with `GLOSSARY_NOT_FILE_BACKED` rather than rewritten, and a failed write is `GLOSSARY_UNWRITABLE`.
203
-
204
- The rest are the values a tool needs to agree with verbatra rather than restate it:
205
-
206
- - `createLocalePathResolver` builds the two-way locale-to-path mapping from the `sourceLocale`, `targetLocales`, and `files` slice of a config: `pathFor(locale)` gives a locale's absolute file path and `localeFor(path)` gives the configured locale owning a path, which is how a file watcher decides whether a change concerns verbatra at all.
207
- - `EXCHANGE_FORMATS`, `DEFAULT_EXCHANGE_FORMAT`, `DEFAULT_WORKBOOK_PATH`, and `DEFAULT_DELIMITED_PATH` are the handoff formats and their default output paths, so a `--format` argument can be validated and offered without restating the list. `EXCHANGE_FORMATS` is derived from the `ExchangeFormat` type itself, so it can never drift behind a format the SDK accepts.
208
- - `CACHE_FILE_NAME` and `LOCK_FILE_NAME` are the names of the two files a run maintains in the project (`verbatra.cache.json` and `verbatra.lock.json`), for tooling that has to find, gitignore, or clear them.
209
- - `verbatraConfigSchema` is the zod schema `loadConfig` validates against, for validating a config object you assembled yourself. The same schema is published as a JSON Schema document at `@verbatra/sdk/config-schema.json`, generated at build time, so an editor can complete and validate a `.verbatrarc.json` or YAML config; the config accepts an optional top-level `$schema` key to point at it, which is ignored at runtime. `scaffoldingMetadata` carries the facts a project generator needs to write a first config (each provider's key environment variable, a starting model, the token-limit option name it takes, and the supported formats), which is what `verbatra init` renders from. Neither holds an API key value.
210
- - `SdkFs` is the file-system port every file the SDK touches goes through, so supplying your own as the `deps.fs` option redirects all of it and an entire run can be held in memory: the run-status file, the lock file, the config glossary, the workbook and delimited I/O, and the locale files themselves, which the format adapters read and write through a port built from this one. Reads are size-bounded by contract and writes are expected to be atomic. The single exception is a caller-supplied `deps.adapterRegistry`: those adapters were constructed by you, so `deps.fs` cannot reach them and passing both means you own that wiring.
138
+ The remaining exports are the building blocks Verbatra Studio, the MCP server, and other tooling sit on. Most are a one-call read or a locked single-key write.
139
+
140
+ | Export | What it does |
141
+ | --- | --- |
142
+ | `editEntry`, `retranslateEntry` | Save a manual translation for one key, or re-run the provider for one key. Both run the candidate through the same integrity gate as a full run and hold the same per-locale write lock; a rejection names an `IntegrityGateReason` and writes nothing |
143
+ | `keyValue`, `localeValues` | Read one key's current source and target values, or a whole locale's key/value pairs |
144
+ | `keyIntegrity` | Report, per changed key, whether its placeholders, inline markup, and ICU still match the locked baseline |
145
+ | `lockState`, `loadLockFile` | Read the lock file's existence, version, and per-locale drift, or the lock file itself |
146
+ | `runStatus`, `budgetStanding` | Read the review-flag and token-usage snapshot the last non-dry run left behind, and turn a `RunBudget` into a standing |
147
+ | `readLocaleFileSnapshot`, `diffLocaleSnapshots` | Snapshot one locale file as per-key content hashes and compare two snapshots: the primitives behind live-refresh watching |
148
+ | `loadConfigWithMeta`, `readGlossaryFile`, `updateGlossaryTerm` | `loadConfig` plus config-source and glossary provenance, and the file-backed glossary read and single-term write that take that provenance rather than a path |
149
+ | `createLocalePathResolver` | Build the two-way locale-to-path mapping from a config, so a watcher can decide whether a changed file concerns verbatra at all |
150
+ | `createDefaultRegistry`, `createTreeFileAdapter`, `createFlatFileAdapter`, `AdapterRegistry`, `nodeAdapterFs` | The format-adapter construction surface: build an adapter for a format verbatra does not ship and hand the registry to a flow as `deps.adapterRegistry` |
151
+ | `verbatraConfigSchema`, `scaffoldingMetadata` | The zod schema `loadConfig` validates against (also published as `@verbatra/sdk/config-schema.json`), and the facts a project generator needs to write a first config |
152
+ | `LOCK_FILE_NAME`, `CACHE_FILE_NAME`, `EXCHANGE_FORMATS`, `DEFAULT_EXCHANGE_FORMAT`, `DEFAULT_WORKBOOK_PATH`, `DEFAULT_DELIMITED_PATH`, `DEFAULT_TMX_PATH`, `DEFAULT_TYPES_PATH` | The file names and handoff formats a run uses, so tooling can find, offer, or gitignore them without restating the list |
153
+ | `SdkFs`, `redact`, `resolveDryRun`, `isCustomFormatId`, `tmxErrorLocation` | The file-system port every file the SDK touches goes through, the secret-redaction pass, and small helpers for agreeing with verbatra rather than restating it |
211
154
 
212
155
  ## Errors and results
213
156
 
214
- `SdkError` is the SDK's own structured error type, thrown for whole-run failures such as a missing or invalid config or an unreadable source file. It carries a stable `code` from the exported `SdkErrorCode` union and never contains an API key. It is not the only error a caller can see: `retranslateEntry` propagates the provider's own `ProviderError` when the provider call fails or returns nothing for the key, and a target locale file that exists but is malformed rejects with the adapter's own error.
157
+ `SdkError` is the SDK's own structured error type, thrown for whole-run failures such as a missing or invalid config or an unreadable source file. It carries a stable `code` from the exported `SdkErrorCode` union and never contains an API key. It is not the only error a caller can see: `retranslateEntry` propagates the provider's own `ProviderError`, and a target locale file that exists but is malformed rejects with the adapter's own `AdapterError`.
215
158
 
216
- Per-locale failures do not throw: they are recorded on the `RunSummary` so one failing locale never aborts the others, and that includes a locale whose write lock could not be acquired. A locale's `error.code` is a preserved string from the underlying provider or adapter failure (`"LOCALE_FAILED"` is only the fallback), deliberately wider than `SdkErrorCode`, so do not treat it as a closed set.
159
+ Per-locale failures do not throw. They are recorded on the `RunSummary` so one failing locale never aborts the others, including a locale whose write lock could not be acquired. A locale's `error.code` is a preserved string from the underlying provider or adapter failure (`"LOCALE_FAILED"` is only the fallback), deliberately wider than `SdkErrorCode`, so do not treat it as a closed set.
217
160
 
218
161
  ## Documentation
219
162
 
220
163
  - [Documentation site](https://verbatra.kreitz-webdev.de)
221
- - [Project README](https://github.com/verbatra/verbatra)
222
- - [`@verbatra/cli`](https://github.com/verbatra/verbatra/tree/main/packages/cli) for the command-line tool
164
+ - [SDK reference](https://verbatra.kreitz-webdev.de/docs/sdk)
165
+ - [Configuration](https://verbatra.kreitz-webdev.de/docs/config-file)
166
+ - [`@verbatra/cli`](https://www.npmjs.com/package/@verbatra/cli) for the command-line tool
223
167
 
224
168
  ## License
225
169
 
@@ -18,20 +18,30 @@
18
18
  }
19
19
  },
20
20
  "format": {
21
- "type": "string",
22
- "enum": [
23
- "i18next-json",
24
- "vue-i18n-json",
25
- "next-intl-json",
26
- "ngx-translate-json",
27
- "xliff",
28
- "yaml",
29
- "arb",
30
- "properties",
31
- "apple-strings",
32
- "apple-xcstrings",
33
- "android-xml",
34
- "gettext-po"
21
+ "anyOf": [
22
+ {
23
+ "type": "string",
24
+ "enum": [
25
+ "i18next-json",
26
+ "vue-i18n-json",
27
+ "next-intl-json",
28
+ "ngx-translate-json",
29
+ "xliff",
30
+ "yaml",
31
+ "arb",
32
+ "properties",
33
+ "apple-strings",
34
+ "apple-xcstrings",
35
+ "android-xml",
36
+ "gettext-po",
37
+ "ini",
38
+ "resx"
39
+ ]
40
+ },
41
+ {
42
+ "type": "string",
43
+ "pattern": "^custom:[a-z0-9]+(?:-[a-z0-9]+)*$"
44
+ }
35
45
  ]
36
46
  },
37
47
  "files": {
@@ -306,11 +316,40 @@
306
316
  "generatePlurals": {
307
317
  "type": "boolean"
308
318
  },
319
+ "fuzzyCache": {
320
+ "type": "object",
321
+ "properties": {
322
+ "enabled": {
323
+ "type": "boolean"
324
+ },
325
+ "threshold": {
326
+ "type": "number",
327
+ "minimum": 0.5,
328
+ "maximum": 1
329
+ }
330
+ },
331
+ "required": [
332
+ "enabled"
333
+ ],
334
+ "additionalProperties": false
335
+ },
309
336
  "maxBatchSize": {
310
337
  "type": "integer",
311
338
  "exclusiveMinimum": 0,
312
339
  "maximum": 9007199254740991
313
340
  },
341
+ "maxLength": {
342
+ "type": "object",
343
+ "propertyNames": {
344
+ "type": "string",
345
+ "minLength": 1
346
+ },
347
+ "additionalProperties": {
348
+ "type": "integer",
349
+ "exclusiveMinimum": 0,
350
+ "maximum": 9007199254740991
351
+ }
352
+ },
314
353
  "maxTokens": {
315
354
  "type": "integer",
316
355
  "exclusiveMinimum": 0,
@@ -322,6 +361,128 @@
322
361
  "warn",
323
362
  "stop"
324
363
  ]
364
+ },
365
+ "rates": {
366
+ "type": "object",
367
+ "properties": {
368
+ "asOf": {
369
+ "type": "string",
370
+ "format": "date",
371
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
372
+ },
373
+ "currency": {
374
+ "type": "string",
375
+ "pattern": "^[A-Z]{3}$"
376
+ },
377
+ "table": {
378
+ "type": "object",
379
+ "propertyNames": {
380
+ "type": "string",
381
+ "minLength": 1
382
+ },
383
+ "additionalProperties": {
384
+ "anyOf": [
385
+ {
386
+ "type": "object",
387
+ "properties": {
388
+ "inputPerMillionTokens": {
389
+ "type": "number",
390
+ "minimum": 0,
391
+ "maximum": 1000000000
392
+ },
393
+ "outputPerMillionTokens": {
394
+ "type": "number",
395
+ "minimum": 0,
396
+ "maximum": 1000000000
397
+ }
398
+ },
399
+ "required": [
400
+ "inputPerMillionTokens",
401
+ "outputPerMillionTokens"
402
+ ],
403
+ "additionalProperties": false
404
+ },
405
+ {
406
+ "type": "object",
407
+ "properties": {
408
+ "perMillionCharacters": {
409
+ "type": "number",
410
+ "minimum": 0,
411
+ "maximum": 1000000000
412
+ }
413
+ },
414
+ "required": [
415
+ "perMillionCharacters"
416
+ ],
417
+ "additionalProperties": false
418
+ }
419
+ ]
420
+ }
421
+ }
422
+ },
423
+ "required": [
424
+ "asOf",
425
+ "currency",
426
+ "table"
427
+ ],
428
+ "additionalProperties": false
429
+ },
430
+ "extract": {
431
+ "type": "object",
432
+ "properties": {
433
+ "framework": {
434
+ "type": "string",
435
+ "enum": [
436
+ "i18next"
437
+ ]
438
+ },
439
+ "roots": {
440
+ "minItems": 1,
441
+ "type": "array",
442
+ "items": {
443
+ "type": "string",
444
+ "minLength": 1
445
+ }
446
+ },
447
+ "exclude": {
448
+ "type": "array",
449
+ "items": {
450
+ "type": "string",
451
+ "minLength": 1
452
+ }
453
+ },
454
+ "literals": {
455
+ "type": "object",
456
+ "properties": {
457
+ "ignore": {
458
+ "type": "array",
459
+ "items": {
460
+ "type": "string",
461
+ "minLength": 1
462
+ }
463
+ }
464
+ },
465
+ "additionalProperties": false
466
+ },
467
+ "unused": {
468
+ "type": "object",
469
+ "properties": {
470
+ "ignore": {
471
+ "type": "array",
472
+ "items": {
473
+ "type": "string",
474
+ "minLength": 1
475
+ }
476
+ }
477
+ },
478
+ "additionalProperties": false
479
+ }
480
+ },
481
+ "required": [
482
+ "framework",
483
+ "roots"
484
+ ],
485
+ "additionalProperties": false
325
486
  }
326
487
  },
327
488
  "required": [