@verbatra/sdk 0.7.1 → 0.8.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/dist/index.d.cts CHANGED
@@ -4,8 +4,19 @@ import { Interactions } from '@google/genai';
4
4
  import OpenAI from 'openai';
5
5
 
6
6
  /**
7
- * A single, format-neutral translation unit. Placeholders are supplied already
8
- * extracted; core never derives them from the value.
7
+ * A single, format-neutral translation unit. Placeholders are supplied already extracted; core never
8
+ * derives them from the value, so an adapter that reads a file is the one authority on what counts as
9
+ * a placeholder in that format.
10
+ *
11
+ * The fields are: `key`, the entry's identifier within its namespace, non-empty and already flattened
12
+ * to the dotted path an adapter produced; `namespace`, the namespace the entry belongs to, empty when
13
+ * the format has no namespacing; `value`, the translatable string itself, which is untrusted input and
14
+ * may be empty; `description` and `meaning`, both optional free-form context that a provider is told to
15
+ * use only for disambiguation and never to translate or echo back; `placeholders`, the format's
16
+ * placeholder tokens in document order, used for the output integrity check and to decide which entries
17
+ * a placeholder-blind provider must withhold; and `isPlural`, whether the entry carries plural forms
18
+ * (an i18next plural key suffix, a vue-i18n pipe-separated value, or an ICU plural), which the adapter
19
+ * decides and which feeds the content hash.
9
20
  */
10
21
  declare const translationEntrySchema$1: z.ZodObject<{
11
22
  key: z.ZodString;
@@ -16,12 +27,16 @@ declare const translationEntrySchema$1: z.ZodObject<{
16
27
  placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
17
28
  isPlural: z.ZodBoolean;
18
29
  }, z.core.$strip>;
19
- /** The validated shape of one translation unit; the inferred type of {@link translationEntrySchema}. */
30
+ /**
31
+ * The validated shape of one translation unit; the inferred type of {@link translationEntrySchema},
32
+ * made deeply readonly. See that schema for what each field carries.
33
+ */
20
34
  type TranslationEntry$1 = Readonly<z.infer<typeof translationEntrySchema$1>>;
21
35
 
22
36
  /**
23
37
  * Result of comparing source placeholders against translated-output placeholders as MULTISETS
24
- * (counts matter). Always returned (never thrown) for an ordinary mismatch.
38
+ * (counts matter, so dropping one of two occurrences of the same token is still a miss). Always
39
+ * returned, never thrown: a mismatch is data the caller decides what to do about, not a failure.
25
40
  */
26
41
  interface PlaceholderIntegrityResult$1 {
27
42
  /** True when nothing is missing or extra: the multisets match, regardless of order. */
@@ -30,13 +45,22 @@ interface PlaceholderIntegrityResult$1 {
30
45
  readonly missing: readonly string[];
31
46
  /** Occurrences present in the translation but absent from source, one per surplus occurrence. Sorted. */
32
47
  readonly extra: readonly string[];
33
- /** Same multiset present in both, but in a different order. */
48
+ /** Same multiset present in both, but in a different order. Never true alongside a mismatch. */
34
49
  readonly reordered: boolean;
35
50
  }
36
51
 
37
- /** A provider is either a prompt-driven LLM or a dedicated machine-translation API. */
52
+ /**
53
+ * A provider is either a prompt-driven LLM (`llm`) or a dedicated machine-translation API
54
+ * (`machine-translation`). The distinction is descriptive, not dispatch: both kinds satisfy the same
55
+ * {@link TranslationProvider} contract, and callers use it to set expectations (an LLM accepts free-form
56
+ * context and a glossary term map, an MT API generally does not).
57
+ */
38
58
  type ProviderKind = "llm" | "machine-translation";
39
- /** Target tone for a translation. Maps to formality for machine-translation providers. */
59
+ /**
60
+ * Target tone for a translation. `formal` and `informal` request the corresponding register, and
61
+ * `neutral` requests neither. Machine-translation providers map it to their formality setting and may
62
+ * report a `FORMALITY_DOWNGRADED` {@link ProviderNotice} when they cannot apply it.
63
+ */
40
64
  type Tone = "formal" | "informal" | "neutral";
41
65
  /**
42
66
  * Produces the placeholder set of a value for the output integrity check. Supplied
@@ -54,7 +78,7 @@ type PlaceholderComparator = (source: string, translated: string) => Placeholder
54
78
  /**
55
79
  * A batch translation request. Format- and provider-neutral: it carries no prompt,
56
80
  * model, key, or other provider-specific field. The placeholder extractor is
57
- * mandatory (see validateRequest).
81
+ * mandatory and is checked before the data fields are parsed.
58
82
  */
59
83
  interface TranslateRequest {
60
84
  /** BCP-47 source locale of the entries (for example, "en"). */
@@ -65,7 +89,7 @@ interface TranslateRequest {
65
89
  readonly entries: readonly TranslationEntry$1[];
66
90
  /** Optional source-term to target-term map applied by glossary-capable providers. */
67
91
  readonly glossary?: Readonly<Record<string, string>>;
68
- /** Optional target tone; machine-translation providers map it to formality. */
92
+ /** Optional target {@link Tone}; machine-translation providers map it to formality. */
69
93
  readonly tone?: Tone;
70
94
  /** Mandatory placeholder extractor; the output integrity check runs against it. */
71
95
  readonly extractPlaceholders: PlaceholderExtractor;
@@ -77,14 +101,16 @@ interface TranslateRequest {
77
101
  readonly comparePlaceholders?: PlaceholderComparator;
78
102
  /**
79
103
  * Optional cancellation signal for this batch. When aborted, an in-flight provider call rejects
80
- * with the abort, unwrapped, instead of a {@link ProviderError} (see `guardProviderCall`). Not a
81
- * plain-data field: it is never validated by `requestDataSchema` and never sent to a provider.
104
+ * with the abort, unwrapped, instead of a `ProviderError`. Not a plain-data field: it is
105
+ * never validated alongside the data fields and never sent to a provider.
82
106
  */
83
107
  readonly signal?: AbortSignal;
84
108
  }
85
109
  /** Token usage, when the provider reports it. Absent for providers without tokens (DeepL). */
86
110
  interface Usage {
111
+ /** Tokens consumed by the request, summed across the initial call and any repair round. */
87
112
  readonly inputTokens: number;
113
+ /** Tokens produced by the response, summed across the initial call and any repair round. */
88
114
  readonly outputTokens: number;
89
115
  }
90
116
  /**
@@ -113,25 +139,38 @@ interface ProviderNotice {
113
139
  }
114
140
  /**
115
141
  * Stable codes for a derived, per-key "needs review" signal. This is verbatra's own computed
116
- * assessment, never a raw model self-score: four are recomputable from plain source/translated
117
- * values (see the shared heuristic function), and `PROVIDER_DEGRADED` is applied by the caller from
118
- * a provider notice.
142
+ * assessment, never a raw model self-score: four are recomputable from plain source and translated
143
+ * values, and `PROVIDER_DEGRADED` is layered on afterwards from the batch's notices.
119
144
  *
120
145
  * - `LENGTH_RATIO_OUTLIER`: the translated value's length is far shorter or longer than the source's.
121
- * - `EQUALS_SOURCE`: the translated value equals the source value verbatim.
122
- * - `GLOSSARY_TERM_MISSED`: a configured glossary target term did not appear in the translation.
146
+ * Only considered once the trimmed source is long enough for the ratio to mean anything.
147
+ * - `EQUALS_SOURCE`: the translated value equals the source value once both are trimmed, so a
148
+ * difference in leading or trailing whitespace alone still counts as equal. Also requires that
149
+ * the locales differ and that the value contains at least one letter (so a bare symbol or number
150
+ * is not flagged).
151
+ * - `GLOSSARY_TERM_MISSED`: a configured glossary source term appeared in the source but its target
152
+ * term did not appear in the translation.
123
153
  * - `INTEGRITY_REORDERED`: the placeholder set matched but landed in a different order.
124
- * - `PROVIDER_DEGRADED`: the batch this key came from carried a graceful-degradation notice.
154
+ * - `PROVIDER_DEGRADED`: the batch this key came from carried a `FORMALITY_DOWNGRADED` or
155
+ * `GLOSSARY_IGNORED` notice, either of which can silently change wording. A
156
+ * `PLACEHOLDER_UNSUPPORTED` notice does not raise it, since the affected entries are withheld
157
+ * rather than degraded.
125
158
  */
126
159
  type ReviewReasonCode = "LENGTH_RATIO_OUTLIER" | "EQUALS_SOURCE" | "GLOSSARY_TERM_MISSED" | "INTEGRITY_REORDERED" | "PROVIDER_DEGRADED";
127
160
  /** A key flagged for human review, carrying every reason code that applies. */
128
161
  interface ReviewFlag {
162
+ /** Always "review": the flag exists only for flagged keys, so there is no "ok" member. */
129
163
  readonly status: "review";
164
+ /** Every {@link ReviewReasonCode} that applies to this key; never empty. */
130
165
  readonly reasons: readonly ReviewReasonCode[];
131
166
  }
132
167
  /** Result of a batch translation: per-key values, per-key integrity outcomes, and any notices. */
133
168
  interface TranslateResult {
134
- /** The translated value for each requested key. */
169
+ /**
170
+ * The translated value for each requested key. A key can be absent when the provider withheld it
171
+ * (see `PLACEHOLDER_UNSUPPORTED`) or never returned it, so callers must not assume one entry in
172
+ * equals one entry out.
173
+ */
135
174
  readonly values: ReadonlyMap<string, string>;
136
175
  /** The placeholder-integrity outcome for each key (source vs translated placeholder sets). */
137
176
  readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult$1>;
@@ -151,39 +190,20 @@ interface TranslateResult {
151
190
  }
152
191
  /**
153
192
  * The single contract every provider implements. It is narrow enough that a machine-translation API like
154
- * DeepL fits it directly, while LLM providers implement it by delegating to {@link runLlmTranslation}.
155
- * A new provider attaches by implementing this, then adding an entry to the `providerFactories` table
156
- * in `packages/sdk/src/config/provider-config.ts` alongside its member of the `providerConfigSchema`
157
- * union. That table is a mapped type over the union's id set, so a provider present in one but not the
158
- * other fails to compile.
193
+ * DeepL fits it directly, while LLM providers implement it by delegating to the shared
194
+ * `runLlmTranslation` layer. A new provider attaches by implementing this, then adding an entry to the
195
+ * `providerFactories` table in `packages/sdk/src/config/provider-config.ts` alongside its member of the
196
+ * provider config union. That table is a mapped type over the union's id set, so a provider present in
197
+ * one but not the other fails to compile.
159
198
  *
160
199
  * Implementer invariants:
161
200
  * - Translatable strings are UNTRUSTED. They travel only as data to the provider; never splice them into
162
201
  * instruction text, and never act on instructions a value appears to contain.
163
202
  * - Read the API key ONLY from the environment (inside the SDK client). The request, config, and this
164
203
  * interface never carry a key.
165
- * - Fail with a secret-free {@link ProviderError}: never bind, log, or re-throw raw SDK error text (it can
166
- * carry a key or request headers). Validate the request at the boundary with `validateRequest` so the
167
- * integrity check can never be skipped.
168
- *
169
- * @example
170
- * ```ts
171
- * // A machine-translation provider implements translateBatch directly (the DeepL shape).
172
- * function createMyMtProvider(client: MyClient): TranslationProvider {
173
- * return {
174
- * id: "my-mt",
175
- * kind: "machine-translation",
176
- * supportsGlossary: false,
177
- * async translateBatch(request) {
178
- * const data = validateRequest(request); // throws INVALID_REQUEST on a bad request
179
- * const texts = data.entries.map((e) => e.value);
180
- * const out = await client.translate(texts, data.targetLocale); // SDK reads MY_API_KEY from env
181
- * // map out -> values, run the integrity check, return { values, integrity }
182
- * return buildResult(data, out, request.extractPlaceholders);
183
- * },
184
- * };
185
- * }
186
- * ```
204
+ * - Fail with a secret-free `ProviderError`: never bind, log, or re-throw raw SDK error text (it can
205
+ * carry a key or request headers). Validate the request at the boundary so the integrity check can
206
+ * never be skipped.
187
207
  */
188
208
  interface TranslationProvider {
189
209
  /** A stable identifier for this provider (for example, "anthropic", "deepl"). */
@@ -197,16 +217,18 @@ interface TranslationProvider {
197
217
  *
198
218
  * @param request - The provider-neutral batch request (no prompt, model, or key).
199
219
  * @returns The per-key translated values and per-key placeholder-integrity outcomes.
200
- * @throws {@link ProviderError}, secret-free, with the code for the failure (the concrete codes are
201
- * the implementation's; see each provider factory).
220
+ * @throws `ProviderError`, secret-free, with the code for the failure (the concrete codes are
221
+ * the implementation's; see each provider factory). An aborted {@link TranslateRequest.signal}
222
+ * instead rejects with the abort itself, unwrapped.
202
223
  */
203
224
  translateBatch(request: TranslateRequest): Promise<TranslateResult>;
204
225
  }
205
226
 
206
227
  /**
207
- * The Anthropic authoring model type, sourced from the SDK's own model union. It is an
208
- * open union (known literals plus `string & {}`), so unknown or newly released model IDs
209
- * are still accepted. This drives editor completions only; the runtime schema validates.
228
+ * The Anthropic authoring model type, sourced from the SDK's own model union so verbatra
229
+ * restates no model IDs. It is an open union (known literals plus `string & {}`), so unknown
230
+ * or newly released model IDs are still accepted. Type-only: it informs editor completions
231
+ * and is never validated at runtime, where the schema stays `z.string().min(1)`.
210
232
  */
211
233
  type AnthropicModel = Anthropic.Messages.Model;
212
234
 
@@ -219,13 +241,24 @@ type AnthropicModel = Anthropic.Messages.Model;
219
241
  type GeminiModel = Interactions.Model;
220
242
 
221
243
  /**
222
- * The OpenAI authoring model type, sourced from the OpenAI SDK's published chat-model
223
- * union. It drives editor completions only; it is never read at runtime, and the
224
- * runtime schema stays `z.string().min(1)`.
244
+ * The OpenAI authoring model type, sourced from the OpenAI SDK's published chat-model union
245
+ * so verbatra restates no model IDs. Type-only: it drives editor completions and is never
246
+ * read at runtime, where the schema stays `z.string().min(1)`.
225
247
  */
226
248
  type OpenAiModel = OpenAI.ChatModel;
227
249
 
228
- /** Zod schema accepting exactly one of {@link SUPPORTED_FORMATS}. */
250
+ /**
251
+ * Zod schema accepting exactly one of the eight source formats verbatra can read and write. Each
252
+ * member names the ecosystem it round-trips, not just a file extension, because several of them share
253
+ * one: `i18next-json` is i18next's nested JSON with its plural key suffixes, `vue-i18n-json` is Vue
254
+ * I18n's JSON with its pipe-separated plural values, `next-intl-json` is next-intl's ICU-message JSON,
255
+ * and `ngx-translate-json` is ngx-translate's nested JSON. The remaining four are `xliff` (the XLIFF
256
+ * interchange XML), `yaml` (plain nested YAML), `arb` (Flutter's Application Resource Bundle), and
257
+ * `properties` (Java and Spring `.properties` files).
258
+ *
259
+ * The set is closed: a format outside it cannot be represented, so adding one means adding a member
260
+ * here and an adapter that claims it.
261
+ */
229
262
  declare const supportedFormatSchema$1: z.ZodEnum<{
230
263
  "i18next-json": "i18next-json";
231
264
  "vue-i18n-json": "vue-i18n-json";
@@ -236,92 +269,110 @@ declare const supportedFormatSchema$1: z.ZodEnum<{
236
269
  arb: "arb";
237
270
  properties: "properties";
238
271
  }>;
239
- /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
272
+ /** One of the eight supported source formats; the inferred type of {@link supportedFormatSchema}. */
240
273
  type SupportedFormat$1 = z.infer<typeof supportedFormatSchema$1>;
241
274
 
242
- /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
275
+ /**
276
+ * The outcome of a size-bounded text read. A file that is absent or larger than the caller's limit
277
+ * is reported as a state rather than thrown, so a caller can treat "no file yet" as normal and an
278
+ * oversized file as a distinct, actionable condition.
279
+ */
243
280
  type BoundedFileRead = {
281
+ /** The file was read in full. */
244
282
  readonly kind: "ok";
283
+ /** The file's content, decoded as UTF-8. */
245
284
  readonly content: string;
246
285
  } | {
286
+ /** No readable file exists at the path. */
247
287
  readonly kind: "missing";
248
288
  } | {
289
+ /** The file exists but exceeds the requested byte limit, so nothing was read. */
249
290
  readonly kind: "too-large";
250
291
  };
251
- /** Outcome of a bounded binary read: the bytes, or why they could not be read in bounds. */
292
+ /**
293
+ * The outcome of a size-bounded binary read, used for interchange workbooks. Mirrors
294
+ * {@link BoundedFileRead} but yields raw bytes rather than decoded text.
295
+ */
252
296
  type BoundedBytesRead = {
297
+ /** The file was read in full. */
253
298
  readonly kind: "ok";
299
+ /** The file's raw bytes. */
254
300
  readonly bytes: Uint8Array;
255
301
  } | {
302
+ /** No readable file exists at the path. */
256
303
  readonly kind: "missing";
257
304
  } | {
305
+ /** The file exists but exceeds the requested byte limit, so nothing was read. */
258
306
  readonly kind: "too-large";
259
307
  };
260
308
  /**
261
- * The minimal file-system surface the SDK needs. Reads are bounded and writes are atomic. Injectable so
262
- * tests stay deterministic; the format adapters do their own file IO and bypass this seam.
309
+ * The file-system port the SDK's own I/O goes through. Supplying your own implementation as a
310
+ * `deps.fs` option redirects that I/O, which is how the SDK's tests avoid touching disk and how an
311
+ * embedding application can back part of a project with something other than a local disk.
312
+ *
313
+ * Know what the seam does and does not cover before relying on it. It carries the run-status file,
314
+ * the lock-file, the config glossary, and workbook and interchange I/O. It does not carry the
315
+ * locale files: those are read and written by `@verbatra/format-adapters`, which uses `node:fs`
316
+ * directly and is given a path rather than this port. Supplying a `deps.fs` therefore does not make
317
+ * a run fully in-memory, and a project's translations are still read from and written to real disk.
318
+ *
319
+ * Reads are size-bounded by contract so that a hostile or accidentally huge file cannot exhaust
320
+ * memory. Writes are expected to be atomic: the default implementation writes to a temporary file
321
+ * and renames it into place, so a crash mid-write never leaves a half-written file behind.
322
+ * Directory creation is the caller's job, so an implementation whose `writeFile` targets a real
323
+ * directory tree must also implement `mkdir`.
263
324
  */
264
325
  interface SdkFs {
265
- /** Whether a readable file exists at the path. */
326
+ /** Reports whether a readable file exists at the path. */
266
327
  fileExists(path: string): Promise<boolean>;
267
- /**
268
- * Read a file as UTF-8 through a single handle, bounded to maxBytes. TOCTOU-safe: the handle is
269
- * fstat'd and the read never advances past the sized length, so swapping in a larger file cannot
270
- * bypass the cap. A missing or unreadable path is "missing"; a file over the cap is "too-large".
271
- */
328
+ /** Reads a file as UTF-8 text, refusing to read more than `maxBytes`. */
272
329
  readFileBounded(path: string, maxBytes: number): Promise<BoundedFileRead>;
273
- /** Read a file as raw bytes with the same TOCTOU-safe, bounded discipline as {@link readFileBounded}. */
330
+ /** Reads a file as raw bytes, refusing to read more than `maxBytes`. */
274
331
  readBytesBounded(path: string, maxBytes: number): Promise<BoundedBytesRead>;
275
- /** Write atomically: a temp file in the same directory, then rename over the target. */
332
+ /** Writes UTF-8 text to the path, replacing any existing file atomically. */
276
333
  writeFile(path: string, data: string): Promise<void>;
277
- /** Write raw bytes atomically (temp file, then rename over the target). Used for the workbook. */
334
+ /** Writes raw bytes to the path, replacing any existing file atomically. */
278
335
  writeBytes(path: string, data: Uint8Array): Promise<void>;
279
336
  /**
280
- * Atomically create a file only if it does not already exist (O_EXCL semantics), creating any
281
- * missing parent directory first. Returns true if this call created the file, false if a file
282
- * was already present (no write performed). The backbone of {@link withLocaleWriteLock}'s
283
- * exclusive-create lock: the parent-directory creation lives here so that primitive never touches
284
- * `node:fs` directly and a fake `SdkFs` in tests never needs a real directory to exist.
337
+ * Creates a file only if it does not already exist, returning `false` when it does. This is the
338
+ * primitive behind the per-locale write lock, so it must be atomic against other processes.
285
339
  */
286
340
  createExclusive(path: string, data: string): Promise<boolean>;
287
- /** Delete a file if present; a no-op if it is already absent. */
341
+ /** Removes the file at the path, succeeding even when it is already absent. */
288
342
  deleteFile(path: string): Promise<void>;
289
- /**
290
- * Create a directory and every missing parent (mkdir -p); a no-op if it already exists.
291
- *
292
- * Optional so an `SdkFs` written against an earlier version stays valid, and because an in-memory
293
- * test double has no directories to create. Only the delimited export calls it, right before writing
294
- * one interchange file per locale into the directory the caller named.
295
- */
343
+ /** Creates a directory and any missing parents. Optional; omit it if the backing store has no directories. */
296
344
  mkdir?(path: string): Promise<void>;
297
345
  }
298
346
 
299
347
  /**
300
- * The content-addressed translation-memory (TM) cache: a local, gitignored, regenerable sidecar to
301
- * the lock file (`verbatra.cache.json`). It lets a translation whose source content is unchanged be
302
- * reused for free, even under a renamed key or across two keys with byte-identical source text,
303
- * instead of being re-sent to the provider.
348
+ * The on-disk translation memory, read from and written to `verbatra.cache.json` (see
349
+ * {@link CACHE_FILE_NAME}). A run consults it before calling the provider, so a string that was
350
+ * already translated under the same configuration is reused instead of paid for again.
304
351
  *
305
- * Nesting is `entries[fingerprint][targetLocale][contentHash] = value`. The fingerprint scopes every
306
- * entry to one translation context (provider, model, tone, glossary), so a tone or glossary change
307
- * never serves a stale value. Unlike the lock file, a corrupt or wrong-version cache degrades to an
308
- * empty cache and never fails a run: it is a cost optimization, not a correctness record.
352
+ * Entries are keyed by configuration fingerprint, then locale, then source-content hash. The
353
+ * fingerprint layer means that changing the provider, model, tone, or glossary does not silently
354
+ * reuse translations produced under the old settings; those entries simply stop matching.
309
355
  */
310
356
  interface TranslationMemory {
311
- /** The cache schema version; a value this build does not recognize degrades the whole file to empty. */
357
+ /** The cache schema version. A file at an unrecognized version is ignored rather than trusted. */
312
358
  readonly version: number;
313
- /** Cached values, nested fingerprint to target locale to source content hash to translated value. */
359
+ /** Cached values, nested as configuration fingerprint, then locale, then source-content hash. */
314
360
  readonly entries: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, string>>>>>>;
315
361
  }
316
362
 
317
- /** The cache file's name, a sibling of the lock file and obviously JSON. */
363
+ /**
364
+ * The file name of the project's translation memory, resolved against the run's working directory.
365
+ * Commit it to reuse translations across machines and CI runs, or add it to `.gitignore` to treat
366
+ * the cache as purely local. Deleting it is always safe: the next run simply repays for the strings
367
+ * it would have reused.
368
+ */
318
369
  declare const CACHE_FILE_NAME = "verbatra.cache.json";
319
370
 
320
371
  /**
321
- * The provider section of the config: a discriminated union over the provider id, reusing each
322
- * provider's own config schema. There is no key field anywhere in this union except
323
- * openai-compatible's optional `apiKeyEnvVar`, which names an environment variable rather than carrying
324
- * a key value; every provider still reads its actual key from the environment at construction.
372
+ * The zod schema for the `provider` block, discriminated on `id`. Each variant's options are
373
+ * validated strictly, so an option that belongs to a different provider is reported as an error
374
+ * rather than ignored. It is embedded in {@link verbatraConfigSchema} and produces
375
+ * {@link ProviderConfig}.
325
376
  */
326
377
  declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
327
378
  id: z.ZodLiteral<"anthropic">;
@@ -360,15 +411,34 @@ declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
360
411
  requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
361
412
  }, z.core.$strict>;
362
413
  }, z.core.$strip>], "id">;
363
- /** One validated provider section: the provider id plus that provider's own options. */
414
+ /**
415
+ * The `provider` block of a verbatra config: a discriminated union on `id`, so each provider
416
+ * carries exactly the options it supports and nothing else.
417
+ *
418
+ * No variant has a field for an API key. Keys are read from the environment by the provider itself
419
+ * (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPL_API_KEY`), which is what keeps
420
+ * them out of config files and out of version control. The `openai-compatible` variant may name a
421
+ * different environment variable through `apiKeyEnvVar`, but still never holds the key itself.
422
+ */
364
423
  type ProviderConfig = z.infer<typeof providerConfigSchema>;
365
- /** The closed set of configurable provider ids. */
424
+ /**
425
+ * The identifier of a supported translation provider: `anthropic`, `openai`, `gemini`, `deepl`, or
426
+ * `openai-compatible`. The last one targets a local or self-hosted server that speaks the OpenAI
427
+ * chat-completions API.
428
+ */
366
429
  type ProviderId = ProviderConfig["id"];
367
430
 
368
431
  /**
369
- * The verbatra project configuration. It carries no API key (the provider reads its key from the
370
- * environment), and unknown top-level keys are rejected so a stray secret cannot hide in it. Validated
371
- * with zod at the boundary regardless of where it was loaded from.
432
+ * The zod schema every verbatra config is validated against. {@link loadConfig} applies it, so most
433
+ * consumers never call it directly; reach for it when you build a config programmatically and want
434
+ * to validate it before handing it to {@link translate}, or when you surface config errors in your
435
+ * own tooling.
436
+ *
437
+ * The object is strict, so an unrecognized key is an error rather than being silently ignored: a
438
+ * typo in a config file is reported instead of quietly doing nothing. Beyond the per-field checks,
439
+ * three whole-config rules are enforced: `targetLocales` must not contain the source locale, it
440
+ * must not contain two locales that differ only in case (they would collide on a case-insensitive
441
+ * file system), and `files.pattern` must contain the `{locale}` token.
372
442
  */
373
443
  declare const verbatraConfigSchema: z.ZodObject<{
374
444
  sourceLocale: z.ZodString;
@@ -444,30 +514,40 @@ declare const verbatraConfigSchema: z.ZodObject<{
444
514
  }>>;
445
515
  }, z.core.$strict>;
446
516
  /**
447
- * The as-authored (or as-parsed) shape of the verbatra configuration, straight from the schema: `glossary`
448
- * is still the union of an inline record or a file path. {@link defineConfig} and the authoring types are
449
- * built on this; `loadConfig` accepts it as input and produces the resolved {@link VerbatraConfig}.
517
+ * A config exactly as it is written in a `verbatra.config.ts` file, before the SDK resolves
518
+ * anything. Its `glossary` may still be a path string pointing at a JSON file.
519
+ *
520
+ * This is what {@link defineConfig} returns and what {@link verbatraConfigSchema} parses. Use
521
+ * {@link VerbatraConfig} for the resolved shape the flows actually consume.
450
522
  */
451
523
  type VerbatraConfigInput = z.infer<typeof verbatraConfigSchema>;
452
524
  /**
453
- * The verbatra configuration after `loadConfig` has resolved it: identical to {@link VerbatraConfigInput}
454
- * except `glossary`, which is always a plain record here. A glossary given as a file path is read,
455
- * parsed, and validated by the loader before a `VerbatraConfig` is produced, so every downstream
456
- * consumer (the translation flow, `watch`, the CLI) keeps receiving the same resolved shape it always
457
- * did.
525
+ * A fully resolved config, ready to pass to any SDK entry point. It differs from
526
+ * {@link VerbatraConfigInput} in one respect: `glossary` is always an in-memory term map, because
527
+ * {@link loadConfig} has already read and validated any glossary file the config pointed at.
528
+ *
529
+ * Every entry point takes this shape, so a caller that builds a config by hand rather than loading
530
+ * one from disk must supply the glossary already resolved.
458
531
  */
459
532
  type VerbatraConfig = Omit<VerbatraConfigInput, "glossary"> & {
533
+ /**
534
+ * Terms that must be translated a fixed way, already resolved to an in-memory map. A config that
535
+ * named a glossary file has had it read by {@link loadConfig} before it reaches here.
536
+ */
460
537
  glossary?: Readonly<Record<string, string>>;
461
538
  };
462
539
 
463
540
  /**
464
- * A provider's known model literals, with the open `string` arm stripped, so the authoring field
465
- * accepts only that provider's models. Static authoring aid only; the runtime schema stays a
466
- * non-empty string, so a brand-new model the installed SDK does not yet list is flagged but still
467
- * runs.
541
+ * Reduces a model union to its literal members, discarding the bare `string` member that some
542
+ * vendor SDKs include to stay open to unreleased models. Without this, `string` would absorb the
543
+ * literals and the editor would offer no completions at all.
468
544
  */
469
545
  type KnownModels<M extends string> = M extends string ? (string extends M ? never : M) : never;
470
- /** One provider's config variant with `options.model` narrowed to that provider's known literals. */
546
+ /**
547
+ * One provider's config variant with its `model` field narrowed from `string` to that vendor's
548
+ * known model IDs, which is what turns model names into an autocompleted, checked union while
549
+ * authoring.
550
+ */
471
551
  type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<ProviderConfig, {
472
552
  id: Id;
473
553
  }> extends infer Variant ? Variant extends {
@@ -480,219 +560,306 @@ type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<Provide
480
560
  };
481
561
  } : never : never;
482
562
  /**
483
- * The authoring view of one provider variant, keyed by id. LLM providers with a known SDK model
484
- * union restrict `options.model` to that provider's literals. DeepL has no model field and
485
- * openai-compatible's model is whatever the local server exposes (no known-model list to restrict
486
- * against), so both are carried through unchanged.
563
+ * Maps each provider ID to its authoring-time config variant. The three language-model providers
564
+ * get narrowed model unions; DeepL has no model to narrow, and `openai-compatible` deliberately
565
+ * keeps a free-form model string because the endpoint is a local or self-hosted server whose model
566
+ * list the SDK cannot know ahead of time.
487
567
  */
488
568
  type AuthoringProviderVariant = {
569
+ /** Anthropic, with `model` narrowed to the Claude model IDs. */
489
570
  anthropic: AuthoringVariant<"anthropic", AnthropicModel>;
571
+ /** OpenAI, with `model` narrowed to the OpenAI chat model IDs. */
490
572
  openai: AuthoringVariant<"openai", OpenAiModel>;
573
+ /** Gemini, with `model` narrowed to the Gemini model IDs. */
491
574
  gemini: AuthoringVariant<"gemini", GeminiModel>;
575
+ /** DeepL, which takes no model because it is a machine-translation API rather than a language model. */
492
576
  deepl: Extract<ProviderConfig, {
493
577
  id: "deepl";
494
578
  }>;
579
+ /** A local or self-hosted OpenAI-compatible endpoint, whose model stays a free-form string. */
495
580
  "openai-compatible": Extract<ProviderConfig, {
496
581
  id: "openai-compatible";
497
582
  }>;
498
583
  };
499
584
  /**
500
- * The authoring view of the whole config for a provider id: a {@link VerbatraConfigInput} whose
501
- * `provider` is that id's single authoring variant, so `options.model` offers only that provider's
502
- * models. `glossary` stays the as-authored union (inline record or file path); resolution happens only
503
- * in `loadConfig`. When `TId` defaults to `ProviderId`, `provider` is the full authoring union. Every
504
- * value here is assignable to {@link VerbatraConfigInput}, since a model literal is a subtype of
505
- * `string`.
585
+ * A config as authored for one specific provider: the ordinary {@link VerbatraConfigInput} shape,
586
+ * but with `provider` narrowed to that provider's variant so its model IDs autocomplete and a model
587
+ * belonging to another vendor is a type error.
588
+ *
589
+ * This is the parameter type of each {@link defineConfig} overload.
506
590
  */
507
591
  type AuthoringConfigFor<TId extends ProviderId = ProviderId> = Omit<VerbatraConfigInput, "provider"> & {
592
+ /** The provider block, narrowed to the variant for `TId`. */
508
593
  provider: AuthoringProviderVariant[TId];
509
594
  };
510
595
  /**
511
- * The authoring view of the whole config across every provider (the `TId = ProviderId`
512
- * case of {@link AuthoringConfigFor}): identical to {@link VerbatraConfigInput} except that
513
- * `provider` offers per-provider model completions.
596
+ * A config authored for any supported provider: {@link AuthoringConfigFor} left un-narrowed, which
597
+ * is the type of the fallback {@link defineConfig} overload.
514
598
  */
515
599
  type AuthoringConfig = AuthoringConfigFor;
516
600
 
517
601
  /**
518
- * Identity helper for authoring a typed `verbatra.config.ts`. It returns its argument unchanged; its
519
- * only purpose is full type inference and editor autocomplete on the config object, including
520
- * completion of the selected provider's known model literals.
602
+ * Types a `verbatra.config.ts` while you author it. The call is a pure identity function at
603
+ * runtime: its only job is to give your editor the type of the config object so that keys are
604
+ * completed, unknown keys are rejected, and the model name is checked against the models the
605
+ * selected provider actually offers.
606
+ *
607
+ * This overload narrows to the Anthropic provider, so `provider.options.model` completes to the
608
+ * Claude model IDs and rejects a model belonging to another vendor.
609
+ *
610
+ * Validation at load time is a separate step: {@link loadConfig} still parses the file against
611
+ * {@link verbatraConfigSchema}, so a config that type-checks can still fail validation.
521
612
  *
522
- * The model restriction is a static authoring constraint only: `loadConfig` validates `model` as a
523
- * non-empty string, so a model the installed provider SDK does not yet list is flagged in the editor
524
- * but still runs. Likewise, `glossary` accepts either an inline record or a file path here; `loadConfig`
525
- * resolves a path to a record before the translation flow ever sees it.
613
+ * @param config - The config object, narrowed to the Anthropic provider.
614
+ * @returns The same object, typed as {@link VerbatraConfigInput}.
615
+ *
616
+ * @example
617
+ * ```ts
618
+ * import { defineConfig } from "@verbatra/sdk";
526
619
  *
527
- * @param config - The verbatra configuration object.
528
- * @returns The same config, typed as {@link VerbatraConfigInput}.
620
+ * export default defineConfig({
621
+ * sourceLocale: "en",
622
+ * targetLocales: ["de", "fr"],
623
+ * format: "i18next-json",
624
+ * files: { pattern: "locales/{locale}/common.json" },
625
+ * provider: {
626
+ * id: "anthropic",
627
+ * options: { model: "claude-sonnet-4-6", maxTokens: 4096 },
628
+ * },
629
+ * });
630
+ * ```
529
631
  */
530
632
  declare function defineConfig(config: AuthoringConfigFor<"anthropic">): VerbatraConfigInput;
633
+ /**
634
+ * Types a `verbatra.config.ts` narrowed to the OpenAI provider, so `provider.options.model`
635
+ * completes to the OpenAI chat model IDs.
636
+ *
637
+ * @param config - The config object, narrowed to the OpenAI provider.
638
+ * @returns The same object, typed as {@link VerbatraConfigInput}.
639
+ */
531
640
  declare function defineConfig(config: AuthoringConfigFor<"openai">): VerbatraConfigInput;
641
+ /**
642
+ * Types a `verbatra.config.ts` narrowed to the Gemini provider, so `provider.options.model`
643
+ * completes to the Gemini model IDs.
644
+ *
645
+ * @param config - The config object, narrowed to the Gemini provider.
646
+ * @returns The same object, typed as {@link VerbatraConfigInput}.
647
+ */
532
648
  declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraConfigInput;
649
+ /**
650
+ * Types a `verbatra.config.ts` narrowed to the DeepL provider. DeepL is a machine-translation API
651
+ * rather than a language model, so its options carry an optional glossary ID instead of a model.
652
+ *
653
+ * @param config - The config object, narrowed to the DeepL provider.
654
+ * @returns The same object, typed as {@link VerbatraConfigInput}.
655
+ */
533
656
  declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfigInput;
657
+ /**
658
+ * Types a `verbatra.config.ts` for any supported provider. This is the fallback overload, and the
659
+ * one that serves `openai-compatible`, where the model is a free-form string because the endpoint
660
+ * is a local or self-hosted server whose model list the SDK cannot know.
661
+ *
662
+ * @param config - The config object, for any provider.
663
+ * @returns The same object, typed as {@link VerbatraConfigInput}.
664
+ */
534
665
  declare function defineConfig(config: AuthoringConfig): VerbatraConfigInput;
535
666
 
536
- /** Where a config's resolved glossary came from: absent, inline, or a file at the recorded path. */
667
+ /**
668
+ * Where a loaded config's glossary came from, reported by {@link loadConfigWithMeta}. The resolved
669
+ * {@link VerbatraConfig} holds only the final term map, so this is the one place a tool can tell an
670
+ * inline glossary from one read out of a file, and name that file.
671
+ */
537
672
  type GlossaryProvenance = {
673
+ /** The config declared no glossary. */
538
674
  readonly source: "none";
539
675
  } | {
676
+ /** The glossary was written inline in the config as a term map. */
540
677
  readonly source: "inline";
541
678
  } | {
679
+ /** The glossary was read from a JSON file the config pointed at. */
542
680
  readonly source: "file";
681
+ /** The absolute path the glossary was read from. */
543
682
  readonly path: string;
544
683
  };
545
684
 
546
- /** Where {@link loadConfig} should obtain the configuration. The three sources are alternatives. */
685
+ /** Options for {@link loadConfig} and {@link loadConfigWithMeta}. */
547
686
  interface LoadConfigOptions {
548
- /** Directory to start the search from. Defaults to the current working directory. */
687
+ /** Directory to search from, and the base for relative paths. Defaults to the process working directory. */
549
688
  readonly cwd?: string;
550
689
  /**
551
- * A pre-resolved config object (e.g. one passed in code) to validate instead of
552
- * searching the file system. Still validated with zod, exactly like a loaded file.
690
+ * A config object to validate directly instead of reading any file. Takes precedence over
691
+ * `configPath` and over searching. Useful for embedding verbatra in a tool that already holds the
692
+ * configuration in memory.
553
693
  */
554
694
  readonly configOverride?: unknown;
555
- /**
556
- * An explicit config file to load instead of searching. A relative path resolves against `cwd`; an
557
- * absolute path is used as given. Parsed and zod-validated exactly like a searched file: a missing
558
- * file is `CONFIG_NOT_FOUND`, a present but invalid one is `CONFIG_INVALID`. Takes precedence over
559
- * search, but `configOverride` takes precedence over it.
560
- */
695
+ /** An explicit config file to load, bypassing the search. A missing file is an error rather than a fallback to search. */
561
696
  readonly configPath?: string;
562
- /** The file-system seam a glossary file path is read through. Defaults to {@link defaultFs}. */
697
+ /** File-system port used to read the glossary file. Defaults to the real file system. */
563
698
  readonly fs?: SdkFs;
564
699
  }
565
- /** Where the loaded config came from: an in-memory override, one explicit file, or a search result. */
700
+ /**
701
+ * Where a loaded config came from, reported by {@link loadConfigWithMeta}. A tool can use it to
702
+ * show which file is in effect, or to tell a real file apart from an in-memory override.
703
+ */
566
704
  type ConfigSource = {
705
+ /**
706
+ * `search` means cosmiconfig found the file by walking up from the working directory;
707
+ * `explicit` means the caller named it through `configPath`.
708
+ */
567
709
  readonly kind: "search" | "explicit";
710
+ /** The absolute path of the config file that was loaded. */
568
711
  readonly filepath: string;
569
712
  } | {
713
+ /** The config came from `configOverride`, so no file was read. */
570
714
  readonly kind: "override";
571
715
  };
572
- /**
573
- * The result of {@link loadConfigWithMeta}: the resolved config, where it was loaded from, and where its
574
- * glossary came from. `filepath` (when present) and a `glossary` file path are always absolute.
575
- */
716
+ /** A validated config together with the provenance of the config itself and of its glossary. */
576
717
  interface LoadedConfig {
718
+ /** The validated, fully resolved config, with any glossary file already read into a term map. */
577
719
  readonly config: VerbatraConfig;
720
+ /** Where the config itself came from. */
578
721
  readonly source: ConfigSource;
722
+ /** Where the glossary came from, if the config declared one. */
579
723
  readonly glossary: GlossaryProvenance;
580
724
  }
581
725
  /**
582
- * Load and validate the verbatra configuration, returning provenance alongside it: which source it was
583
- * loaded from ({@link ConfigSource}), and whether its glossary is absent, inline, or resolved from a
584
- * file ({@link GlossaryProvenance}). {@link loadConfig} is a thin wrapper that returns only the
585
- * `config` field; use this directly when the provenance is needed (for example, to display it or to
586
- * re-resolve a glossary file relative to the same base directory).
726
+ * Loads and validates the project config, and additionally reports where it came from and where its
727
+ * glossary came from. Use this over {@link loadConfig} when a tool needs to show the user which
728
+ * config file is in effect, or to distinguish an inline glossary from a glossary file.
587
729
  *
588
- * @param options - Where/what to load: `cwd`, an in-memory `configOverride`, an explicit `configPath`,
589
- * or a `fs` seam override.
590
- * @returns The resolved {@link VerbatraConfig}, its {@link ConfigSource}, and its {@link GlossaryProvenance}.
591
- * @throws {@link SdkError} `CONFIG_NOT_FOUND`: no config was found by search, or the explicit `configPath`
592
- * does not exist.
593
- * @throws {@link SdkError} `CONFIG_INVALID`: a config was found but is unparseable or fails validation, or
594
- * its glossary file path could not be read as a valid, UTF-8, flat string record (a raw error never
595
- * escapes).
730
+ * Resolution order is: an explicit `configOverride`, then an explicit `configPath`, then a
731
+ * cosmiconfig search upward from `cwd` across `verbatra.config.ts`, the `.verbatrarc` family, and a
732
+ * `verbatra` property in `package.json`. A `configPath` that does not exist is an error rather than
733
+ * a fallback to searching, so a typo in a path never silently loads a different project's config.
734
+ *
735
+ * A glossary given as a path is read and validated here, so the returned config always carries a
736
+ * resolved term map.
737
+ *
738
+ * @param options - Where and how to look for the config.
739
+ * @returns The validated config with its config-source and glossary provenance.
740
+ *
741
+ * @throws {@link SdkError} `CONFIG_NOT_FOUND`: no config was found by search, or the explicit
742
+ * `configPath` does not exist.
743
+ * @throws {@link SdkError} `CONFIG_INVALID`: the config could not be loaded or fails validation, or
744
+ * its glossary file is missing, oversized, not UTF-8, not valid JSON, or not a flat string map.
596
745
  */
597
746
  declare function loadConfigWithMeta(options?: LoadConfigOptions): Promise<LoadedConfig>;
598
747
  /**
599
- * Load and validate the verbatra configuration. Supports a code-defined
600
- * verbatra.config.ts and file-based configs (.verbatrarc.json/.yaml, package.json
601
- * property) through cosmiconfig + cosmiconfig-typescript-loader. Multiple sources ->
602
- * first-found-wins by cosmiconfig precedence. Any failure is a structured SdkError;
603
- * a raw zod error is never thrown upward and an unvalidated config never proceeds.
604
- *
605
- * Precedence: `configOverride` (validate in-memory) > `configPath` (load one explicit file) > search.
606
- *
607
- * A `glossary` given as a file path is read, parsed, and validated at load time (see
608
- * {@link loadConfigWithMeta} for its provenance); a relative path resolves against the loaded config
609
- * file's directory, or against `cwd` for a `configOverride`. Watch mode does not re-resolve a glossary
610
- * file on later edits; restart to pick up a change.
611
- *
612
- * @param options - Where/what to load: `cwd`, an in-memory `configOverride`, or an explicit `configPath`.
613
- * @returns The validated {@link VerbatraConfig}.
614
- * @throws {@link SdkError} `CONFIG_NOT_FOUND`: no config was found by search, or the explicit `configPath`
615
- * does not exist.
616
- * @throws {@link SdkError} `CONFIG_INVALID`: a config was found but is unparseable or fails validation, or
617
- * its glossary file path could not be resolved (a raw error never escapes).
748
+ * Loads and validates the project config. This is the normal starting point for every SDK flow:
749
+ * pass the result to {@link translate}, {@link check}, {@link diff}, or any other entry point.
750
+ *
751
+ * Resolution order is an explicit `configOverride`, then an explicit `configPath`, then a
752
+ * cosmiconfig search upward from `cwd`. A glossary declared as a file path is read and validated
753
+ * here, so the returned {@link VerbatraConfig} always carries a resolved term map.
754
+ *
755
+ * Reach for {@link loadConfigWithMeta} when you also need to know which file was loaded.
756
+ *
757
+ * @param options - Where and how to look for the config.
758
+ * @returns The validated, fully resolved config.
759
+ *
760
+ * @throws {@link SdkError} `CONFIG_NOT_FOUND`: no config was found by search, or the explicit
761
+ * `configPath` does not exist.
762
+ * @throws {@link SdkError} `CONFIG_INVALID`: the config could not be loaded or fails validation, or
763
+ * its glossary file is missing, oversized, not UTF-8, not valid JSON, or not a flat string map.
764
+ *
618
765
  * @example
619
766
  * ```ts
620
767
  * import { loadConfig, translate } from "@verbatra/sdk";
621
768
  *
622
- * // Search upward from the cwd (verbatra.config.ts, .verbatrarc.json, or a package.json "verbatra" key):
623
769
  * const config = await loadConfig();
624
- * // Or load one explicit file (relative resolves against cwd; absolute as given):
625
- * // const config = await loadConfig({ configPath: "verbatra.config.ts" });
626
- *
627
770
  * const summary = await translate({ config });
771
+ * console.log(`${summary.succeeded.length} locales up to date`);
628
772
  * ```
629
773
  */
630
774
  declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig>;
631
775
 
632
776
  /**
633
- * Structured, secret-free error codes for the SDK boundaries. A key never appears in
634
- * any message: provider/adapter/core errors are already secret-free, and the SDK never
635
- * reads or holds a key. Each names a distinct boundary:
777
+ * The stable, structured failure codes the SDK throws. Branch on {@link SdkError.code} rather than
778
+ * on a message; the messages are written for humans and are not part of the contract.
779
+ *
780
+ * A code never carries a secret. API keys are read from the environment by the providers alone, so
781
+ * the SDK never holds one, and provider, adapter, and core errors are secret-free before the SDK
782
+ * wraps them.
636
783
  *
637
- * - `CONFIG_NOT_FOUND`: no config was found by search, or an explicit `configPath` does not exist
638
- * (thrown by `loadConfig`).
784
+ * - `CONFIG_NOT_FOUND`: no config file was found by search, or an explicit `configPath` does not
785
+ * exist. Thrown by {@link loadConfig} and {@link loadConfigWithMeta}.
639
786
  * - `CONFIG_INVALID`: a config was found but is unparseable or fails validation, or its glossary
640
- * file path could not be resolved (thrown by `loadConfig`).
641
- * - `UNKNOWN_FORMAT`: no adapter is registered for the configured format (thrown by every entry
642
- * point that selects an adapter, before any file is read).
643
- * - `UNKNOWN_LOCALE`: a requested locale is not among the configured target locales (thrown via
644
- * the shared locale selection by `check`, `diff`, `keyIntegrity`, `lockState`, `exportWorkbook`,
645
- * `keyValue`, `editEntry`, and `retranslateEntry`).
646
- * - `UNKNOWN_KEY`: a requested key is not among the source resource's own keys (thrown by
647
- * `keyValue`, `editEntry`, and `retranslateEntry`).
648
- * - `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw; wraps the provider's own error,
649
- * including a missing `*_API_KEY` reported as `MISSING_API_KEY` (thrown by non-dry-run
650
- * `translate` and by `retranslateEntry`).
651
- * - `SOURCE_UNREADABLE`: the source locale file is absent (thrown by every entry point that reads
652
- * the source, and by `watch` at startup).
653
- * - `SOURCE_INVALID`: the source locale file could not be read or parsed; wraps the adapter read
654
- * error (thrown by every entry point that reads the source).
655
- * - `LOCK_FILE_INVALID`: the lock-file is present but corrupt, oversized, or at an unsupported
656
- * version (thrown wherever the lock-file is read or updated: `translate`, `check`, `diff`,
657
- * `keyIntegrity`, `lockState`, `loadLockFile`, `exportWorkbook`, `importWorkbook`, `editEntry`,
658
- * and `retranslateEntry`).
659
- * - `LOCK_CONTENDED`: a locale's write lock (`withLocaleWriteLock`) could not be acquired before
660
- * its timeout elapsed, because another process is holding it (or an orphaned lock file was left
661
- * behind by a killed process); the message names the lock file's path (thrown by `translate`,
662
- * `importWorkbook`, `editEntry`, and `retranslateEntry`).
787
+ * file could not be resolved or parsed. Thrown by {@link loadConfig} and
788
+ * {@link loadConfigWithMeta}. {@link importWorkbook} does not throw it: when a handoff sheet or
789
+ * file names a locale that is not a configured target locale, it records this code on that
790
+ * locale's {@link LocaleSummary} instead.
791
+ * - `UNKNOWN_FORMAT`: no adapter is registered for the configured format. Thrown by every entry
792
+ * point that selects an adapter, before any file is read.
793
+ * - `UNKNOWN_LOCALE`: a requested locale is not among the configured target locales. Thrown through
794
+ * the shared locale selection by {@link check}, {@link diff}, {@link keyIntegrity},
795
+ * {@link lockState}, {@link exportWorkbook}, {@link keyValue}, {@link editEntry}, and
796
+ * {@link retranslateEntry}.
797
+ * - `UNKNOWN_KEY`: the requested key is not present in the source resource. Thrown by
798
+ * {@link keyValue}, {@link editEntry}, and {@link retranslateEntry}.
799
+ * - `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw. Wraps the provider's own error,
800
+ * including a missing `*_API_KEY` environment variable. Thrown by a non-dry-run
801
+ * {@link translate} and by {@link retranslateEntry}.
802
+ * - `SOURCE_UNREADABLE`: the source locale file is absent. Thrown by every entry point that reads
803
+ * the source, including {@link importWorkbook}, and by {@link watch} at startup.
804
+ * {@link importWorkbook} additionally throws it when the handoff file itself is missing.
805
+ * - `SOURCE_INVALID`: the source locale file, or an interchange file, could not be parsed. Wraps
806
+ * the adapter or reader error.
807
+ * - `LOCK_FILE_INVALID`: the lock-file exists but is corrupt, oversized, or at an unsupported
808
+ * version. Thrown wherever the lock-file is read or updated: {@link translate}, {@link check},
809
+ * {@link diff}, {@link keyIntegrity}, {@link lockState}, {@link loadLockFile},
810
+ * {@link exportWorkbook}, {@link importWorkbook}, {@link editEntry}, and
811
+ * {@link retranslateEntry}.
812
+ * - `LOCK_CONTENDED`: a locale's write lock could not be acquired before its timeout elapsed,
813
+ * because another process holds it or a killed process left the lock file behind. The message
814
+ * names the lock file's path. Thrown by {@link editEntry} and {@link retranslateEntry}, which
815
+ * act on one locale. {@link translate} and {@link importWorkbook} do not throw it: they record it
816
+ * on the contended locale's {@link LocaleSummary} and carry on with the other locales.
663
817
  * - `LOCALE_LAYOUT_INVALID`: the configured `files.pattern` and `files.localeStyle` cannot be
664
- * combined (a segment style needs the `{locale}` token to stand alone between separators), or the
665
- * declared style has no valid path spelling for a configured locale (`zh-Hans` under `posix`, for
666
- * instance), or a locale expands to something that is not a single path segment. Thrown by
667
- * `createLocalePathResolver`, and so by every entry point that resolves a locale to a path, before
668
- * any file is read and before any provider call.
669
- * - `LOCALE_PATH_COLLISION`: two configured locales resolve to the same absolute file path, which
670
- * would make the path-to-locale direction meaningless and would let two concurrent locale workers
671
- * race on one file (thrown by `createLocalePathResolver`, at the same point as the above).
672
- * - `CONCURRENCY_INVALID`: the `concurrency` input is not an integer of at least 1 (thrown by
673
- * `translate` and, per run, by `watch`, before any locale runs).
674
- * - `CONCURRENCY_BUDGET_CONFLICT`: a live run requested `concurrency` greater than 1 while a token
675
- * budget (`maxTokens`) is configured; the two are mutually exclusive because concurrency makes the
676
- * budget's stop guarantee nondeterministic (thrown by `translate` before any provider call; a dry
677
- * run is exempt, since it never folds usage into or consults the budget tracker, so the conflict
678
- * cannot arise).
679
- * - `LOCALE_FAILED` (NOT thrown): the fallback `code` recorded on a failed `LocaleSummary` when a
680
- * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction
681
- * on `translate`.
818
+ * combined, or the style has no valid path spelling for a configured locale. Thrown by
819
+ * {@link createLocalePathResolver}, and so by every entry point that maps a locale to a path,
820
+ * before any file is read and before any provider call.
821
+ * - `LOCALE_PATH_COLLISION`: two configured locales resolve to the same absolute path, which would
822
+ * make the path-to-locale direction ambiguous and let two locale workers race on one file. Thrown
823
+ * at the same point as `LOCALE_LAYOUT_INVALID`.
824
+ * - `CONCURRENCY_INVALID`: the `concurrency` input is not an integer of at least 1. Thrown by
825
+ * {@link translate} before any locale runs, and by {@link watch} once at startup, before any
826
+ * watching begins, since the value is fixed for the session rather than re-read per run.
827
+ * - `CONCURRENCY_BUDGET_CONFLICT`: a live run requested a `concurrency` above 1 while a token
828
+ * budget is configured. The two are mutually exclusive because concurrency makes the budget's
829
+ * stop guarantee nondeterministic. A dry run is exempt, since it never consults the budget.
830
+ * - `LOCALE_FAILED`: never thrown. It is the fallback code recorded on a failed
831
+ * {@link LocaleSummary} when a per-locale failure carries no code of its own.
682
832
  */
683
833
  type SdkErrorCode = "CONFIG_NOT_FOUND" | "CONFIG_INVALID" | "UNKNOWN_FORMAT" | "UNKNOWN_LOCALE" | "UNKNOWN_KEY" | "PROVIDER_CONSTRUCTION_FAILED" | "SOURCE_UNREADABLE" | "SOURCE_INVALID" | "LOCK_FILE_INVALID" | "LOCK_CONTENDED" | "LOCALE_LAYOUT_INVALID" | "LOCALE_PATH_COLLISION" | "CONCURRENCY_INVALID" | "CONCURRENCY_BUDGET_CONFLICT" | "LOCALE_FAILED";
684
- /** The single structured error the SDK throws or records. Never carries a secret. */
834
+ /**
835
+ * The single structured error the SDK throws. Every whole-run failure surfaces as an `SdkError`
836
+ * carrying a stable {@link SdkErrorCode}; per-locale failures, provider notices, and integrity
837
+ * findings are reported as data on the {@link RunSummary} instead of being thrown.
838
+ *
839
+ * An `SdkError` never carries a secret in its message.
840
+ */
685
841
  declare class SdkError extends Error {
686
- /** The stable {@link SdkErrorCode} for this failure; branch on this, not the message. */
842
+ /** The stable {@link SdkErrorCode} for this failure. Branch on this, not on the message. */
687
843
  readonly code: SdkErrorCode;
688
844
  /**
689
845
  * @param code - The stable failure code.
690
- * @param message - A fixed, secret-free message; the SDK never holds a key to put here.
846
+ * @param message - A human-readable description of the failure. Never contains a secret.
691
847
  */
692
848
  constructor(code: SdkErrorCode, message: string);
693
849
  }
694
850
 
695
- /** Zod schema accepting exactly one of {@link SUPPORTED_FORMATS}. */
851
+ /**
852
+ * Zod schema accepting exactly one of the eight source formats verbatra can read and write. Each
853
+ * member names the ecosystem it round-trips, not just a file extension, because several of them share
854
+ * one: `i18next-json` is i18next's nested JSON with its plural key suffixes, `vue-i18n-json` is Vue
855
+ * I18n's JSON with its pipe-separated plural values, `next-intl-json` is next-intl's ICU-message JSON,
856
+ * and `ngx-translate-json` is ngx-translate's nested JSON. The remaining four are `xliff` (the XLIFF
857
+ * interchange XML), `yaml` (plain nested YAML), `arb` (Flutter's Application Resource Bundle), and
858
+ * `properties` (Java and Spring `.properties` files).
859
+ *
860
+ * The set is closed: a format outside it cannot be represented, so adding one means adding a member
861
+ * here and an adapter that claims it.
862
+ */
696
863
  declare const supportedFormatSchema: z.ZodEnum<{
697
864
  "i18next-json": "i18next-json";
698
865
  "vue-i18n-json": "vue-i18n-json";
@@ -703,12 +870,23 @@ declare const supportedFormatSchema: z.ZodEnum<{
703
870
  arb: "arb";
704
871
  properties: "properties";
705
872
  }>;
706
- /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
873
+ /** One of the eight supported source formats; the inferred type of {@link supportedFormatSchema}. */
707
874
  type SupportedFormat = z.infer<typeof supportedFormatSchema>;
708
875
 
709
876
  /**
710
- * A single, format-neutral translation unit. Placeholders are supplied already
711
- * extracted; core never derives them from the value.
877
+ * A single, format-neutral translation unit. Placeholders are supplied already extracted; core never
878
+ * derives them from the value, so an adapter that reads a file is the one authority on what counts as
879
+ * a placeholder in that format.
880
+ *
881
+ * The fields are: `key`, the entry's identifier within its namespace, non-empty and already flattened
882
+ * to the dotted path an adapter produced; `namespace`, the namespace the entry belongs to, empty when
883
+ * the format has no namespacing; `value`, the translatable string itself, which is untrusted input and
884
+ * may be empty; `description` and `meaning`, both optional free-form context that a provider is told to
885
+ * use only for disambiguation and never to translate or echo back; `placeholders`, the format's
886
+ * placeholder tokens in document order, used for the output integrity check and to decide which entries
887
+ * a placeholder-blind provider must withhold; and `isPlural`, whether the entry carries plural forms
888
+ * (an i18next plural key suffix, a vue-i18n pipe-separated value, or an ICU plural), which the adapter
889
+ * decides and which feeds the content hash.
712
890
  */
713
891
  declare const translationEntrySchema: z.ZodObject<{
714
892
  key: z.ZodString;
@@ -719,23 +897,32 @@ declare const translationEntrySchema: z.ZodObject<{
719
897
  placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
720
898
  isPlural: z.ZodBoolean;
721
899
  }, z.core.$strip>;
722
- /** The validated shape of one translation unit; the inferred type of {@link translationEntrySchema}. */
900
+ /**
901
+ * The validated shape of one translation unit; the inferred type of {@link translationEntrySchema},
902
+ * made deeply readonly. See that schema for what each field carries.
903
+ */
723
904
  type TranslationEntry = Readonly<z.infer<typeof translationEntrySchema>>;
724
- /** All translation entries for one locale and namespace, keyed by entry key. */
905
+
906
+ /**
907
+ * All translation entries for one locale and namespace, keyed by entry key. This is the neutral
908
+ * intermediate representation every adapter reads into and writes back out of, so nothing downstream
909
+ * of an adapter parses a format's own syntax.
910
+ */
725
911
  interface LocaleResource {
726
912
  /** The locale these entries belong to (for example, "en" or "de"). */
727
913
  readonly locale: string;
728
- /** The namespace these entries belong to. */
914
+ /** The namespace these entries belong to; empty when the format has no namespacing. */
729
915
  readonly namespace: string;
730
- /** The source format the resource came from, for round-trip fidelity. */
916
+ /** The source format the resource came from, retained for round-trip fidelity on write. */
731
917
  readonly format: SupportedFormat;
732
- /** Entries addressable by key. */
918
+ /** Entries addressable by key, in the order the source file defined them. */
733
919
  readonly entries: ReadonlyMap<string, TranslationEntry>;
734
920
  }
735
921
 
736
922
  /**
737
923
  * Result of comparing source placeholders against translated-output placeholders as MULTISETS
738
- * (counts matter). Always returned (never thrown) for an ordinary mismatch.
924
+ * (counts matter, so dropping one of two occurrences of the same token is still a miss). Always
925
+ * returned, never thrown: a mismatch is data the caller decides what to do about, not a failure.
739
926
  */
740
927
  interface PlaceholderIntegrityResult {
741
928
  /** True when nothing is missing or extra: the multisets match, regardless of order. */
@@ -744,17 +931,21 @@ interface PlaceholderIntegrityResult {
744
931
  readonly missing: readonly string[];
745
932
  /** Occurrences present in the translation but absent from source, one per surplus occurrence. Sorted. */
746
933
  readonly extra: readonly string[];
747
- /** Same multiset present in both, but in a different order. */
934
+ /** Same multiset present in both, but in a different order. Never true alongside a mismatch. */
748
935
  readonly reordered: boolean;
749
936
  }
750
937
 
751
938
  /**
752
- * The result of reading a file into core's intermediate representation.
939
+ * The result of reading a file into core's intermediate representation. The two diagnostic lists are
940
+ * reported as data rather than thrown, so one bad leaf never fails a whole read.
753
941
  */
754
942
  interface ReadResult {
755
943
  /** The parsed locale resource. */
756
944
  readonly resource: LocaleResource;
757
- /** Keys whose values are invalid for the format's message syntax. Empty for non-ICU formats. */
945
+ /**
946
+ * Keys whose values are invalid for the format's message syntax. Empty for a format with no
947
+ * message-syntax check, which treats every value as valid.
948
+ */
758
949
  readonly invalidIcuKeys: readonly string[];
759
950
  /**
760
951
  * Dotted paths of leaves that were present in the source but excluded because they are not
@@ -768,22 +959,14 @@ interface ReadResult {
768
959
  * The contract every format adapter implements. A new format attaches by implementing this interface
769
960
  * and registering it in an {@link AdapterRegistry}.
770
961
  *
771
- * For a JSON-family format, implement via {@link createJsonFileAdapter} rather than from scratch. A
772
- * non-JSON format implements this interface directly and first needs its format added to core's
773
- * `SupportedFormat`.
774
- *
775
- * @example
776
- * ```ts
777
- * const registry = createDefaultRegistry();
778
- * const resolution = registry.resolve("locales/en.json", { format: "i18next-json" });
779
- * if (resolution.status === "resolved") {
780
- * const { resource } = await resolution.adapter.read("locales/en.json", "en");
781
- * await resolution.adapter.write(resource, "locales/en.json");
782
- * }
783
- * ```
962
+ * Implement it through one of `@verbatra/format-adapters`' shared factories rather than from
963
+ * scratch: `createTreeFileAdapter` for a nested-tree format (with `createJsonFileAdapter` as its
964
+ * JSON specialization) and `createFlatFileAdapter` for a flat key/value format. A format that fits
965
+ * neither shape implements this interface directly, and either way it first needs its member added
966
+ * to core's {@link SupportedFormat}.
784
967
  */
785
968
  interface FormatAdapter {
786
- /** The single format this adapter handles (a `SupportedFormat` from core). */
969
+ /** The single format this adapter handles (a {@link SupportedFormat} from core). */
787
970
  readonly format: SupportedFormat;
788
971
  /**
789
972
  * Detect whether this adapter can handle a file, by path extension and an optional content sample.
@@ -801,7 +984,7 @@ interface FormatAdapter {
801
984
  * @param filePath - The file to read.
802
985
  * @param locale - The locale to tag the resource with.
803
986
  * @returns The parsed resource and the keys whose values are invalid for the format.
804
- * @throws {@link AdapterError} when the content is malformed, oversized, or structurally invalid
987
+ * @throws `AdapterError` when the content is malformed, oversized, or structurally invalid
805
988
  * (the implementation names the specific codes). A missing or unopenable path instead rejects with
806
989
  * the underlying filesystem error.
807
990
  */
@@ -811,7 +994,7 @@ interface FormatAdapter {
811
994
  *
812
995
  * @param resource - The resource to serialize.
813
996
  * @param filePath - The destination file.
814
- * @throws {@link AdapterError} if the resource cannot be represented in the format; rejects with the
997
+ * @throws `AdapterError` if the resource cannot be represented in the format; rejects with the
815
998
  * underlying filesystem error on a write failure.
816
999
  */
817
1000
  write(resource: LocaleResource, filePath: string): Promise<void>;
@@ -823,8 +1006,9 @@ interface FormatAdapter {
823
1006
  */
824
1007
  extractPlaceholders(value: string): readonly string[];
825
1008
  /**
826
- * Validate a single value against the format's message syntax before it is written. Non-ICU formats
827
- * (i18next, vue-i18n, ngx-translate) treat every value as valid and return true.
1009
+ * Validate a single value against the format's message syntax before it is written. Only the
1010
+ * ICU-message formats carry a real check today (next-intl and ARB); every other adapter has no
1011
+ * message syntax to violate and returns true for any value.
828
1012
  *
829
1013
  * @param value - The candidate translated value to validate.
830
1014
  * @returns True when the value is valid for the format's message syntax. Does not throw; an
@@ -836,7 +1020,7 @@ interface FormatAdapter {
836
1020
  * When present, callers use it instead of independently extracting each side's placeholders with
837
1021
  * {@link extractPlaceholders} and diffing the flat lists, since flattening a plural/select value loses
838
1022
  * which branch a placeholder came from. Absent for every non-ICU adapter, which is compared via
839
- * `extractPlaceholders` plus `checkPlaceholders` as before.
1023
+ * `extractPlaceholders` plus a flat multiset check as before.
840
1024
  *
841
1025
  * @param sourceValue - The source value.
842
1026
  * @param targetValue - The translated value to check against it.
@@ -845,34 +1029,56 @@ interface FormatAdapter {
845
1029
  comparePlaceholders?(sourceValue: string, targetValue: string): PlaceholderIntegrityResult;
846
1030
  }
847
1031
 
848
- /** Outcome of resolving an adapter for a file. Structured; never throws. */
1032
+ /**
1033
+ * Outcome of resolving an adapter for a file. Structured, never thrown: an unresolvable file is a
1034
+ * status the caller branches on, not an exception.
1035
+ *
1036
+ * `resolved` carries the single {@link FormatAdapter} to use. `no-match` means nothing claimed the
1037
+ * file, and reports the formats that were tried (every registered format for a detection attempt, or
1038
+ * just the one requested when an explicit format was given). `ambiguous` means more than one adapter
1039
+ * claimed the file, which is routine since every JSON adapter claims `.json`; it reports the competing
1040
+ * candidates instead of guessing, and the caller resolves it by naming a format explicitly.
1041
+ */
849
1042
  type AdapterResolution = {
1043
+ /** Exactly one adapter claimed the file. */
850
1044
  readonly status: "resolved";
1045
+ /** The adapter to read and write the file with. */
851
1046
  readonly adapter: FormatAdapter;
852
1047
  } | {
1048
+ /** No registered adapter claimed the file. */
853
1049
  readonly status: "no-match";
1050
+ /** The file that could not be matched. */
854
1051
  readonly filePath: string;
1052
+ /**
1053
+ * The formats that were tried: every registered format for a detection attempt, or just the
1054
+ * one requested when an explicit format was given.
1055
+ */
855
1056
  readonly triedFormats: readonly SupportedFormat[];
856
1057
  } | {
1058
+ /** More than one adapter claimed the file, so detection cannot choose between them. */
857
1059
  readonly status: "ambiguous";
1060
+ /** The file that several adapters claimed. */
858
1061
  readonly filePath: string;
1062
+ /** The competing formats. Resolve the ambiguity by naming one of them explicitly. */
859
1063
  readonly candidates: readonly SupportedFormat[];
860
1064
  };
861
1065
  /** Options for {@link AdapterRegistry.resolve}: select a format explicitly, or aid detection. */
862
1066
  interface ResolveOptions {
863
- /** A content sample to aid detection. */
1067
+ /** A leading content sample to aid detection. Ignored when `format` is given. */
864
1068
  readonly sample?: string;
865
1069
  /** Bypass detection and select this format explicitly. */
866
1070
  readonly format?: SupportedFormat;
867
1071
  }
868
1072
  /**
869
- * Holds the registered adapters and resolves one for a file. Open for extension:
870
- * adapters attach through register without changing resolution logic.
1073
+ * Holds the registered adapters and resolves one for a file. Open for extension: adapters attach
1074
+ * through {@link AdapterRegistry.register} without changing resolution logic, and resolution never
1075
+ * throws.
871
1076
  */
872
1077
  declare class AdapterRegistry {
873
1078
  private readonly adapters;
874
1079
  /**
875
- * Register an adapter.
1080
+ * Register an adapter. Registration order is the order detection consults them, and a format may be
1081
+ * registered more than once, in which case an explicit-format lookup takes the first.
876
1082
  *
877
1083
  * @param adapter - The adapter to add.
878
1084
  * @returns This registry, for chaining.
@@ -880,11 +1086,6 @@ declare class AdapterRegistry {
880
1086
  register(adapter: FormatAdapter): this;
881
1087
  private formats;
882
1088
  private resolveByFormat;
883
- /**
884
- * Resolve by asking every registered adapter's `canHandle`. Several adapters can claim one file
885
- * (all JSON adapters claim `.json`), so more than one match is reported as `ambiguous` rather
886
- * than guessed at.
887
- */
888
1089
  private resolveByDetection;
889
1090
  /**
890
1091
  * Resolve the adapter for a file, by explicit format when given, otherwise by detection.
@@ -897,1447 +1098,1571 @@ declare class AdapterRegistry {
897
1098
  resolve(filePath: string, options?: ResolveOptions): AdapterResolution;
898
1099
  }
899
1100
 
900
- /** Per-locale drift status: counts only, no key lists. */
1101
+ /** One locale's counts in a {@link CheckSummary}. */
901
1102
  interface LocaleCheckSummary {
902
- /** The target locale this entry reports on. */
1103
+ /** The target locale these counts describe. */
903
1104
  readonly locale: string;
904
- /** Keys present in source but absent from the target. */
1105
+ /** Number of source keys with no translation in this locale yet. */
905
1106
  readonly missing: number;
906
- /** Keys whose source changed since the target was last translated. */
1107
+ /** Number of keys whose source text changed since the locale was last translated. */
907
1108
  readonly stale: number;
908
- /** Keys whose recorded baseline still matches the source. */
1109
+ /** Number of keys whose translation still matches the source recorded in the lock-file. */
909
1110
  readonly upToDate: number;
910
- /** True when nothing needs (re)translating for this locale. */
1111
+ /** True when this locale has nothing missing and nothing stale. */
911
1112
  readonly inSync: boolean;
912
1113
  }
913
- /** The aggregate read-only status across all checked target locales. */
1114
+ /** The result of {@link check}: per-locale counts plus one project-wide verdict. */
914
1115
  interface CheckSummary {
915
- /** True only when every checked locale is in sync. */
1116
+ /** True only when every reported locale is in sync. This is the value a CI gate should assert on. */
916
1117
  readonly inSync: boolean;
917
- /** One entry per checked target locale, in config order. */
1118
+ /** Per-locale counts, in configured target order. */
918
1119
  readonly locales: readonly LocaleCheckSummary[];
919
1120
  }
920
- /** Input for {@link check}: the validated config and which locales to check. */
1121
+ /** Input for {@link check}. */
921
1122
  interface CheckInput {
922
- /** The validated configuration (typically from {@link loadConfig}). */
1123
+ /** The resolved project config, normally from {@link loadConfig}. */
923
1124
  readonly config: VerbatraConfig;
924
- /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
1125
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
925
1126
  readonly cwd?: string;
926
- /** Subset of target locales to check; defaults to all configured target locales. */
1127
+ /** Restrict the report to these target locales. Defaults to every configured target locale. */
927
1128
  readonly locales?: readonly string[];
928
1129
  }
929
- /** Composition seam for {@link check}: inject a registry and a file system for tests. */
1130
+ /** Injectable dependencies for {@link check}. Every field has a working default. */
930
1131
  interface CheckDeps {
1132
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
931
1133
  readonly adapterRegistry?: AdapterRegistry;
1134
+ /** File-system port. Defaults to the real file system. */
932
1135
  readonly fs?: SdkFs;
933
1136
  }
934
1137
  /**
935
- * Reports how many keys are missing or stale per target locale, without calling a provider, writing
936
- * any file, or touching the lock. Each locale is reported as counts only (missing, stale,
937
- * up-to-date); a locale is `inSync` when nothing is missing or stale, and the top-level `inSync` is
938
- * true only when every checked locale is. Orphaned keys and integrity are not reported, since they
939
- * concern a write; use {@link diff} for the key lists.
1138
+ * Reports whether every target locale is up to date, without writing anything and without calling
1139
+ * the provider. This is the CI gate: run it on a pull request and fail the build when
1140
+ * {@link CheckSummary.inSync} is false.
940
1141
  *
941
- * @param input - The validated config and which locales to check.
942
- * @param deps - Optional composition seams (registry, file system) for tests.
943
- * @returns The aggregate and per-locale drift status.
944
- * @throws {@link SdkError} `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`, `SOURCE_INVALID`, `LOCK_FILE_INVALID`
945
- * with the same meanings as in `translate`, or `UNKNOWN_LOCALE` when a requested locale is not
946
- * among the configured target locales.
1142
+ * Staleness is judged against the lock-file baseline, not against the target file's mere existence,
1143
+ * so a key whose source text changed after it was translated counts as stale even though a
1144
+ * translation is present. A locale file that does not exist yet is treated as empty rather than as
1145
+ * an error, so a newly added locale reports every key as missing.
1146
+ *
1147
+ * Use {@link diff} instead when you need the key names rather than the counts.
1148
+ *
1149
+ * Note that a malformed target locale file surfaces the adapter's own parse error rather than a
1150
+ * wrapped {@link SdkError}, because only source reads are wrapped. A caller that maps SDK codes
1151
+ * should be ready for an unrecognized error from a target file.
1152
+ *
1153
+ * @param input - The config and the optional locale filter.
1154
+ * @param deps - Optional adapter registry and file-system overrides.
1155
+ * @returns Per-locale counts and the project-wide in-sync verdict.
1156
+ *
1157
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1158
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
1159
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
1160
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1161
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
1162
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
1163
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
1164
+ * unsupported version.
1165
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: a requested locale is not a configured target locale.
947
1166
  */
948
1167
  declare function check(input: CheckInput, deps?: CheckDeps): Promise<CheckSummary>;
949
1168
 
950
- /** Per-locale pending change: the key lists, not counts. */
1169
+ /** One locale's pending work in a {@link DiffSummary}, as key names rather than counts. */
951
1170
  interface LocaleDiff {
952
- /** The target locale this entry reports on. */
1171
+ /** The target locale this entry describes. */
953
1172
  readonly locale: string;
954
- /** Keys present in source but absent from the target; would be added by a run. */
1173
+ /** Keys present in the source but absent from this locale. */
955
1174
  readonly missing: readonly string[];
956
- /** Keys whose source changed since last translated; would be re-translated. */
1175
+ /** Keys whose source text changed since this locale was last translated. */
957
1176
  readonly changed: readonly string[];
958
- /** Keys present in target but absent from source; report-only, never pending. */
1177
+ /**
1178
+ * Keys present in this locale but no longer in the source. They are reported, never removed,
1179
+ * unless a run is asked to prune.
1180
+ */
959
1181
  readonly orphaned: readonly string[];
960
- /** True when the locale has missing or changed keys; orphaned keys do not count. */
1182
+ /**
1183
+ * True when this locale has missing or changed keys. Orphaned keys alone do not count as pending,
1184
+ * because they need no translation work.
1185
+ */
961
1186
  readonly hasPendingChanges: boolean;
962
1187
  }
963
- /** The aggregate read-only diff across all checked target locales. */
1188
+ /** The result of {@link diff}: per-locale key lists plus one project-wide verdict. */
964
1189
  interface DiffSummary {
965
- /** True when any checked locale has pending changes. */
1190
+ /** True when any locale has missing or changed keys. */
966
1191
  readonly hasPendingChanges: boolean;
967
- /** One entry per checked target locale, in config order. */
1192
+ /** Per-locale key lists, in configured target order. */
968
1193
  readonly locales: readonly LocaleDiff[];
969
1194
  }
970
- /** Input for {@link diff}: the validated config and which locales to diff. */
1195
+ /** Input for {@link diff}. */
971
1196
  interface DiffInput {
972
- /** The validated configuration (typically from {@link loadConfig}). */
1197
+ /** The resolved project config, normally from {@link loadConfig}. */
973
1198
  readonly config: VerbatraConfig;
974
- /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
1199
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
975
1200
  readonly cwd?: string;
976
- /** Subset of target locales to diff; defaults to all configured target locales. */
1201
+ /** Restrict the report to these target locales. Defaults to every configured target locale. */
977
1202
  readonly locales?: readonly string[];
978
1203
  }
979
- /** Composition seam for {@link diff}: inject a registry and a file system for tests. */
1204
+ /** Injectable dependencies for {@link diff}. Every field has a working default. */
980
1205
  interface DiffDeps {
1206
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
981
1207
  readonly adapterRegistry?: AdapterRegistry;
1208
+ /** File-system port. Defaults to the real file system. */
982
1209
  readonly fs?: SdkFs;
983
1210
  }
984
1211
  /**
985
- * Reports the exact pending change per target locale as three key lists (missing, changed,
986
- * orphaned), without calling a provider, writing any file, or touching the lock. The detailed
987
- * sibling of {@link check}. A locale's `hasPendingChanges` is driven by missing or changed only;
988
- * orphaned keys are reported but do not flip it, since a default `translate` run does not prune.
989
- * The top-level `hasPendingChanges` is true when any checked locale has pending changes.
1212
+ * Reports the per-locale drift between the source and each target locale as key names, without
1213
+ * writing anything and without calling the provider. It answers "what exactly would a run change",
1214
+ * where {@link check} answers "is anything pending at all".
1215
+ *
1216
+ * The comparison runs against the lock-file baseline, so `changed` means the source text moved
1217
+ * since the key was last translated rather than merely that the two strings differ. Orphaned keys
1218
+ * are reported but never removed here; pruning happens only in {@link translate}.
1219
+ *
1220
+ * Note that a malformed target locale file surfaces the adapter's own parse error rather than a
1221
+ * wrapped {@link SdkError}, because only source reads are wrapped. A caller that maps SDK codes
1222
+ * should be ready for an unrecognized error from a target file.
990
1223
  *
991
- * @param input - The validated config and which locales to diff.
992
- * @param deps - Optional composition seams (registry, file system) for tests.
993
- * @returns The aggregate and per-locale pending change.
994
- * @throws {@link SdkError} `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`, `SOURCE_INVALID`, `LOCK_FILE_INVALID`
995
- * with the same meanings as in `translate`, or `UNKNOWN_LOCALE` when a requested locale is not
996
- * among the configured target locales.
1224
+ * @param input - The config and the optional locale filter.
1225
+ * @param deps - Optional adapter registry and file-system overrides.
1226
+ * @returns Per-locale missing, changed, and orphaned key lists.
1227
+ *
1228
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1229
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
1230
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
1231
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1232
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
1233
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
1234
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
1235
+ * unsupported version.
1236
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: a requested locale is not a configured target locale.
997
1237
  */
998
1238
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
999
1239
 
1000
- /** Why {@link gateCandidateValue} rejected a candidate value. */
1240
+ /**
1241
+ * Why a candidate translation was refused before it could be written. The same gate guards
1242
+ * {@link editEntry} and {@link retranslateEntry}, so a hand-typed value and a provider-produced one
1243
+ * are held to identical standards.
1244
+ *
1245
+ * A rejected value is never written to the locale file and never recorded in the lock-file, so the
1246
+ * previous translation stays intact.
1247
+ *
1248
+ * - `placeholder`: the candidate does not carry the same placeholders as the source, so
1249
+ * interpolation would break at runtime.
1250
+ * - `icu`: the candidate is not a valid ICU message under the configured format's adapter.
1251
+ * - `degenerate`: the candidate collapsed into runaway output rather than a translation. Two shapes
1252
+ * are detected: the candidate is at least twelve times the length of a source of meaningful
1253
+ * length, or a short unit repeats consecutively enough to dominate the value. An untranslated
1254
+ * echo of the source is not degenerate by this rule; it surfaces as the `EQUALS_SOURCE` review
1255
+ * reason instead, which flags rather than refuses.
1256
+ * - `empty`: the source has text but the candidate is blank, which would silently erase a string.
1257
+ */
1001
1258
  type IntegrityGateReason = "placeholder" | "icu" | "degenerate" | "empty";
1002
1259
 
1003
- /** Input for {@link editEntry}: the validated config, exactly one target locale/key pair, and the human-typed replacement value. */
1260
+ /** Input for {@link editEntry}. */
1004
1261
  interface EditEntryInput {
1005
- /** The validated configuration (typically from {@link loadConfig}). */
1262
+ /** The resolved project config, normally from {@link loadConfig}. */
1006
1263
  readonly config: VerbatraConfig;
1007
- /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
1264
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
1008
1265
  readonly cwd?: string;
1009
- /** The target locale to write the correction into. Must be a configured target locale. */
1266
+ /** The target locale to write to. Must be a configured target locale. */
1010
1267
  readonly locale: string;
1011
- /** The source key being corrected. Must exist in the source resource. */
1268
+ /** The key to edit. Must exist in the source resource. */
1012
1269
  readonly key: string;
1013
- /** The human-typed replacement value. */
1270
+ /** The new translation. It is accepted only if it passes the integrity gate. */
1014
1271
  readonly value: string;
1015
1272
  }
1016
- /** Composition seam for {@link editEntry}: inject a registry and a file system for tests. No provider builder: this seam never calls a provider. */
1273
+ /** Injectable dependencies for {@link editEntry}. Every field has a working default. */
1017
1274
  interface EditEntryDeps {
1275
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
1018
1276
  readonly adapterRegistry?: AdapterRegistry;
1277
+ /** File-system port. Defaults to the real file system. */
1019
1278
  readonly fs?: SdkFs;
1020
1279
  }
1021
1280
  /**
1022
- * The two-armed result of one edit attempt: acceptance carries the newly written value; a
1023
- * rejection carries the candidate value and which {@link gateCandidateValue} check failed it,
1024
- * with nothing written. Deliberately has no review-reasons field: a human-typed correction never
1025
- * touches a provider, so there is no provider-derived review signal to report.
1281
+ * The outcome of {@link editEntry}. A rejection is data rather than an exception, because a value
1282
+ * that fails the integrity gate is an expected editing outcome that a UI needs to explain, not a
1283
+ * failure of the call.
1026
1284
  */
1027
1285
  type EditEntryResult = {
1286
+ /** The value passed the integrity gate and was written. */
1028
1287
  readonly accepted: true;
1288
+ /** The value now stored for the key. */
1029
1289
  readonly value: string;
1030
1290
  } | {
1291
+ /** The value was refused; nothing was written and the previous translation is intact. */
1031
1292
  readonly accepted: false;
1293
+ /** Which integrity rule the value broke. */
1032
1294
  readonly reason: IntegrityGateReason;
1295
+ /** The rejected value, echoed back so a UI can show what was refused. */
1033
1296
  readonly value: string;
1034
1297
  };
1035
1298
  /**
1036
- * Writes exactly one human-typed correction for exactly one target locale, gated through the
1037
- * shared {@link gateCandidateValue} before anything reaches disk. On acceptance, writes the target
1038
- * locale file (merging just this one key into its current entries, every other key untouched) and
1039
- * updates the lock entry for this key only (see {@link updateLockFileLocale}'s `"merge"` mode); on
1040
- * rejection, writes nothing. Never calls a provider: {@link EditEntryDeps} has no provider seam,
1041
- * so there is no way to construct or call one. On acceptance it also feeds the accepted value into
1042
- * the local translation-memory cache (best-effort, keyed by the source content hash and the config
1043
- * fingerprint), so a later run can reuse it for free; a cache failure is swallowed and never fails
1044
- * the edit.
1045
- *
1046
- * `locale` and `key` are resolved fresh on every call, never from a cache: `locale` against
1047
- * `config.targetLocales`, `key` against the source resource read fresh from disk.
1048
- *
1049
- * The target-file write and the lock-file update run inside one held `withLocaleWriteLock`
1050
- * critical section, so no second writer for this locale can observe the target file updated but
1051
- * the lock entry not yet, or vice versa. Reading the source, resolving the key, and selecting the
1052
- * adapter stay outside the lock: they are read-only and need no protection.
1053
- *
1054
- * Never touches `.verbatra-local/run-status.json`: that file is rewritten wholesale by
1055
- * `translate()`/`watch()` outside any locale lock, so patching it here would race that rewrite. A
1056
- * key edited here stays stale in that persisted snapshot until the next real run, which self-heals
1057
- * it since this call already advanced the lock hash.
1058
- *
1059
- * @param input - The validated config, the target locale, the source key, and the replacement value.
1060
- * @param deps - Optional composition seams (registry, file system) for tests.
1061
- * @returns The two-armed {@link EditEntryResult}.
1299
+ * Saves a manually edited translation for a single key. This is the write path behind a review UI:
1300
+ * a human corrects one string and it is persisted without re-running the whole project.
1301
+ *
1302
+ * The edit is held to the same integrity gate as a provider translation, so a value that drops a
1303
+ * placeholder, breaks ICU syntax, runs away into a hugely oversized or repetition-dominated value,
1304
+ * or blanks a non-empty string is refused. A refusal comes back as `accepted: false` rather than as
1305
+ * a thrown error, and nothing is written.
1306
+ *
1307
+ * The locale's write lock is taken for the whole read-modify-write, so an edit cannot interleave
1308
+ * with a concurrent {@link translate} run on the same locale. The gate runs inside that lock, so a
1309
+ * refused edit takes and releases the lock as well and can therefore fail with `LOCK_CONTENDED`
1310
+ * even though it would have written nothing. An accepted edit then updates the lock-file baseline
1311
+ * and feeds the translation memory, which means a later run treats the key as up to date and
1312
+ * reuses the edited text rather than paying the provider to translate it again.
1313
+ *
1314
+ * Note that a malformed target locale file surfaces the adapter's own parse error rather than a
1315
+ * wrapped {@link SdkError}, because only source reads are wrapped. A caller that maps SDK codes
1316
+ * should be ready for an unrecognized error from a target file.
1317
+ *
1318
+ * @param input - The config, locale, key, and new value.
1319
+ * @param deps - Optional adapter registry and file-system overrides.
1320
+ * @returns Whether the value was accepted, and on rejection the reason.
1321
+ *
1062
1322
  * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1063
- * @throws {@link SdkError} `UNKNOWN_LOCALE`: `locale` is not among `config.targetLocales`.
1064
- * @throws {@link SdkError} `SOURCE_UNREADABLE` / `SOURCE_INVALID`: the source locale file is absent
1065
- * or unreadable.
1066
- * @throws {@link SdkError} `UNKNOWN_KEY`: `key` does not exist in the source resource.
1323
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: the requested locale is not a configured target locale.
1324
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
1325
+ * cannot be combined, or the locale has no valid path spelling under that style.
1326
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1327
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
1328
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
1329
+ * @throws {@link SdkError} `UNKNOWN_KEY`: the key is not present in the source resource.
1330
+ * @throws {@link SdkError} `LOCK_CONTENDED`: the locale's write lock could not be acquired before
1331
+ * the timeout elapsed.
1067
1332
  * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
1068
- * unsupported version.
1069
- * @throws {@link SdkError} `LOCK_CONTENDED`: this locale's write lock could not be acquired before
1070
- * its timeout, because another process is holding it.
1333
+ * unsupported version.
1071
1334
  */
1072
1335
  declare function editEntry(input: EditEntryInput, deps?: EditEntryDeps): Promise<EditEntryResult>;
1073
1336
 
1074
- /**
1075
- * One key's placeholder and ICU integrity result for one target locale. Only ever computed for a
1076
- * "changed" key (a current source and a current target value both exist). `hasPlaceholders` is
1077
- * false when the source value carries no placeholders, in which case `matches` is trivially true
1078
- * and carries no signal on its own. `icuValid` is computed unconditionally, independent of
1079
- * `matches`: unlike `gateCandidateValue`, which stops at the first failing check, this is an
1080
- * information report and stays accurate even when the key already fails on placeholders; it is
1081
- * always true for a non-ICU format. Carries no source or target string value: only the boolean
1082
- * results and, on a placeholder mismatch, the specific tokens involved.
1083
- */
1337
+ /** One key's placeholder and ICU verdict in a {@link LocaleKeyIntegrity} report. */
1084
1338
  interface KeyIntegrityEntry {
1339
+ /** The key this verdict describes. */
1085
1340
  readonly key: string;
1341
+ /** Whether the source text contains any placeholders at all. When false, `matches` is trivially true. */
1086
1342
  readonly hasPlaceholders: boolean;
1343
+ /** True when the translation carries exactly the source's placeholders. */
1087
1344
  readonly matches: boolean;
1345
+ /** Placeholders present in the source but absent from the translation. */
1088
1346
  readonly missing: readonly string[];
1347
+ /** Placeholders present in the translation but not in the source. */
1089
1348
  readonly extra: readonly string[];
1349
+ /** True when the translation parses as a valid ICU message under the configured format. */
1090
1350
  readonly icuValid: boolean;
1091
1351
  }
1092
- /** One target locale's integrity entries for the keys checked against it. */
1352
+ /** One locale's per-key integrity verdicts. */
1093
1353
  interface LocaleKeyIntegrity {
1354
+ /** The target locale these verdicts describe. */
1094
1355
  readonly locale: string;
1356
+ /** Verdicts for the changed keys that exist in both the source and this locale. */
1095
1357
  readonly entries: readonly KeyIntegrityEntry[];
1096
1358
  }
1097
- /** Input for {@link keyIntegrity}: the validated config, which locales to check, and an optional key filter. */
1359
+ /** Input for {@link keyIntegrity}. */
1098
1360
  interface KeyIntegrityInput {
1099
- /** The validated configuration (typically from {@link loadConfig}). */
1361
+ /** The resolved project config, normally from {@link loadConfig}. */
1100
1362
  readonly config: VerbatraConfig;
1101
- /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
1363
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
1102
1364
  readonly cwd?: string;
1103
- /** Subset of target locales to check; defaults to all configured target locales. */
1365
+ /** Restrict the report to these target locales. Defaults to every configured target locale. */
1104
1366
  readonly locales?: readonly string[];
1105
- /** Restrict the check to these keys; only the ones that are "changed" for a locale are checked. Defaults to every changed key. */
1367
+ /** Restrict the report to these keys. Defaults to every key the diff reports as changed. */
1106
1368
  readonly keys?: readonly string[];
1107
1369
  }
1108
- /** Composition seam for {@link keyIntegrity}: inject a registry and a file system for tests. */
1370
+ /** Injectable dependencies for {@link keyIntegrity}. Every field has a working default. */
1109
1371
  interface KeyIntegrityDeps {
1372
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
1110
1373
  readonly adapterRegistry?: AdapterRegistry;
1374
+ /** File-system port. Defaults to the real file system. */
1111
1375
  readonly fs?: SdkFs;
1112
1376
  }
1113
1377
  /**
1114
- * Runs the placeholder check and the ICU message-validity check for each selected target locale's
1115
- * "changed" keys (present with a current value on both sides, per core's `diffResources`) and
1116
- * reports a per-key result. Uses the adapter's own `comparePlaceholders` when present (the
1117
- * branch-aware ICU path), otherwise core's `checkPlaceholders` over the stored placeholder lists.
1118
- * `icuValid` is `adapter.validateMessage(target)`, computed unconditionally and independently of
1119
- * the placeholder result, so a target that is placeholder-valid but syntactically invalid ICU is
1120
- * still reported accurately. Read-only: it calls no provider, writes no file, and never touches
1121
- * the lock. Missing and orphaned keys are never checked, since one side's value does not exist for
1122
- * those.
1378
+ * Reports, per changed key, whether the existing translation still carries the source's
1379
+ * placeholders and still parses as valid ICU. It writes nothing and calls no provider.
1380
+ *
1381
+ * The scope is deliberately the changed keys rather than every key: a key whose source text has not
1382
+ * moved was already gated when it was written, so re-reporting it would bury the keys that a source
1383
+ * edit may have just invalidated. Only keys present in both the source and the target are judged,
1384
+ * since a missing translation has no placeholders to compare.
1385
+ *
1386
+ * This is the read-only counterpart to the gate that {@link editEntry} and
1387
+ * {@link retranslateEntry} enforce at write time, and the data behind a review dashboard's
1388
+ * per-key integrity indicator.
1123
1389
  *
1124
- * The returned entries never carry a source or target string value, only the boolean results and,
1125
- * on a placeholder mismatch, the specific tokens involved.
1390
+ * Note that a malformed target locale file surfaces the adapter's own parse error rather than a
1391
+ * wrapped {@link SdkError}, because only source reads are wrapped. A caller that maps SDK codes
1392
+ * should be ready for an unrecognized error from a target file.
1126
1393
  *
1127
- * @param input - The validated config, which locales to check, and an optional key filter.
1128
- * @param deps - Optional composition seams (registry, file system) for tests.
1129
- * @returns One entry per checked locale, each carrying the integrity result for its changed keys
1130
- * (narrowed to `input.keys` when supplied).
1131
- * @throws {@link SdkError} `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`, `SOURCE_INVALID`,
1132
- * `LOCK_FILE_INVALID`, or `UNKNOWN_LOCALE`, with the same meanings as in {@link diff}.
1394
+ * @param input - The config and the optional locale and key filters.
1395
+ * @param deps - Optional adapter registry and file-system overrides.
1396
+ * @returns One entry per requested locale, each holding its per-key verdicts.
1397
+ *
1398
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1399
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
1400
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
1401
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1402
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
1403
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
1404
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
1405
+ * unsupported version.
1406
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: a requested locale is not a configured target locale.
1133
1407
  */
1134
1408
  declare function keyIntegrity(input: KeyIntegrityInput, deps?: KeyIntegrityDeps): Promise<readonly LocaleKeyIntegrity[]>;
1135
1409
 
1136
- /** Input for {@link keyValue}: the validated config and exactly one target locale/key pair. */
1410
+ /** Input for {@link keyValue}. */
1137
1411
  interface KeyValueInput {
1138
- /** The validated configuration (typically from {@link loadConfig}). */
1412
+ /** The resolved project config, normally from {@link loadConfig}. */
1139
1413
  readonly config: VerbatraConfig;
1140
- /** Directory the file pattern resolves against; defaults to cwd. */
1414
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
1141
1415
  readonly cwd?: string;
1142
- /** The target locale to read the current translation from. Must be a configured target locale. */
1416
+ /** The target locale to read the translation from. Must be a configured target locale. */
1143
1417
  readonly locale: string;
1144
- /** The source key to read. Must exist in the source resource. */
1418
+ /** The key to read. Must exist in the source resource. */
1145
1419
  readonly key: string;
1146
1420
  }
1147
- /** Composition seam for {@link keyValue}: inject a registry and a file system for tests. */
1421
+ /** Injectable dependencies for {@link keyValue}. Every field has a working default. */
1148
1422
  interface KeyValueDeps {
1423
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
1149
1424
  readonly adapterRegistry?: AdapterRegistry;
1425
+ /** File-system port. Defaults to the real file system. */
1150
1426
  readonly fs?: SdkFs;
1151
1427
  }
1152
- /**
1153
- * The current source and target values for one key/locale pair. `target` is absent exactly when
1154
- * the key does not yet exist in that target locale; a present-but-empty-string target is a real
1155
- * value and is returned as such.
1156
- */
1428
+ /** One key's current source and target text, as returned by {@link keyValue}. */
1157
1429
  interface KeyValueResult {
1430
+ /** The key's text in the source locale. Always present, since a missing key is an error. */
1158
1431
  readonly source: string;
1432
+ /** The key's text in the requested target locale, or absent when it has not been translated yet. */
1159
1433
  readonly target?: string;
1160
1434
  }
1161
1435
  /**
1162
- * Reads a key's current source and target values for exactly one target locale. Read-only: it
1163
- * calls no provider, writes no file, and mutates nothing. Exposes only the current values on disk,
1164
- * never a previous one.
1436
+ * Reads one key's current source and target text. It writes nothing and calls no provider, and is
1437
+ * the read half of the single-key editing flow that {@link editEntry} and
1438
+ * {@link retranslateEntry} complete.
1439
+ *
1440
+ * The two sides are asymmetric on purpose: a key absent from the source is an error, because it
1441
+ * cannot be edited or retranslated, while a key absent from the target is normal and simply comes
1442
+ * back with no `target`.
1443
+ *
1444
+ * Note that a malformed target locale file surfaces the adapter's own parse error rather than a
1445
+ * wrapped {@link SdkError}, because only source reads are wrapped. A caller that maps SDK codes
1446
+ * should be ready for an unrecognized error from a target file.
1165
1447
  *
1166
- * `locale` and `key` are resolved fresh on every call, so a caller feeding an edit dialog with
1167
- * this result and then submitting through `editEntry` is always working from live data, never a
1168
- * cached snapshot.
1448
+ * @param input - The config, locale, and key to read.
1449
+ * @param deps - Optional adapter registry and file-system overrides.
1450
+ * @returns The key's source text and, when present, its current translation.
1169
1451
  *
1170
- * @param input - The validated config, the target locale, and the source key to read.
1171
- * @param deps - Optional composition seams (registry, file system) for tests.
1172
- * @returns The current source value and, when present, the current target value.
1173
1452
  * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1174
- * @throws {@link SdkError} `UNKNOWN_LOCALE`: `locale` is not among `config.targetLocales`.
1175
- * @throws {@link SdkError} `SOURCE_UNREADABLE` / `SOURCE_INVALID`: the source locale file is absent
1176
- * or unreadable.
1177
- * @throws {@link SdkError} `UNKNOWN_KEY`: `key` does not exist in the source resource.
1453
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: the requested locale is not a configured target locale.
1454
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
1455
+ * cannot be combined, or the locale has no valid path spelling under that style.
1456
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1457
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
1458
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
1459
+ * @throws {@link SdkError} `UNKNOWN_KEY`: the key is not present in the source resource.
1178
1460
  */
1179
1461
  declare function keyValue(input: KeyValueInput, deps?: KeyValueDeps): Promise<KeyValueResult>;
1180
1462
 
1181
1463
  /**
1182
- * One locale file reduced to a content hash per key, taken at one point in time. Two snapshots of
1183
- * the same file can be compared with {@link diffLocaleSnapshots} to count what changed between
1184
- * them.
1464
+ * One locale file reduced to a per-key content hash. Holding hashes rather than values keeps a
1465
+ * long-lived watcher's memory flat in the size of the translations and makes comparison a cheap
1466
+ * equality check.
1185
1467
  */
1186
1468
  interface LocaleFileSnapshot {
1187
- /** The locale this snapshot was read for (the configured source locale or a target locale). */
1469
+ /** The locale this snapshot was taken for. */
1188
1470
  readonly locale: string;
1189
- /** Content hash per key, from core's `contentHash`. */
1471
+ /** Content hash per key, in the order the adapter read them. */
1190
1472
  readonly hashes: ReadonlyMap<string, string>;
1191
1473
  }
1192
- /** Input for {@link readLocaleFileSnapshot}: the validated config and which locale to snapshot. */
1474
+ /** Input for {@link readLocaleFileSnapshot}. */
1193
1475
  interface ReadLocaleFileSnapshotInput {
1194
- /** The validated configuration (typically from {@link loadConfig}). */
1476
+ /** The resolved project config, normally from {@link loadConfig}. */
1195
1477
  readonly config: VerbatraConfig;
1196
- /** The locale to snapshot: the configured source locale or any configured target locale. */
1478
+ /**
1479
+ * The locale file to snapshot. Unlike most entry points this is not checked against the
1480
+ * configured target locales, so the source locale can be snapshotted too.
1481
+ */
1197
1482
  readonly locale: string;
1198
- /** Directory the file pattern resolves against; defaults to cwd. */
1483
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
1199
1484
  readonly cwd?: string;
1200
1485
  }
1201
- /** Composition seam for {@link readLocaleFileSnapshot}: inject a registry and a file system for tests. */
1486
+ /** Injectable dependencies for {@link readLocaleFileSnapshot}. Every field has a working default. */
1202
1487
  interface ReadLocaleFileSnapshotDeps {
1488
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
1203
1489
  readonly adapterRegistry?: AdapterRegistry;
1490
+ /** File-system port. Defaults to the real file system. */
1204
1491
  readonly fs?: SdkFs;
1205
1492
  }
1206
1493
  /**
1207
- * Reads one locale file through the configured adapter and reduces it to a content hash per key.
1208
- * Uses the same tolerant read path as {@link readTarget}: a file that does not exist yet reads as
1209
- * an empty snapshot rather than throwing, so a caller can establish a baseline before a locale's
1210
- * first write.
1494
+ * Reads one locale file as a per-key content hash. Together with {@link diffLocaleSnapshots} this
1495
+ * is the building block for a live-refresh watcher: hold a snapshot, re-read on a file event, and
1496
+ * compare the two to learn what actually changed.
1497
+ *
1498
+ * A locale file that does not exist yet reads as an empty snapshot rather than an error, so a
1499
+ * watcher can start before the first translation run has written anything.
1500
+ *
1501
+ * Note that this entry point does not validate the locale against the configured targets, and that
1502
+ * a malformed target file surfaces the adapter's own parse error rather than a wrapped
1503
+ * {@link SdkError}, because no source-file contract is being asserted here.
1211
1504
  *
1212
- * @param input - The validated config, the locale to snapshot, and the directory to resolve against.
1213
- * @param deps - Optional composition seams (registry, file system) for tests.
1214
- * @returns The locale's per-key content hash snapshot.
1215
- * @throws {@link SdkError} `UNKNOWN_FORMAT` when the configured format has no registered adapter;
1216
- * rejects with the adapter's own error when the file exists but its content is malformed.
1505
+ * @param input - The config and the locale to snapshot.
1506
+ * @param deps - Optional adapter registry and file-system overrides.
1507
+ * @returns The locale's per-key content hashes.
1508
+ *
1509
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1510
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
1511
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
1512
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1217
1513
  */
1218
1514
  declare function readLocaleFileSnapshot(input: ReadLocaleFileSnapshotInput, deps?: ReadLocaleFileSnapshotDeps): Promise<LocaleFileSnapshot>;
1219
- /** A locale file's added, changed, and removed key counts between two of its own snapshots. Counts only, never key names. */
1515
+ /** How many keys were added, changed, and removed between two {@link LocaleFileSnapshot}s. */
1220
1516
  interface LocaleSnapshotDelta {
1517
+ /** Keys present in the current snapshot but not the previous one. */
1221
1518
  readonly added: number;
1519
+ /** Keys present in both snapshots whose content hash differs. */
1222
1520
  readonly changed: number;
1521
+ /** Keys present in the previous snapshot but not the current one. */
1223
1522
  readonly removed: number;
1224
1523
  }
1225
1524
  /**
1226
- * Compares two snapshots of the same locale file, taken at different points in time, and counts
1227
- * how many keys were added, changed, or removed. A direct key-to-hash comparison rather than
1228
- * core's `diffResources`: that function's source/target/baseline shape exists for cross-resource
1229
- * comparison, and forcing a same-file, two-points-in-time comparison through it would need a
1230
- * confusing relabeling (missing becomes removed, orphaned becomes added).
1525
+ * Compares two {@link LocaleFileSnapshot}s and counts what moved. It is pure, synchronous, and
1526
+ * touches no file system, so a watcher can call it on every file event without cost.
1231
1527
  *
1232
- * @param previous - The earlier snapshot of the file.
1233
- * @param current - The later snapshot of the same file.
1234
- * @returns The counts of keys added, changed, and removed between the two snapshots.
1528
+ * The two snapshots are compared by key and content hash alone; neither the locale names nor the
1529
+ * order are consulted, so passing snapshots of two different locales compares them rather than
1530
+ * failing.
1531
+ *
1532
+ * @param previous - The earlier snapshot.
1533
+ * @param current - The later snapshot.
1534
+ * @returns The added, changed, and removed key counts.
1235
1535
  */
1236
1536
  declare function diffLocaleSnapshots(previous: LocaleFileSnapshot, current: LocaleFileSnapshot): LocaleSnapshotDelta;
1237
1537
 
1238
- /** Per-locale key count and drift against the source and target files, from the recorded lock baseline. */
1538
+ /** One locale's lock-file baseline and the drift measured against it. */
1239
1539
  interface LockLocaleState {
1240
- /** The target locale this entry reports on. */
1540
+ /** The target locale this entry describes. */
1241
1541
  readonly locale: string;
1242
- /** Number of keys the lock records a baseline for, in this locale. */
1542
+ /** How many keys the lock-file records a baseline hash for in this locale. */
1243
1543
  readonly keyCount: number;
1244
- /** Keys present in source but absent from the target. */
1544
+ /** Number of source keys with no translation in this locale yet. */
1245
1545
  readonly missing: number;
1246
- /** Keys whose source changed since the target was last translated. */
1546
+ /** Number of keys whose source text changed since the recorded baseline. */
1247
1547
  readonly stale: number;
1248
- /** Keys whose recorded baseline still matches the source. */
1548
+ /** Number of keys whose translation still matches the recorded baseline. */
1249
1549
  readonly upToDate: number;
1250
1550
  }
1251
1551
  /**
1252
- * The lock-file's existence, version, and per-locale drift. `exists` is the outcome of an explicit
1253
- * probe of the lock-file path, never inferred from {@link readLockFile}'s missing-file degrade, so
1254
- * "no lock-file yet" and "an empty but present lock-file" stay distinguishable.
1552
+ * The result of {@link lockState}. The absence of a lock-file is a first-class state rather than an
1553
+ * error, because a project that has never been translated legitimately has none.
1255
1554
  */
1256
1555
  type LockStateResult = {
1556
+ /** No lock-file exists yet, so there is no baseline to report against. */
1257
1557
  readonly exists: false;
1258
1558
  } | {
1559
+ /** A lock-file exists and was read successfully. */
1259
1560
  readonly exists: true;
1561
+ /** The lock-file's schema version. */
1260
1562
  readonly version: number;
1563
+ /** Per-locale baseline sizes and drift, in configured target order. */
1261
1564
  readonly locales: readonly LockLocaleState[];
1262
1565
  };
1263
- /** Input for {@link lockState}: the validated config and which locales to report on. */
1566
+ /** Input for {@link lockState}. */
1264
1567
  interface LockStateInput {
1265
- /** The validated configuration (typically from {@link loadConfig}). */
1568
+ /** The resolved project config, normally from {@link loadConfig}. */
1266
1569
  readonly config: VerbatraConfig;
1267
- /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
1570
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
1268
1571
  readonly cwd?: string;
1269
- /** Subset of target locales to report on; defaults to all configured target locales. */
1572
+ /** Restrict the report to these target locales. Defaults to every configured target locale. */
1270
1573
  readonly locales?: readonly string[];
1271
1574
  }
1272
- /** Composition seam for {@link lockState}: inject a registry and a file system for tests. */
1575
+ /** Injectable dependencies for {@link lockState}. Every field has a working default. */
1273
1576
  interface LockStateDeps {
1577
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
1274
1578
  readonly adapterRegistry?: AdapterRegistry;
1579
+ /** File-system port. Defaults to the real file system. */
1275
1580
  readonly fs?: SdkFs;
1276
1581
  }
1277
1582
  /**
1278
- * Report the lock-file's existence, version, and per-locale drift (key count from the recorded
1279
- * baseline, plus missing, stale, and up-to-date counts against the current source and target),
1280
- * without calling a provider, writing any file, or mutating the lock. `exists` comes from an
1281
- * explicit probe of the lock-file path: when it is absent, the result is `{ exists: false }` and
1282
- * no source or target file is read, since there is no recorded baseline to report drift against.
1583
+ * Reports the lock-file's existence, version, and the per-locale drift measured against its
1584
+ * baseline. It writes nothing and calls no provider.
1585
+ *
1586
+ * Where {@link check} answers "is the project in sync", this answers "what does the lock-file
1587
+ * actually record", which is what a diagnostic view needs when the two disagree: a locale with
1588
+ * translations present but a `keyCount` of zero, for instance, means the files were written outside
1589
+ * verbatra and have no baseline.
1590
+ *
1591
+ * When no lock-file exists the call returns `exists: false` rather than throwing, and does no
1592
+ * further reading.
1593
+ *
1594
+ * Note that a malformed target locale file surfaces the adapter's own parse error rather than a
1595
+ * wrapped {@link SdkError}, because only source reads are wrapped. A caller that maps SDK codes
1596
+ * should be ready for an unrecognized error from a target file.
1597
+ *
1598
+ * @param input - The config and the optional locale filter.
1599
+ * @param deps - Optional adapter registry and file-system overrides.
1600
+ * @returns The lock-file's version and per-locale baseline state, or `exists: false`.
1283
1601
  *
1284
- * @param input - The validated config and which locales to report on.
1285
- * @param deps - Optional composition seams (registry, file system) for tests.
1286
- * @returns The lock-file's existence and, when present, its version and per-locale drift.
1287
- * @throws {@link SdkError} `UNKNOWN_LOCALE` when a requested locale is not among the configured
1288
- * target locales; when the lock-file exists, also `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`,
1289
- * `SOURCE_INVALID`, or `LOCK_FILE_INVALID` with the same meanings as in `translate`.
1602
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: a requested locale is not a configured target locale.
1603
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
1604
+ * unsupported version.
1605
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1606
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
1607
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
1608
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1609
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
1610
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
1290
1611
  */
1291
1612
  declare function lockState(input: LockStateInput, deps?: LockStateDeps): Promise<LockStateResult>;
1292
1613
 
1293
- /** Builds the provider from its config. Injectable so tests stay offline. */
1614
+ /**
1615
+ * Builds a {@link TranslationProvider} from the config's `provider` block. Passed as
1616
+ * `deps.createProvider` to {@link translate}, {@link watch}, and {@link retranslateEntry}, it is the
1617
+ * seam for injecting a stub in a test or a provider the SDK does not ship.
1618
+ *
1619
+ * The default implementation dispatches on the provider ID and reads the API key from the
1620
+ * environment. A factory that throws is reported as `PROVIDER_CONSTRUCTION_FAILED`.
1621
+ */
1294
1622
  type CreateProvider = (config: ProviderConfig) => TranslationProvider;
1295
1623
 
1296
- /** Input for {@link retranslateEntry}: the validated config and exactly one target locale/key pair. */
1624
+ /** Input for {@link retranslateEntry}. */
1297
1625
  interface RetranslateEntryInput {
1298
- /** The validated configuration (typically from {@link loadConfig}). */
1626
+ /** The resolved project config, normally from {@link loadConfig}. */
1299
1627
  readonly config: VerbatraConfig;
1300
- /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
1628
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
1301
1629
  readonly cwd?: string;
1302
- /** The target locale to retranslate the key into. Must be a configured target locale. */
1630
+ /** The target locale to retranslate into. Must be a configured target locale. */
1303
1631
  readonly locale: string;
1304
- /** The source key to retranslate. Must exist in the source resource. */
1632
+ /** The key to retranslate. Must exist in the source resource. */
1305
1633
  readonly key: string;
1306
1634
  }
1307
- /** Composition seam for {@link retranslateEntry}: inject a registry, a provider builder, and a file system for tests. */
1635
+ /** Injectable dependencies for {@link retranslateEntry}. Every field has a working default. */
1308
1636
  interface RetranslateEntryDeps {
1637
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
1309
1638
  readonly adapterRegistry?: AdapterRegistry;
1639
+ /** Provider factory. Defaults to constructing the provider named in the config. */
1310
1640
  readonly createProvider?: CreateProvider;
1641
+ /** File-system port. Defaults to the real file system. */
1311
1642
  readonly fs?: SdkFs;
1312
1643
  }
1313
1644
  /**
1314
- * The two-armed result of one retranslate attempt: `accepted` carries the newly written value and
1315
- * any derived "needs review" reasons; a rejection carries the candidate value and which
1316
- * {@link gateCandidateValue} check failed it, without writing anything.
1645
+ * The outcome of {@link retranslateEntry}. As with {@link EditEntryResult}, a value refused by the
1646
+ * integrity gate is reported as data rather than thrown, because it is an expected outcome of
1647
+ * asking a provider for a fresh translation.
1317
1648
  */
1318
1649
  type RetranslateEntryResult = {
1650
+ /** The provider's value passed the integrity gate and was written. */
1319
1651
  readonly accepted: true;
1652
+ /** The newly translated value now stored for the key. */
1320
1653
  readonly value: string;
1654
+ /**
1655
+ * Quality signals the provider layer raised for this value, such as a length-ratio outlier or
1656
+ * a value identical to the source. Empty when nothing was flagged. The value is written
1657
+ * either way; these are advisory.
1658
+ */
1321
1659
  readonly reviewReasons: readonly ReviewReasonCode[];
1322
1660
  } | {
1661
+ /** The value was refused; nothing was written and the previous translation is intact. */
1323
1662
  readonly accepted: false;
1663
+ /** Which integrity rule the provider's value broke. */
1324
1664
  readonly reason: IntegrityGateReason;
1665
+ /** The rejected value, echoed back so a UI can show what was refused. */
1325
1666
  readonly value: string;
1326
1667
  };
1327
1668
  /**
1328
- * Retranslate exactly one key for exactly one target locale: a single-entry `translateBatch` call
1329
- * through the same provider registry `translate()` uses, gated through the shared
1330
- * {@link gateCandidateValue} before anything reaches disk. On acceptance, writes the target locale
1331
- * file (merging just this one key into its current entries, every other key untouched) and updates
1332
- * the lock entry for this key only (see {@link updateLockFileLocale}'s `"merge"` mode); on rejection,
1333
- * writes nothing. Reuses exactly the building blocks `translate()` already uses
1334
- * (`selectProvider`/`provider.translateBatch`) and nothing else: no bespoke per-provider plumbing.
1335
- * Always calls the provider (it never consults the cache for a hit, since its whole purpose is a
1336
- * fresh translation), then feeds the accepted value into the translation-memory cache best-effort so
1337
- * a later run can reuse it; a cache failure is swallowed and never fails the retranslate.
1338
- *
1339
- * `locale` and `key` are resolved fresh on every call, never cached: `locale` against
1340
- * `config.targetLocales` (`UNKNOWN_LOCALE`), `key` against the source resource's own keys, read
1341
- * fresh from disk (`UNKNOWN_KEY`), via a `Map`-backed lookup, never `key in obj` or
1342
- * `obj[key] !== undefined` against a plain object.
1343
- *
1344
- * The target-file write and the lock-file update run inside the same held
1345
- * `withLocaleWriteLock(cwd, locale, fs, ...)` critical section as the provider call itself, so no
1346
- * second writer for this locale can ever observe the target file updated but the lock entry not
1347
- * yet, or vice versa. The same locking also covers `translate()`/`watch()` and `importWorkbook()`.
1348
- *
1349
- * Not atomic across the two writes for a single caller's own run: the target locale file is
1350
- * written before the lock entry is updated. If `updateLockFileLocale` then throws
1351
- * (`LOCK_FILE_INVALID` for a corrupt lock-file, or `LOCK_CONTENDED` if its own internal lock-file
1352
- * guard cannot be acquired before its timeout), the target file already carries the new value
1353
- * while the lock still records the old hash, and the thrown error does not imply nothing was
1354
- * saved. The same ordering, and the same partial-write shape on that rare path, also exists in
1355
- * `translate()` and `importWorkbook()`.
1356
- *
1357
- * @param input - The validated config, the target locale, and the source key to retranslate.
1358
- * @param deps - Optional composition seams (registry, provider builder, file system) for tests.
1359
- * @returns The two-armed {@link RetranslateEntryResult}.
1669
+ * Re-runs the configured provider for a single key and saves the result. This is the paid
1670
+ * counterpart to {@link editEntry}: it calls the provider and therefore spends tokens, which is why
1671
+ * a UI should gate it behind an explicit user action.
1672
+ *
1673
+ * The returned value goes through the same integrity gate as a full run, so a translation that
1674
+ * loses a placeholder or breaks ICU syntax is refused and nothing is written. Provider quality
1675
+ * signals are surfaced on an accepted result as `reviewReasons` rather than blocking the write.
1676
+ *
1677
+ * The locale's write lock is taken before the provider is called and held across it, covering the
1678
+ * whole read-translate-write cycle, so a slow provider keeps the lock for the length of its call
1679
+ * and a concurrent {@link translate} run on that locale waits. The lock is held for a refused
1680
+ * translation too, since the gate runs inside it. An accepted value then updates the lock-file
1681
+ * baseline and feeds the translation memory, so a later {@link translate} run sees the key as up
1682
+ * to date.
1683
+ *
1684
+ * Note that a malformed target locale file surfaces the adapter's own parse error rather than a
1685
+ * wrapped {@link SdkError}, because only source reads are wrapped. A caller that maps SDK codes
1686
+ * should be ready for an unrecognized error from a target file.
1687
+ *
1688
+ * @param input - The config, locale, and key to retranslate.
1689
+ * @param deps - Optional adapter registry, provider factory, and file-system overrides.
1690
+ * @returns Whether the new value was accepted, with review reasons or the rejection reason.
1691
+ *
1360
1692
  * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1361
- * @throws {@link SdkError} `UNKNOWN_LOCALE`: `locale` is not among `config.targetLocales`.
1362
- * @throws {@link SdkError} `SOURCE_UNREADABLE` / `SOURCE_INVALID`: the source locale file is absent
1363
- * or unreadable.
1364
- * @throws {@link SdkError} `UNKNOWN_KEY`: `key` does not exist in the source resource.
1365
- * @throws {@link SdkError} `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw (wraps a
1366
- * missing `*_API_KEY` as `MISSING_API_KEY`).
1367
- * @throws {@link ProviderError} the provider call itself failed, or returned no value at all for
1368
- * this key (`INVALID_RESPONSE`).
1693
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: the requested locale is not a configured target locale.
1694
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
1695
+ * cannot be combined, or the locale has no valid path spelling under that style.
1696
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1697
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
1698
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
1699
+ * @throws {@link SdkError} `UNKNOWN_KEY`: the key is not present in the source resource.
1700
+ * @throws {@link SdkError} `PROVIDER_CONSTRUCTION_FAILED`: the provider could not be constructed,
1701
+ * most often because its API key environment variable is unset.
1702
+ * @throws {@link SdkError} `LOCK_CONTENDED`: the locale's write lock could not be acquired before
1703
+ * the timeout elapsed.
1369
1704
  * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
1370
- * unsupported version.
1371
- * @throws {@link SdkError} `LOCK_CONTENDED`: this locale's write lock could not be acquired before
1372
- * its timeout, because another process (another CLI run, or a second Studio write) is holding it.
1705
+ * unsupported version.
1706
+ * @throws `ProviderError` `INVALID_RESPONSE`: the provider returned no value for the key. Provider
1707
+ * transport and rate-limit failures propagate as `ProviderError` too, since a single-key call has
1708
+ * no per-locale summary to record them on.
1373
1709
  */
1374
1710
  declare function retranslateEntry(input: RetranslateEntryInput, deps?: RetranslateEntryDeps): Promise<RetranslateEntryResult>;
1375
1711
 
1376
- /** A stable code for an SDK-originated notice (not a provider notice). */
1712
+ /**
1713
+ * Conditions the SDK itself reports on a locale, as opposed to the {@link ProviderNotice} codes a
1714
+ * provider raises. Each marks a run that completed but did something a caller may want to know
1715
+ * about.
1716
+ *
1717
+ * - `PLURAL_CATEGORIES_INCOMPLETE`: plural generation could not produce every category the target
1718
+ * language requires, so the entry is written with the categories that were produced.
1719
+ * - `SUB_BATCH_FAILED`: one sub-batch of a locale failed while others succeeded. The locale is
1720
+ * reported as `partial` rather than failed.
1721
+ * - `BLANK_ROW_BASELINE_RETAINED`: an imported handoff row was blank, so the existing translation
1722
+ * and its lock-file baseline were kept rather than being erased.
1723
+ * - `BUDGET_TOKENS_EXCEEDED`: the configured token budget was passed. Under `warn` the run
1724
+ * continues; under `stop` the remaining keys are withheld.
1725
+ * - `CACHE_VERSION_UNRECOGNIZED`: the translation memory is at a version this release does not
1726
+ * understand, so it was ignored rather than trusted.
1727
+ */
1377
1728
  type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED" | "CACHE_VERSION_UNRECOGNIZED";
1378
- /** Summed token usage across every provider call in a scope (one locale, or the whole run). */
1729
+ /**
1730
+ * Token usage as reported by the provider. Absent when the provider does not report usage, which is
1731
+ * the case for machine-translation APIs such as DeepL that do not bill in tokens.
1732
+ */
1379
1733
  interface UsageSummary {
1734
+ /** Tokens consumed by the prompts sent to the provider. */
1380
1735
  readonly inputTokens: number;
1736
+ /** Tokens consumed by the provider's responses. */
1381
1737
  readonly outputTokens: number;
1382
1738
  }
1383
- /** The configured token-budget behavior: warn and continue, or stop making further provider calls. */
1384
- type BudgetBehavior = "warn" | "stop";
1385
1739
  /**
1386
- * The run-wide token-budget outcome, present on {@link RunSummary} only when `maxTokens` is configured.
1387
- * `supported: false` means the configured provider never reported usage in this run (a token-less
1388
- * provider such as DeepL, or a dry-run): the guardrail is honestly inert, `tokensUsed` stays `0`, and
1389
- * `exceeded` stays `false`, never a false trip.
1740
+ * What a run does when it passes its token budget: `warn` finishes the work and reports the
1741
+ * overrun, `stop` withholds the remaining keys.
1390
1742
  */
1743
+ type BudgetBehavior = "warn" | "stop";
1744
+ /** The token budget in force for a run, and how much of it was actually consumed. */
1391
1745
  interface RunBudget {
1392
- /** The configured ceiling. */
1746
+ /** The configured ceiling, in tokens. */
1393
1747
  readonly maxTokens: number;
1394
- /** The configured behavior once the ceiling is reached. */
1748
+ /** Whether passing the ceiling warns or stops the run. */
1395
1749
  readonly behavior: BudgetBehavior;
1396
- /** Whether the configured provider reported usage at all this run. */
1750
+ /**
1751
+ * Whether the configured provider reports usage at all. When false the budget cannot be enforced,
1752
+ * because there is nothing to count.
1753
+ */
1397
1754
  readonly supported: boolean;
1398
- /** Cumulative input plus output tokens across the run so far. */
1755
+ /** Tokens consumed across the whole run so far. */
1399
1756
  readonly tokensUsed: number;
1400
- /** Whether `tokensUsed` reached or passed `maxTokens` at any point this run. */
1757
+ /** True once `tokensUsed` passed `maxTokens`. */
1401
1758
  readonly exceeded: boolean;
1402
1759
  }
1403
- /**
1404
- * A notice raised by the SDK itself (not a provider), structurally identical to a {@link ProviderNotice}
1405
- * so both share the {@link LocaleSummary.notices} channel. Carries only a stable code and a static,
1406
- * secret-free message; never a key value or translatable content.
1407
- */
1760
+ /** A condition the SDK reported on a locale. See {@link SdkNoticeCode}. */
1408
1761
  interface SdkNotice {
1409
- /** The stable {@link SdkNoticeCode} for what the SDK is reporting. */
1762
+ /** The stable notice code. Branch on this, not on the message. */
1410
1763
  readonly code: SdkNoticeCode;
1411
- /** A static, safe description; never a key or translatable content. */
1764
+ /** A human-readable description of the condition. */
1412
1765
  readonly message: string;
1413
1766
  }
1414
- /** A notice on a locale summary: either a provider-emitted notice or an SDK-emitted one. */
1767
+ /**
1768
+ * Any notice attached to a locale: either one the provider raised, such as a downgraded formality
1769
+ * or an ignored glossary, or one the SDK raised. Discriminate on `code`.
1770
+ */
1415
1771
  type LocaleNotice = ProviderNotice | SdkNotice;
1416
- /** One key flagged for human review, and every reason code that applies to it. */
1772
+ /**
1773
+ * A translated key the provider layer flagged as worth a human look. The translation was still
1774
+ * written; these are advisory quality signals, not rejections.
1775
+ */
1417
1776
  interface NeedsReviewEntry {
1418
- /** The flagged key. */
1777
+ /** The key that was flagged. */
1419
1778
  readonly key: string;
1420
- /** Every {@link ReviewReasonCode} that applies to this key. */
1779
+ /** Why it was flagged. A key can carry more than one reason. */
1421
1780
  readonly reasons: readonly ReviewReasonCode[];
1422
1781
  }
1423
- /**
1424
- * One row a workbook import could not read: the 1-based row number, the file line for a delimited
1425
- * import, and the header label of the offending column. Carries no cell content, so untrusted workbook
1426
- * text never reaches the summary.
1427
- */
1782
+ /** A row of an imported handoff that could not be read. Reported rather than aborting the import. */
1428
1783
  interface MalformedRowReport {
1429
- /** The 1-based worksheet row number of the malformed row. */
1784
+ /** The row's 1-based index within its sheet or file. */
1430
1785
  readonly row: number;
1431
1786
  /**
1432
- * The 1-based file line the malformed record starts on. Present only for a delimited (csv or tsv)
1433
- * import; an xlsx workbook has rows rather than lines. See {@link DuplicateKeyReport.line} for why
1434
- * both numbers are reported.
1787
+ * The 1-based physical line in the source file, for delimited formats where a quoted value can
1788
+ * span several lines. Absent for `.xlsx`, which has rows but no lines.
1435
1789
  */
1436
1790
  readonly line?: number;
1437
- /** The header label of the column the row was rejected on (for example "Status"). */
1791
+ /** The column whose value was missing or unusable. */
1438
1792
  readonly column: string;
1439
1793
  }
1440
1794
  /**
1441
- * One duplicate-key conflict a workbook import found in a locale's sheet: the duplicated key and the
1442
- * 1-based worksheet row number of the later (losing) occurrence. The first occurrence is the winner.
1795
+ * A key that appeared more than once in an imported handoff. The first occurrence wins and the rest
1796
+ * are reported here, so a translator who duplicated a row learns which value was actually used.
1443
1797
  */
1444
1798
  interface DuplicateKeyReport {
1445
- /** The key that appeared more than once in the locale's sheet. */
1799
+ /** The key that appeared more than once. */
1446
1800
  readonly key: string;
1447
- /** The 1-based worksheet row number of the later occurrence that lost to the first. */
1801
+ /** The 1-based row index of the ignored occurrence. */
1448
1802
  readonly row: number;
1449
- /**
1450
- * The 1-based file line this later occurrence starts on. Present only for a delimited (csv or tsv)
1451
- * import.
1452
- *
1453
- * A delimited record holding a quoted line break covers one spreadsheet row but several editor
1454
- * lines, so `row` is the number a translator sees in a spreadsheet and `line` the one they see in a
1455
- * text editor. Both are reported so the position holds whichever tool they opened the file in.
1456
- */
1803
+ /** The 1-based physical line of the ignored occurrence, for delimited formats. Absent for `.xlsx`. */
1457
1804
  readonly line?: number;
1458
1805
  }
1459
- /** Structured outcome for one target locale; surfaced as data on the run, never thrown. */
1806
+ /**
1807
+ * Everything that happened for one locale during a run. The key lists are disjoint accounts of what
1808
+ * became of each key, so a caller can reconstruct the whole run without re-reading any file.
1809
+ *
1810
+ * A locale that failed outright still appears here with `status: "failed"` and an `error`, rather
1811
+ * than the whole run throwing. That is the central contract of {@link translate}: one unreachable
1812
+ * provider or one unwritable file does not discard the locales that succeeded.
1813
+ */
1460
1814
  interface LocaleSummary {
1461
- /** The target locale this summary is for. */
1815
+ /** The target locale this summary describes. */
1462
1816
  readonly locale: string;
1463
1817
  /**
1464
- * This locale's honest run status (a non-success does not abort the run). `"succeeded"` when
1465
- * nothing was withheld, including a genuine no-op with no candidate keys at all. `"partial"` when
1466
- * at least one key was accepted and written (a {@link LocaleSummary.translated} key or a
1467
- * {@link LocaleSummary.generated} plural form) but at least one was withheld (any of
1468
- * {@link LocaleSummary.integrityMismatches}, {@link LocaleSummary.providerFailures}, or
1469
- * {@link LocaleSummary.budgetWithheld} non-empty). `"failed"` when the locale had candidate keys
1470
- * but accepted nothing at all (neither translated nor generated) while withholding at least one.
1471
- * Withheld keys keep their prior lock hash and retry next run.
1472
- *
1473
- * A workbook import's structural findings, {@link LocaleSummary.unfilled},
1474
- * {@link LocaleSummary.malformedRows}, and {@link LocaleSummary.duplicateKeys}, deliberately do NOT
1475
- * feed this status: they are surfaced in their own lists (and the CLI) but are not withholdings of
1476
- * an attempted translation, so a locale that dropped a malformed row, collapsed a duplicate key, or
1477
- * left a row unfilled while accepting its other rows can still be `"succeeded"`.
1478
- *
1479
- * This is a settled decision, not an omission. Recording the reasoning here so the question stops
1480
- * recurring:
1481
- *
1482
- * 1. Two questions, two commands. `import`'s exit code answers "did the import apply what the
1483
- * translator gave me". `check` and `diff` answer "is this project fully translated", and both
1484
- * already exit non-zero or report pending in every one of these cases. Folding pending human
1485
- * work into the import exit code duplicates an existing gate rather than adding one.
1486
- * 2. It would break the staged handoff. With `de` and `fr` configured and only `de` filled in,
1487
- * failing on unfilled work makes `fr` fail and takes the whole import with it, even though `de`
1488
- * imported perfectly. Locale-at-a-time handoff is this feature's most common workflow.
1489
- * 3. {@link LocaleSummary.malformedRows} structurally cannot signal lost work. The Status column is
1490
- * the only reachable rejection in the workbook reader; every other field is an unconstrained
1491
- * string or a tolerant parse. So a row is malformed on its Status cell alone, entirely
1492
- * independently of whether its Translation cell was filled. An untouched sheet with a mangled
1493
- * Status column (spreadsheet autocorrect, a CAT-tool round trip) is all-malformed with zero lost
1494
- * work. A bucket that cannot distinguish dropped work from absent work cannot honestly drive a
1495
- * failure status.
1496
- * 4. {@link LocaleSummary.duplicateKeys} never could: the first occurrence wins and the intent is
1497
- * honoured.
1818
+ * `succeeded` when everything asked for was done, `partial` when some keys were translated and
1819
+ * others were not, and `failed` when the locale produced no usable result.
1498
1820
  */
1499
1821
  readonly status: "succeeded" | "partial" | "failed";
1500
- /**
1501
- * Keys translated and written this run. In dry-run, the keys that would be translated
1502
- * (the provider is not called and nothing is written).
1503
- */
1822
+ /** Keys newly translated by the provider in this run. */
1504
1823
  readonly translated: readonly string[];
1505
- /** Keys already up to date, left unchanged this run. */
1824
+ /** Keys already up to date against the lock-file baseline, so no provider call was made. */
1506
1825
  readonly unchanged: readonly string[];
1507
- /** Target keys with no corresponding source key (candidates for removal). Reported regardless of pruning. */
1826
+ /** Keys present in this locale but no longer in the source. Reported, and removed only when pruning. */
1508
1827
  readonly orphaned: readonly string[];
1509
- /**
1510
- * Orphaned keys actually removed this run because pruning was on. In a dry-run with pruning on, the keys
1511
- * that would be removed. Empty when pruning is off (the orphans then survive and are reported in
1512
- * `orphaned` only). A subset of `orphaned`; never includes a source-present key.
1513
- */
1828
+ /** Orphaned keys that were actually removed, which happens only when the run was asked to prune. */
1514
1829
  readonly pruned: readonly string[];
1515
- /** Source keys flagged invalid-ICU that were skipped for translation this run. */
1516
- readonly invalidIcuSource: readonly string[];
1517
1830
  /**
1518
- * Keys served from the local translation-memory cache instead of the provider: their source content
1519
- * was unchanged (possibly under a renamed key or shared with another key) and the cached value passed
1520
- * the integrity gate, so no provider request was made for them. They are written and lock-advanced
1521
- * exactly like a translated key, but kept distinct from {@link LocaleSummary.translated} because they
1522
- * cost no provider usage. Always present, empty by default; empty in a dry-run and when the cache is
1523
- * bypassed. Sorted by key.
1831
+ * Keys whose source text is not valid ICU. They are skipped rather than sent to the provider,
1832
+ * because a broken source message cannot yield a sound translation.
1524
1833
  */
1834
+ readonly invalidIcuSource: readonly string[];
1835
+ /** Keys served from the translation memory instead of the provider, and so not paid for. */
1525
1836
  readonly cacheHits: readonly string[];
1526
1837
  /**
1527
- * Translated keys that failed the placeholder-integrity check and were withheld. Never includes a
1528
- * key withheld because the provider call itself failed; see {@link LocaleSummary.providerFailures}
1529
- * for that case.
1838
+ * Keys whose translation was refused by the integrity gate, for instance because it dropped a
1839
+ * placeholder. The previous translation, if any, is left untouched.
1530
1840
  */
1531
1841
  readonly integrityMismatches: readonly string[];
1532
- /**
1533
- * Keys withheld because nothing was translated for them this run, as distinct from a translation
1534
- * that came back and failed the placeholder-integrity check. This covers two causes: the provider
1535
- * call itself failed (for example a revoked API key, a rate limit, or a network timeout), in which
1536
- * case the corresponding {@link LocaleSummary.notices} entry carries the secret-free failure code
1537
- * and message; or the call succeeded but the key was still missing or duplicated in the response
1538
- * after the shared LLM layer's bounded reconcile repair round, in which case no notice is added.
1539
- * Empty in a dry-run.
1540
- */
1842
+ /** Keys the provider failed to translate, for instance because their sub-batch errored. */
1541
1843
  readonly providerFailures: readonly string[];
1542
- /**
1543
- * Plural-category keys verbatra synthesized this run (for example a Polish `items_few` the source
1544
- * never supplied), kept distinct from {@link translated}. Empty unless plural generation was enabled
1545
- * and acted, and empty in a dry-run.
1546
- */
1844
+ /** Keys whose plural categories were generated for the target language rather than translated one by one. */
1547
1845
  readonly generated: readonly string[];
1548
- /**
1549
- * Candidate keys never sent to the provider because a configured `maxTokens` budget already tripped
1550
- * in `"stop"` mode: keys remaining in the current locale once the ceiling was crossed, and every
1551
- * candidate key of a later, fully-skipped locale. Always present, empty by default, same convention as
1552
- * {@link LocaleSummary.integrityMismatches} and {@link LocaleSummary.providerFailures}. Each key keeps
1553
- * its prior lock hash and is picked up again next run, exactly like a `providerFailures` key.
1554
- */
1846
+ /** Keys not translated because the token budget was exhausted under `stop` behavior. */
1555
1847
  readonly budgetWithheld: readonly string[];
1556
- /**
1557
- * Summed token usage across every provider call for this locale (main translation plus plural
1558
- * generation). Absent when no call in this locale reported usage: a dry-run, a token-less provider
1559
- * (DeepL), or a locale with nothing to translate. Never a fabricated `{ inputTokens: 0, outputTokens: 0 }`.
1560
- */
1848
+ /** Token usage for this locale. Absent when the provider does not report usage. */
1561
1849
  readonly usage?: UsageSummary;
1562
- /**
1563
- * Notices for this locale: provider notices (e.g. DeepL graceful-degradation) and SDK notices
1564
- * (e.g. a target language needing more CLDR plural categories than the source supplies). Empty when
1565
- * nothing was degraded or flagged.
1566
- */
1850
+ /** Provider and SDK notices raised while running this locale. Always present, possibly empty. */
1567
1851
  readonly notices: readonly LocaleNotice[];
1568
- /**
1569
- * Keys accepted and written this run that the review heuristics flagged for a second look, sorted
1570
- * by key. Distinct from the placeholder/ICU integrity gate: a review flag is advisory only and never
1571
- * withholds a key, so this never double-reports a key already surfaced through
1572
- * {@link LocaleSummary.integrityMismatches} or {@link LocaleSummary.providerFailures}. Empty when
1573
- * nothing was flagged, always empty for a workbook import (which never calls a provider or recomputes
1574
- * flags on its own path).
1575
- */
1852
+ /** Translated keys flagged as worth a human look. The translations were still written. */
1576
1853
  readonly needsReview: readonly NeedsReviewEntry[];
1577
- /**
1578
- * Keys the translator left blank that still need a translation, drifted or not, sorted by key.
1579
- * Pending work made visible: the row is skipped (nothing is written and the prior lock baseline is
1580
- * kept), but the key is surfaced so an unfinished import is never silently reported as done. Only a
1581
- * workbook import populates this; the provider path, which never leaves a candidate key unfilled by
1582
- * a person, always leaves it empty.
1583
- *
1584
- * Membership is decided by the import-time diff, not by the `status` the row was exported with: a
1585
- * never-translated key exports as `new`, and a first handoff is all-new, which is the most common
1586
- * unfilled case there is. A row exported as `changed` whose key no longer needs work by import time
1587
- * is correspondingly excluded.
1588
- */
1854
+ /** Keys left with no translation after the run, whatever the cause. */
1589
1855
  readonly unfilled: readonly string[];
1590
- /**
1591
- * Rows a workbook import could not read for this locale, each by worksheet row number and offending
1592
- * column, in row order. The sheet's other rows still import; a malformed row is reported, not
1593
- * written. Only a workbook import populates this; the provider path always leaves it empty.
1594
- */
1856
+ /** Unreadable rows from an imported handoff. Always empty for a {@link translate} run. */
1595
1857
  readonly malformedRows: readonly MalformedRowReport[];
1596
- /**
1597
- * Duplicate-key conflicts a workbook import found in this locale's sheet, in row order. The rule is
1598
- * first occurrence wins: the first row for a key is judged and its later occurrences are reported
1599
- * here and otherwise ignored. Only a workbook import populates this; the provider path always leaves
1600
- * it empty.
1601
- */
1858
+ /** Repeated keys from an imported handoff. Always empty for a {@link translate} run. */
1602
1859
  readonly duplicateKeys: readonly DuplicateKeyReport[];
1603
1860
  /**
1604
- * A structured, secret-free error for a locale that threw (an adapter/lock/provider-construction
1605
- * failure isolated as data): present only on a "failed" locale, and only that throw path sets it.
1606
- * A locale that is "failed" because every candidate key was withheld (all under `providerFailures`,
1607
- * `integrityMismatches`, or `budgetWithheld`) carries those lists and any notices instead, and has
1608
- * no `error`. `code` is a preserved string (the underlying provider/adapter error's `code`, or
1609
- * `"LOCALE_FAILED"` as a fallback), intentionally wider than {@link SdkErrorCode}, so do not treat
1610
- * it as a closed set.
1861
+ * Why this locale failed, when the failure came from a thrown error. The `code` is the failure's
1862
+ * own code where it has one, and `LOCALE_FAILED` otherwise. Never carries a secret.
1863
+ *
1864
+ * Absent unless `status` is `failed`, but a `failed` locale does not always carry it: a locale
1865
+ * whose every key was withheld by the integrity gate, a provider failure, or the token budget is
1866
+ * `failed` with nothing thrown, so this stays undefined and the withheld keys are the account of
1867
+ * what went wrong. Treat it as an optional detail on a failure, never as the failure test.
1611
1868
  */
1612
1869
  readonly error?: {
1870
+ /** The failure's own code where it had one, and `LOCALE_FAILED` otherwise. */
1613
1871
  readonly code: string;
1872
+ /** A human-readable description of the failure. Never contains a secret. */
1614
1873
  readonly message: string;
1615
1874
  };
1616
1875
  }
1617
- /** The aggregate result of a run across all target locales. */
1876
+ /**
1877
+ * The result of a whole run, returned by {@link translate}, {@link watch}, and
1878
+ * {@link importWorkbook}.
1879
+ *
1880
+ * Per-locale outcomes are data, not exceptions: inspect `failed` and `partial` to decide an exit
1881
+ * code rather than relying on the call to throw. Only whole-run failures, such as an invalid config
1882
+ * or an unreadable source file, throw an {@link SdkError}.
1883
+ */
1618
1884
  interface RunSummary {
1619
- /** Whether this was a dry-run (no provider calls, no writes). */
1885
+ /** True when the run computed everything but wrote nothing and called no provider. */
1620
1886
  readonly dryRun: boolean;
1621
- /** One {@link LocaleSummary} per target locale, in config order. */
1887
+ /** The full per-locale account, in configured target order. */
1622
1888
  readonly locales: readonly LocaleSummary[];
1623
- /** Locales whose run succeeded with nothing withheld (status `"succeeded"`). */
1889
+ /** Names of the locales whose status is `succeeded`. */
1624
1890
  readonly succeeded: readonly string[];
1625
- /**
1626
- * Locales that wrote at least one translation but withheld at least one candidate key (status
1627
- * `"partial"`). A partial locale still exits the CLI `0`: it made progress, and its withheld keys
1628
- * retry next run. Never overlaps `succeeded` or `failed`.
1629
- */
1891
+ /** Names of the locales whose status is `partial`. */
1630
1892
  readonly partial: readonly string[];
1631
- /** Locales whose run failed, having accepted nothing (status `"failed"`; see each locale's `error`). */
1893
+ /** Names of the locales whose status is `failed`. Check this before treating a run as clean. */
1632
1894
  readonly failed: readonly string[];
1633
- /**
1634
- * Summed token usage across every locale's {@link LocaleSummary.usage}. Absent when no locale in the
1635
- * run reported usage (a dry-run, or an all-token-less-provider run).
1636
- */
1895
+ /** Token usage summed across every locale. Absent when the provider does not report usage. */
1637
1896
  readonly usage?: UsageSummary;
1638
- /** The run-wide token-budget outcome; present only when `maxTokens` is configured. */
1897
+ /** The token budget in force, present only when the config set one. */
1639
1898
  readonly budget?: RunBudget;
1640
1899
  }
1641
1900
 
1642
1901
  /**
1643
- * One target locale's snapshot inside the persisted run-status file. `status` is carried over from
1644
- * `LocaleSummary.status` unchanged, so an empty `needsReview` on a failed or partial locale is never
1645
- * misread as "this locale ran clean with nothing to review".
1902
+ * One locale's entry in a persisted {@link RunStatusFile}. It is a deliberately narrow projection
1903
+ * of {@link LocaleSummary}: only the parts a later session still needs, namely the outcome, the
1904
+ * review flags, and the cost.
1646
1905
  */
1647
1906
  interface RunStatusLocale {
1907
+ /** The target locale this entry describes. */
1648
1908
  readonly locale: string;
1909
+ /** How the locale finished in the recorded run. */
1649
1910
  readonly status: "succeeded" | "partial" | "failed";
1650
- /** The full, unmodified list from `LocaleSummary.needsReview`; already bounded and content-free. */
1911
+ /** Keys the run flagged as worth a human look. */
1651
1912
  readonly needsReview: readonly NeedsReviewEntry[];
1652
- /** Absent exactly when the source `LocaleSummary.usage` was absent; never a fabricated zero. */
1913
+ /** Token usage for this locale. Absent when the provider does not report usage. */
1653
1914
  readonly usage?: UsageSummary;
1654
1915
  }
1655
1916
  /**
1656
- * The persisted shape of `.verbatra-local/run-status.json`: one whole-run snapshot, overwritten on
1657
- * every non-dry-run `translate()`/`watch()` run that reaches the end of its per-locale loop. Contains
1658
- * only the review-flag and token/usage data already computed for `RunSummary`; never a translation
1659
- * string, a provider error message, or a provider notice message.
1917
+ * The snapshot a completed non-dry-run writes to `.verbatra-local/run-status.json`, read back by
1918
+ * {@link runStatus}. It lets a tool started after the run, such as a dashboard opened once
1919
+ * translation finished, still show what was flagged and what it cost.
1920
+ *
1921
+ * It is local state, not project state: unlike the lock-file it is not meant to be committed, and
1922
+ * losing it costs nothing beyond that history.
1660
1923
  */
1661
1924
  interface RunStatusFile {
1925
+ /** The status-file schema version. A file at an unrecognized version is ignored rather than trusted. */
1662
1926
  readonly version: number;
1663
- /** ISO timestamp of when this snapshot was written. */
1927
+ /** When the run finished, as an ISO 8601 timestamp. */
1664
1928
  readonly generatedAt: string;
1665
- /** Summed run-wide token usage, mirroring `RunSummary.usage`; absent under the same conditions. */
1929
+ /** Token usage summed across every locale. Absent when the provider does not report usage. */
1666
1930
  readonly usage?: UsageSummary;
1667
- /** The run-wide token-budget outcome, mirroring `RunSummary.budget`; present only when configured. */
1931
+ /** The token budget in force during the run, present only when one was configured. */
1668
1932
  readonly budget?: RunBudget;
1933
+ /** Per-locale outcomes from the recorded run. */
1669
1934
  readonly locales: readonly RunStatusLocale[];
1670
1935
  }
1671
1936
 
1672
- /** Input for {@link runStatus}: the persisted file is self-contained, so no `config` is needed. */
1937
+ /** Input for {@link runStatus}. */
1673
1938
  interface RunStatusInput {
1674
- /** Directory the run-status file resolves against; defaults to cwd. */
1939
+ /** Directory holding the `.verbatra-local` status directory. Defaults to the process working directory. */
1675
1940
  readonly cwd?: string;
1676
1941
  }
1677
- /** Composition seam for {@link runStatus}: inject a file system for tests. */
1942
+ /** Injectable dependencies for {@link runStatus}. Every field has a working default. */
1678
1943
  interface RunStatusDeps {
1944
+ /** File-system port. Defaults to the real file system. */
1679
1945
  readonly fs?: SdkFs;
1680
1946
  }
1681
1947
  /**
1682
- * The persisted run-status snapshot, or its absence. `available: false` covers a project that has
1683
- * never run a non-dry-run `translate()`/`watch()`, exactly like every other degrade case
1684
- * {@link runStatus} folds into it: never a distinct error, so a caller does not need to special-case
1685
- * "first run" apart from "corrupt file" apart from "unrecognized version".
1948
+ * The result of {@link runStatus}. Having no readable status file is a normal state, not an error:
1949
+ * it simply means no non-dry-run has completed in this directory yet.
1686
1950
  */
1687
1951
  type RunStatusResult = {
1952
+ /** No usable status file was found, so there is nothing to report. */
1688
1953
  readonly available: false;
1689
1954
  } | ({
1955
+ /** A status file was found and parsed; the {@link RunStatusFile} fields are spread alongside. */
1690
1956
  readonly available: true;
1691
1957
  } & RunStatusFile);
1692
1958
  /**
1693
- * Read the most recent run's persisted review-flag and token/usage snapshot from
1694
- * `.verbatra-local/run-status.json`, without calling a provider, writing any file, or mutating the
1695
- * lock-file. Never throws: a missing file, invalid JSON, a schema mismatch, or an unrecognized
1696
- * `version` all degrade to `{ available: false }`, since this file is best-effort telemetry, not a
1697
- * correctness baseline (contrast {@link lockState}, which can throw `LOCK_FILE_INVALID`).
1959
+ * Reads the review-flag and token-usage snapshot that the last non-dry-run {@link translate} or
1960
+ * {@link watch} left behind in `.verbatra-local/run-status.json`. It writes nothing and calls no
1961
+ * provider.
1962
+ *
1963
+ * This exists so a tool started after a run, such as a dashboard opened once translation finished,
1964
+ * can still show which keys were flagged for review and what the run cost, without re-running
1965
+ * anything.
1966
+ *
1967
+ * The read is deliberately total: a missing, oversized, unparseable, schema-invalid, or
1968
+ * wrong-version file all report `available: false` rather than throwing, because stale local status
1969
+ * should never break the tool reading it. This call throws nothing.
1698
1970
  *
1699
- * @param input - Only the directory the file resolves against; no config is needed.
1700
- * @param deps - Optional composition seam (file system) for tests.
1701
- * @returns `{ available: false }`, or `{ available: true, ...the persisted fields }`.
1971
+ * @param input - The optional working directory.
1972
+ * @param deps - Optional file-system override.
1973
+ * @returns The persisted run status, or `available: false` when none is usable.
1702
1974
  */
1703
1975
  declare function runStatus(input?: RunStatusInput, deps?: RunStatusDeps): Promise<RunStatusResult>;
1704
1976
 
1705
- /** Diagnostic details, read leniently from a held lock file, about the process currently holding it. */
1977
+ /**
1978
+ * Whatever could be read about the process currently holding a write lock. Both fields are optional
1979
+ * because a lock file left behind by a killed process may be truncated or unreadable, and reporting
1980
+ * a partial holder is more useful than reporting none.
1981
+ */
1706
1982
  interface LockHolder {
1707
- /** The holder's process id, when the lock file could be read and carried a numeric `pid`. */
1983
+ /** The process ID recorded when the lock was taken. */
1708
1984
  readonly pid?: number;
1709
- /** The holder's acquire time as an ISO-8601 string, when the lock file carried a string `acquiredAt`. */
1985
+ /** When the lock was taken, as an ISO 8601 timestamp. */
1710
1986
  readonly acquiredAt?: string;
1711
1987
  }
1712
- /** One wait-progress notification emitted while an acquire is blocked on a held lock. */
1988
+ /**
1989
+ * Reported while a run waits for another process to release a locale's write lock. It exists so a
1990
+ * CLI or UI can explain a stall rather than appearing to hang, and can name the lock file a user
1991
+ * may need to delete after a crash.
1992
+ */
1713
1993
  interface LockWaitEvent {
1714
- /** The on-disk path of the contended lock file, the exact path a person can delete if it is orphaned. */
1994
+ /** Absolute path of the lock file being waited on. */
1715
1995
  readonly lockPath: string;
1716
- /** Milliseconds elapsed since this acquire started waiting, for a "still waiting" progress line. */
1996
+ /** How long this acquisition has been waiting, in milliseconds. */
1717
1997
  readonly elapsedMs: number;
1718
- /**
1719
- * Diagnostics about the holding process, present only when the lock file could be read and parsed.
1720
- * Purely for messaging; never consulted for any acquire decision.
1721
- */
1998
+ /** What is known about the holding process, when anything could be read. */
1722
1999
  readonly holder?: LockHolder;
1723
2000
  }
1724
- /** Called while an acquire is blocked, to surface wait progress; the lock module itself writes no output. */
2001
+ /**
2002
+ * Called on each poll while waiting for a locale's write lock. Passed as `onLockWait` to
2003
+ * {@link translate} and {@link watch}.
2004
+ */
1725
2005
  type LockWaitListener = (event: LockWaitEvent) => void;
1726
2006
 
1727
2007
  /**
1728
- * One structured progress notification emitted while a run advances through its locales and their
1729
- * provider sub-batches. Emitted to a caller-supplied {@link ProgressListener}; the SDK itself writes
1730
- * nothing to any stream, so a caller (the CLI) is free to render these to stderr and keep stdout a
1731
- * clean summary or NDJSON stream. Discriminated on `type`, mirroring the lock-wait event contract.
2008
+ * Any event a run emits while it works. Discriminate on `type`. Progress is reported rather than
2009
+ * printed so that a CLI, a dashboard, and a CI log can each render it in their own way.
1732
2010
  */
1733
2011
  type ProgressEvent = LocaleStartedEvent | SubBatchProgressEvent | LocaleFinishedEvent | RunFinishedEvent;
1734
- /** A locale is about to be processed: its position in the run's target-locale order. */
2012
+ /** Emitted when a locale's work begins. */
1735
2013
  interface LocaleStartedEvent {
2014
+ /** Discriminant for {@link ProgressEvent}. */
1736
2015
  readonly type: "locale-started";
1737
- /** The target locale about to run. */
2016
+ /** The locale starting. */
1738
2017
  readonly locale: string;
1739
- /** Zero-based index of this locale in the run's target-locale order. */
2018
+ /**
2019
+ * This locale's 0-based position among the run's locales. With concurrency above 1 several
2020
+ * locales are in flight at once, so these indices do not arrive in order.
2021
+ */
1740
2022
  readonly localeIndex: number;
1741
- /** Total number of target locales in this run. */
2023
+ /** How many locales the run covers in total. */
1742
2024
  readonly totalLocales: number;
1743
2025
  }
1744
2026
  /**
1745
- * One sub-batch of a locale's main translation loop has been reached. Emitted once per loop
1746
- * iteration, in order, so a caller can render a "batch N of M" line. Fired at the top of the
1747
- * iteration regardless of whether the batch then makes a provider call: a budget-withheld batch
1748
- * (the run's token budget already tripped) still emits this event, which keeps `batchIndex` 1-based
1749
- * and contiguous against the total. A dry-run never reaches this loop and so never emits it.
2027
+ * Emitted as each sub-batch of a locale is reached, immediately before the provider call for it is
2028
+ * attempted. Large locales are split into batches, so this is the finer-grained signal to drive a
2029
+ * progress bar with.
2030
+ *
2031
+ * It announces an attempt rather than confirming a send: a batch withheld because the token budget
2032
+ * has already stopped the run still emits, and no provider call follows it. That is deliberate, so
2033
+ * that `batchIndex` always advances to `totalBatches` and a progress bar reaches its end instead of
2034
+ * stalling at the point the budget tripped.
1750
2035
  */
1751
2036
  interface SubBatchProgressEvent {
2037
+ /** Discriminant for {@link ProgressEvent}. */
1752
2038
  readonly type: "sub-batch";
1753
- /** The locale whose entries this sub-batch carries. */
2039
+ /** The locale this batch belongs to. */
1754
2040
  readonly locale: string;
1755
- /** One-based index of this sub-batch within the locale. */
2041
+ /** This batch's 1-based position within the locale, counting up to `totalBatches`. */
1756
2042
  readonly batchIndex: number;
1757
- /** Total number of sub-batches this locale's main translation loop runs. */
2043
+ /** How many batches the locale was split into. */
1758
2044
  readonly totalBatches: number;
1759
2045
  }
1760
- /** A locale finished processing: how many keys it accepted (translated). */
2046
+ /** Emitted when a locale's work ends, whether it succeeded, partially succeeded, or failed. */
1761
2047
  interface LocaleFinishedEvent {
2048
+ /** Discriminant for {@link ProgressEvent}. */
1762
2049
  readonly type: "locale-finished";
1763
- /** The target locale that finished. */
2050
+ /** The locale that finished. */
1764
2051
  readonly locale: string;
1765
- /** Number of keys accepted (translated) for this locale. In a dry-run, the keys that would be translated. */
2052
+ /** How many keys were newly translated for this locale. */
1766
2053
  readonly translated: number;
1767
- /**
1768
- * Zero-based index of this locale in the run's target-locale order, carrying the same value as the
1769
- * matching {@link LocaleStartedEvent}. This is a correlation key, so a consumer can pair a finish
1770
- * with its start without matching on the locale name.
1771
- *
1772
- * It is deliberately NOT a progress counter. The worker pool claims indices up front, so this is a
1773
- * claim ordinal rather than a completion ordinal: under concurrency the locales finish in a
1774
- * different order, and rendering `localeIndex + 1` of `totalLocales` on a finish produces a
1775
- * counter that jumps around and never reaches its own total in order. Do not reintroduce one.
1776
- */
2054
+ /** This locale's 0-based position among the run's locales. */
1777
2055
  readonly localeIndex: number;
1778
- /** Total number of target locales in this run, mirroring {@link LocaleStartedEvent}. */
2056
+ /** How many locales the run covers in total. */
1779
2057
  readonly totalLocales: number;
1780
2058
  }
1781
- /** The run reached the end of its locale loop: how many locales it processed. */
2059
+ /** Emitted once, after every locale has finished. */
1782
2060
  interface RunFinishedEvent {
2061
+ /** Discriminant for {@link ProgressEvent}. */
1783
2062
  readonly type: "run-finished";
1784
- /**
1785
- * Number of locales the run processed (including failed locales: a per-locale failure is isolated
1786
- * as a summary rather than thrown, so it is still counted here).
1787
- */
2063
+ /** How many locales completed, including those that failed. */
1788
2064
  readonly localesCompleted: number;
1789
2065
  }
1790
2066
  /**
1791
- * Called with each {@link ProgressEvent} as a run advances; the SDK emits no output of its own. A
1792
- * caller (the CLI) uses this to render progress, always to stderr, so stdout stays byte-identical to
1793
- * a run with no listener attached.
2067
+ * Called for each {@link ProgressEvent} a run emits. Passed as `onProgress` to {@link translate}
2068
+ * and {@link watch}.
1794
2069
  */
1795
2070
  type ProgressListener = (event: ProgressEvent) => void;
1796
2071
 
1797
- /** Everything the one-shot run needs: the validated config and where/how to run it. */
2072
+ /** Input for {@link translate}. Only `config` is required; every other field has a default. */
1798
2073
  interface TranslateInput {
1799
- /** The validated configuration (typically from {@link loadConfig}). */
2074
+ /** The resolved project config, normally from {@link loadConfig}. */
1800
2075
  readonly config: VerbatraConfig;
1801
- /** Directory the file pattern and lock-file resolve against; defaults to the current working directory. */
2076
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
1802
2077
  readonly cwd?: string;
1803
- /** When true, read + diff + report only: the provider is never constructed or called and nothing is written. */
2078
+ /**
2079
+ * Compute the whole run but write nothing and call no provider. The returned
2080
+ * {@link RunSummary} reports what would have happened, which makes this safe to run in CI to
2081
+ * preview work without spending anything. Defaults to false.
2082
+ */
1804
2083
  readonly dryRun?: boolean;
1805
2084
  /**
1806
- * When true, remove orphaned keys (target keys absent from source) from the written file and the lock.
1807
- * Off by default. When set, takes precedence over the config's `prune` option for this run; when unset,
1808
- * the config's `prune` applies. Only `diff.orphaned` keys are ever removed; no other key is touched.
2085
+ * Remove keys that no longer exist in the source. Defaults to the config's `prune`, then to
2086
+ * false, so orphaned keys are reported but kept unless removal is asked for explicitly.
1809
2087
  */
1810
2088
  readonly prune?: boolean;
1811
2089
  /**
1812
- * When true, synthesize the CLDR plural forms a richer target language requires but the source lacks
1813
- * (i18next-JSON + LLM providers only; every other case falls back to the existing warning). Off by
1814
- * default. When set, takes precedence over the config's `generatePlurals` option for this run; when
1815
- * unset, the config's `generatePlurals` applies.
2090
+ * Generate the plural categories a target language requires rather than translating each
2091
+ * category separately. Defaults to the config's `generatePlurals`, then to false.
1816
2092
  */
1817
2093
  readonly generatePlurals?: boolean;
1818
2094
  /**
1819
- * Called while a locale's write lock is blocked on another process holding it: once after the first
1820
- * failed acquire (with the holder's pid/acquiredAt when the lock file is readable), then periodically
1821
- * with the growing elapsed time. Never called for an uncontended run. The SDK writes no output; this
1822
- * is the only wait-progress signal (the CLI renders its "still waiting" line from it).
2095
+ * Called while waiting on another process's write lock, so a CLI can explain a stall instead of
2096
+ * appearing to hang.
1823
2097
  */
1824
2098
  readonly onLockWait?: LockWaitListener;
1825
- /**
1826
- * Called as the run advances: once per locale before it starts and once after it finishes, once
1827
- * per provider sub-batch within a locale, and once when the whole locale loop ends. Never fires a
1828
- * sub-batch event on a dry-run (no provider call is made). The SDK writes no output; this is the
1829
- * only progress signal (the CLI renders it to stderr, keeping stdout byte-identical).
1830
- *
1831
- * Pairing is not guaranteed when the run throws: a whole-run failure (for example a corrupt
1832
- * lock-file surfacing as `LOCK_FILE_INVALID`, which re-throws instead of being isolated per locale)
1833
- * can emit a `locale-started` event with no matching `locale-finished`, and no `run-finished` at
1834
- * all. A per-locale failure that is isolated (not re-thrown) still emits both, and is counted in
1835
- * `run-finished`.
1836
- */
2099
+ /** Called as locales and sub-batches start and finish, for progress reporting. */
1837
2100
  readonly onProgress?: ProgressListener;
1838
- /**
1839
- * Override how long a locale's write lock keeps retrying before failing with `LOCK_CONTENDED`, in
1840
- * milliseconds. Defaults to the lock's own 10-minute default when unset (surfaced to the CLI as
1841
- * `--lock-timeout`).
1842
- */
2101
+ /** How long to wait for a locale's write lock before failing with `LOCK_CONTENDED`. */
1843
2102
  readonly lockAcquireTimeoutMs?: number;
1844
2103
  /**
1845
- * How many target locales may run at once, a positive integer (surfaced to the CLI as
1846
- * `--concurrency`). Defaults to 1, which runs locales strictly in sequence and is byte-identical
1847
- * to a run with this option unset. A value below 1 or a non-integer is rejected with a whole-run
1848
- * `CONCURRENCY_INVALID` error. On a live run, a value greater than 1 is rejected with a whole-run
1849
- * `CONCURRENCY_BUDGET_CONFLICT` error when the config sets a `maxTokens` budget, because
1850
- * concurrency would make the budget's stop guarantee nondeterministic; a dry run is exempt. The
1851
- * per-locale write locks already isolate concurrent locales on disk, so no extra locking is added.
2104
+ * How many locales to run at once. Must be an integer of at least 1; defaults to 1. On a live
2105
+ * run it cannot be combined with a configured token budget, because concurrent locales would
2106
+ * overshoot the budget nondeterministically.
1852
2107
  */
1853
2108
  readonly concurrency?: number;
1854
2109
  /**
1855
- * Whether to use the local content-addressed translation-memory cache (`verbatra.cache.json`). On by
1856
- * default; the CLI exposes it as `--no-cache`. When false, both the cache read and the end-of-run
1857
- * cache write are skipped: the run makes exactly the provider calls it would with no cache present,
1858
- * and leaves any existing cache file untouched. Ignored on a dry-run, which never reads or writes the
1859
- * cache regardless. See the cache module for the fingerprint and degrade-to-empty contract.
2110
+ * Consult and update the translation memory. Defaults to true. Turning it off forces every key
2111
+ * through the provider, which is what to do when you want to re-pay for a fresh translation.
1860
2112
  */
1861
2113
  readonly cache?: boolean;
1862
2114
  }
1863
- /** Composition seam: inject a registry, a provider builder, and a file system for tests. */
2115
+ /** Injectable dependencies for {@link translate}. Every field has a working default. */
1864
2116
  interface TranslateDeps {
1865
- /** Adapter registry to resolve the format from; defaults to the built-in registry. */
2117
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
1866
2118
  readonly adapterRegistry?: AdapterRegistry;
1867
- /** Provider builder; defaults to constructing the configured provider (which reads its key from env). */
2119
+ /** Provider factory. Defaults to constructing the provider named in the config. */
1868
2120
  readonly createProvider?: CreateProvider;
1869
- /** File system for existence checks and the lock-file; defaults to the real file system. */
2121
+ /** File-system port. Defaults to the real file system. */
1870
2122
  readonly fs?: SdkFs;
1871
2123
  }
1872
2124
  /**
1873
- * The one-shot end-to-end translate flow. Whole-run failures (config already validated
1874
- * by the caller, unknown format, provider construction, unreadable/invalid source,
1875
- * corrupt lock-file) throw a structured SdkError. Per-locale failures are isolated: a
1876
- * failing locale is reported and the run continues; the lock-file reflects exactly the
1877
- * locales that succeeded. Dry-run reads + diffs + reports without constructing/calling
1878
- * the provider and without writing any file or the lock-file.
1879
- *
1880
- * A per-locale failure does not throw: it is recorded on that locale's {@link LocaleSummary} as
1881
- * `status: "failed"` with a secret-free `{ code, message }`, where `code` is a preserved string (the
1882
- * underlying provider/adapter code, or `"LOCALE_FAILED"` as a fallback), not necessarily an
1883
- * {@link SdkErrorCode}. DeepL notices, integrity mismatches, and invalid-ICU source keys likewise
1884
- * surface on each `LocaleSummary`, never as throws.
1885
- *
1886
- * A dry-run reads the lock once, outside any lock, and reuses that one snapshot for every locale:
1887
- * it never writes, so there is nothing to serialize against. A live run instead re-reads the lock
1888
- * fresh inside each locale's own write lock (see `runLiveLocale`), so two concurrent live calls
1889
- * never both diff against the same stale pre-loop snapshot. The token-budget tracker is fresh per
1890
- * invocation: the `maxTokens` ceiling is per-run, not cumulative across watch cycles.
1891
- *
1892
- * On a non-dry-run that reaches the end of the loop, the run's review-flag and token/usage data is
1893
- * also written to `.verbatra-local/run-status.json` (readable back through {@link runStatus}). This
1894
- * write is best-effort: any failure is caught and swallowed, never re-thrown and never reflected on
1895
- * the returned {@link RunSummary}.
1896
- *
1897
- * @param input - The validated config and run options (cwd, dryRun, prune, generatePlurals).
1898
- * @param deps - Optional composition seams (registry, provider builder, file system) for tests.
1899
- * @returns A {@link RunSummary}: the per-locale {@link LocaleSummary}s and the succeeded/partial/failed locale lists.
2125
+ * Runs the one-shot translation flow over every configured target locale: read the source, diff
2126
+ * each locale against the lock-file baseline, translate what is missing or stale, verify placeholder
2127
+ * and ICU integrity, write the locale files, and update the lock-file and translation memory.
2128
+ *
2129
+ * Failure handling is the contract worth understanding. Whole-run problems, such as an unreadable
2130
+ * source file or a provider that cannot be constructed, throw an {@link SdkError} before any locale
2131
+ * runs. Once locales are running, a per-locale failure is recorded on that locale's
2132
+ * {@link LocaleSummary} and the other locales continue, so one unreachable provider or one
2133
+ * unwritable file never discards work that succeeded. A locale whose write lock stays contended is
2134
+ * one of these per-locale failures: it is recorded with code `LOCK_CONTENDED` on that locale's
2135
+ * summary rather than thrown. Callers should therefore inspect {@link RunSummary.failed} rather
2136
+ * than relying on a thrown error to detect trouble.
2137
+ *
2138
+ * A corrupt lock-file is the one exception. On a live run each locale reads the lock-file inside
2139
+ * its own write lock, so `LOCK_FILE_INVALID` escapes as a thrown error even after earlier locales
2140
+ * have already translated and written. No further locale is started, and the locales in flight
2141
+ * finish and release their write locks before the call rejects.
2142
+ *
2143
+ * Each locale takes its own write lock for the read-modify-write, so concurrent runs and
2144
+ * single-key edits cannot interleave on one file. Translations that fail the integrity gate are
2145
+ * refused rather than written, leaving the previous value intact.
2146
+ *
2147
+ * Set `dryRun` to compute the whole plan without writing or spending anything.
2148
+ *
2149
+ * @param input - The config and the per-run options.
2150
+ * @param deps - Optional adapter registry, provider factory, and file-system overrides.
2151
+ * @returns The per-locale account of the run, including usage and budget.
2152
+ *
1900
2153
  * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1901
- * @throws {@link SdkError} `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw (this wraps the
1902
- * provider's own error, including a missing `*_API_KEY` reported as `MISSING_API_KEY`); only on a
1903
- * non-dry-run, since dry-run never constructs the provider.
2154
+ * @throws {@link SdkError} `CONCURRENCY_INVALID`: `concurrency` is not an integer of at least 1.
2155
+ * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: a live run combined a `concurrency` above
2156
+ * 1 with a configured token budget. A dry run is exempt.
2157
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
2158
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
2159
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
1904
2160
  * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
1905
- * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be read or parsed (wraps the
1906
- * adapter read error).
1907
- * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is present but corrupt or oversized.
1908
- * @throws {@link SdkError} `CONCURRENCY_INVALID`: `concurrency` is defined but not an integer of at
1909
- * least 1; raised before any locale runs.
1910
- * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: a live run set `concurrency` greater than 1
1911
- * while the config configures a `maxTokens` budget; raised before any provider call (a dry run is
1912
- * exempt).
2161
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
2162
+ * @throws {@link SdkError} `PROVIDER_CONSTRUCTION_FAILED`: the provider could not be constructed,
2163
+ * most often because its API key environment variable is unset. Not thrown on a dry run, which
2164
+ * never constructs a provider.
2165
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
2166
+ * unsupported version. A dry run reads it once before any locale runs; a live run reads it per
2167
+ * locale, so this can abort the run after other locales have already been written.
2168
+ *
1913
2169
  * @example
1914
2170
  * ```ts
1915
2171
  * import { loadConfig, translate } from "@verbatra/sdk";
1916
2172
  *
1917
- * // The provider reads its API key from the environment (e.g. ANTHROPIC_API_KEY); no key is passed here.
1918
2173
  * const config = await loadConfig();
1919
- * const summary = await translate({ config });
2174
+ * const summary = await translate({
2175
+ * config,
2176
+ * onProgress: (event) => {
2177
+ * if (event.type === "locale-finished") {
2178
+ * console.log(`${event.locale}: ${event.translated} keys`);
2179
+ * }
2180
+ * },
2181
+ * });
1920
2182
  *
1921
- * for (const locale of summary.locales) {
1922
- * if (locale.status === "failed") {
1923
- * // Surfaced, not thrown: code is a preserved string (LOCALE_FAILED is only the fallback).
1924
- * console.error(`${locale.locale}: ${locale.error?.code} ${locale.error?.message}`);
1925
- * } else {
1926
- * console.log(`${locale.locale}: ${locale.translated.length} translated, ${locale.notices.length} notices`);
2183
+ * if (summary.failed.length > 0) {
2184
+ * for (const locale of summary.locales.filter((entry) => entry.status === "failed")) {
2185
+ * console.error(`${locale.locale}: ${locale.error?.message}`);
1927
2186
  * }
2187
+ * process.exitCode = 1;
1928
2188
  * }
1929
- *
1930
- * // Preview only: no provider call, no writes.
1931
- * const preview = await translate({ config, dryRun: true });
1932
2189
  * ```
1933
2190
  */
1934
2191
  declare function translate(input: TranslateInput, deps?: TranslateDeps): Promise<RunSummary>;
1935
2192
 
1936
2193
  /**
1937
2194
  * The two delimiter-separated interchange formats. They are one implementation with one parameter, the
1938
- * field delimiter: `tsv` is not a second format, it is `csv` with a tab.
2195
+ * field delimiter: `tsv` is not a second format, it is `csv` with a tab. The member is also the file
2196
+ * extension an export writes and an import reads back.
1939
2197
  */
1940
2198
  type DelimitedFormat = "csv" | "tsv";
1941
2199
 
1942
2200
  /**
1943
- * The interchange format a handoff moves in. `xlsx` is the default and the only shape that carries
1944
- * every target locale in one artifact; `csv` and `tsv` are plain text and move one file per target
1945
- * locale, so their path names a directory rather than a file.
2201
+ * The file shape of a translator handoff: a single styled `.xlsx` workbook with one sheet per
2202
+ * locale, or one plain `.csv` or `.tsv` file per locale.
2203
+ *
2204
+ * The workbook is the friendlier artifact to hand a human translator, while the delimited forms are
2205
+ * easier to diff, review, and feed to another tool. {@link exportWorkbook} and
2206
+ * {@link importWorkbook} both accept either.
1946
2207
  */
1947
2208
  type ExchangeFormat = "xlsx" | DelimitedFormat;
2209
+ /**
2210
+ * Every {@link ExchangeFormat} at runtime, for a tool that has to validate a `--format` argument or
2211
+ * offer the choices to a user. The order is the order to present them in: the workbook first, then
2212
+ * the delimited forms.
2213
+ *
2214
+ * It is derived from a record keyed by {@link ExchangeFormat} itself, so a format added to the type
2215
+ * without being added here fails to compile. A consumer that checks membership against this list can
2216
+ * therefore never silently reject a format the SDK accepts.
2217
+ */
2218
+ declare const EXCHANGE_FORMATS: readonly ExchangeFormat[];
2219
+ /** The {@link ExchangeFormat} {@link exportWorkbook} and {@link importWorkbook} use when the caller names none. */
2220
+ declare const DEFAULT_EXCHANGE_FORMAT: ExchangeFormat;
1948
2221
 
1949
- /** Default workbook output path, relative to the resolved working directory. */
2222
+ /** Default output path for an `.xlsx` handoff, used when {@link ExportWorkbookInput.out} is omitted. */
1950
2223
  declare const DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
1951
2224
  /**
1952
- * Default delimited output directory, relative to the resolved working directory. A delimited export
1953
- * writes one file per target locale, so its output path names a directory, not a file.
2225
+ * Default output directory for a delimited handoff. It carries no extension because it names a
2226
+ * directory, not a file: the export creates it and writes one `<locale>.<format>` file inside it
2227
+ * per exported locale, such as `de.csv`.
1954
2228
  */
1955
2229
  declare const DEFAULT_DELIMITED_PATH = "verbatra-translations";
1956
- /** Input for {@link exportWorkbook}: the validated config and where/how to run the export. */
2230
+ /** Input for {@link exportWorkbook}. */
1957
2231
  interface ExportWorkbookInput {
1958
- /** The validated configuration (typically from {@link loadConfig}). */
2232
+ /** The resolved project config, normally from {@link loadConfig}. */
1959
2233
  readonly config: VerbatraConfig;
1960
- /** Directory the file pattern, lock-file, and output path resolve against; defaults to cwd. */
2234
+ /** Directory the `files.pattern` and `out` are resolved against. Defaults to the process working directory. */
1961
2235
  readonly cwd?: string;
1962
2236
  /**
1963
- * Output path; defaults to {@link DEFAULT_WORKBOOK_PATH} for `xlsx` and to
1964
- * {@link DEFAULT_DELIMITED_PATH} for `csv` and `tsv`. It is the workbook file for `xlsx` and the
1965
- * directory the per-locale files are written into for `csv` and `tsv` (created if missing).
2237
+ * Where to write the handoff. Defaults to {@link DEFAULT_WORKBOOK_PATH} for `xlsx` and to
2238
+ * {@link DEFAULT_DELIMITED_PATH} for the delimited formats.
1966
2239
  */
1967
2240
  readonly out?: string;
1968
- /** Subset of target locales to export; defaults to all configured target locales. */
2241
+ /** Restrict the export to these target locales. Defaults to every configured target locale. */
1969
2242
  readonly locales?: readonly string[];
1970
- /** Include unchanged keys (off by default; export is missing-and-changed only). */
2243
+ /**
2244
+ * Include keys that are already up to date, not just the missing and stale ones. Useful when a
2245
+ * translator needs the surrounding context to translate consistently. Defaults to false.
2246
+ */
1971
2247
  readonly includeUnchanged?: boolean;
1972
- /** Interchange format to write; defaults to `xlsx`, so an existing caller is unaffected. */
2248
+ /** The handoff shape to write. Defaults to `xlsx`. */
1973
2249
  readonly format?: ExchangeFormat;
1974
2250
  }
1975
- /** Composition seam for {@link exportWorkbook}: inject a registry and a file system for tests. */
2251
+ /** Injectable dependencies for {@link exportWorkbook}. Every field has a working default. */
1976
2252
  interface ExportWorkbookDeps {
2253
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
1977
2254
  readonly adapterRegistry?: AdapterRegistry;
2255
+ /** File-system port. Defaults to the real file system. */
1978
2256
  readonly fs?: SdkFs;
1979
2257
  }
1980
- /** The outcome of an export: where it was written and how many rows per locale. */
2258
+ /** What {@link exportWorkbook} wrote. */
1981
2259
  interface ExportWorkbookResult {
1982
- /**
1983
- * The absolute path written to: the workbook file for `xlsx`, the directory holding one
1984
- * `<locale>.csv` or `<locale>.tsv` per exported locale for the delimited formats.
1985
- */
2260
+ /** The absolute path written, or the shared base path when one file per locale was written. */
1986
2261
  readonly path: string;
1987
- /** Per-locale row counts, in config order; the same set the workbook carries. */
2262
+ /** Row counts per exported locale. */
1988
2263
  readonly locales: readonly {
2264
+ /** The exported target locale. */
1989
2265
  readonly locale: string;
2266
+ /** How many translatable rows that locale contributed. */
1990
2267
  readonly rows: number;
1991
2268
  }[];
1992
2269
  }
1993
2270
  /**
1994
- * Export the strings needing human translation into a styled `.xlsx` workbook, or into one plain
1995
- * `.csv` or `.tsv` file per target locale. Each target locale is diffed against the source and lock
1996
- * baseline to pick the rows (missing and changed by default; add unchanged with `includeUnchanged`),
1997
- * and the result is written to `out`. No provider is called and no lock-file is written.
2271
+ * Writes the strings awaiting translation to a handoff a human translator can work in: a styled
2272
+ * `.xlsx` workbook with one sheet per locale, or one `.csv` or `.tsv` file per locale.
2273
+ *
2274
+ * By default only missing and stale keys are exported, which is what makes the handoff a work list
2275
+ * rather than a dump of the whole project. Each row carries the source text alongside any existing
2276
+ * translation and a review status, so the translator sees what changed and why a string was
2277
+ * flagged.
1998
2278
  *
1999
- * The delimited formats trade the workbook's protection for a diffable, git-friendly handoff: a
2000
- * workbook leaves only the Translation column editable and hides the source hash, while every field of
2001
- * a delimited file is editable and its source hash is visible. An edited source hash is never trusted
2002
- * on import; it is compared against the live source and the row is withheld as drift.
2279
+ * This is the outbound half of the exchange; {@link importWorkbook} reads the filled handoff back
2280
+ * through the same diff, lock, and integrity checks. It writes only the handoff file and never
2281
+ * touches the locale files or the lock-file.
2003
2282
  *
2004
- * A delimited export also writes a small hidden manifest naming the locales it wrote. It exists so a
2005
- * re-export with a narrower `locales` selection retires the locales it dropped: their files stay on
2006
- * disk, but the next import recognizes them as leftovers from the earlier run and refuses to apply
2007
- * them. The export deletes nothing, so an unrelated file in the output directory is never at risk.
2283
+ * Note that a malformed target locale file surfaces the adapter's own parse error rather than a
2284
+ * wrapped {@link SdkError}, because only source reads are wrapped. A caller that maps SDK codes
2285
+ * should be ready for an unrecognized error from a target file.
2008
2286
  *
2009
- * @param input - The validated config and export options.
2010
- * @param deps - Optional composition seams (registry, file system) for tests.
2011
- * @returns Where the export was written and the per-locale row counts.
2012
- * @throws {@link SdkError} `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`, `SOURCE_INVALID`, `LOCK_FILE_INVALID`
2013
- * with the same meanings as in `translate`, or `UNKNOWN_LOCALE` when a requested locale is not
2014
- * among the configured target locales.
2287
+ * @param input - The config, output path, locale filter, and handoff format.
2288
+ * @param deps - Optional adapter registry and file-system overrides.
2289
+ * @returns The path written and the per-locale row counts.
2290
+ *
2291
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
2292
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
2293
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
2294
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
2295
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist.
2296
+ * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be parsed.
2297
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
2298
+ * unsupported version.
2299
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: a requested locale is not a configured target locale.
2015
2300
  */
2016
2301
  declare function exportWorkbook(input: ExportWorkbookInput, deps?: ExportWorkbookDeps): Promise<ExportWorkbookResult>;
2017
2302
 
2018
- /** Input for {@link importWorkbook}: the validated config, the workbook path, and run options. */
2303
+ /** Input for {@link importWorkbook}. */
2019
2304
  interface ImportWorkbookInput {
2020
- /** The validated configuration (typically from {@link loadConfig}). */
2305
+ /** The resolved project config, normally from {@link loadConfig}. */
2021
2306
  readonly config: VerbatraConfig;
2022
2307
  /**
2023
- * Path to the filled handoff to import: the workbook file for `xlsx`, and for `csv` and `tsv`
2024
- * either one `<locale>.csv` / `<locale>.tsv` file or a directory holding one per target locale.
2308
+ * Path to the filled handoff. For a delimited import the path is tried as a single file first,
2309
+ * so one individual `<locale>.<format>` file can be imported on its own, with the locale taken
2310
+ * from its file name. If no file exists there, the path is treated as the directory the
2311
+ * per-locale files were written into and every configured target locale found inside is read.
2025
2312
  */
2026
2313
  readonly workbook: string;
2027
- /** Directory the file pattern, lock-file, and workbook path resolve against; defaults to cwd. */
2314
+ /** Directory the `files.pattern` and `workbook` are resolved against. Defaults to the process working directory. */
2028
2315
  readonly cwd?: string;
2029
- /** When true, validate and report only: write no locale file and update no lock-file. */
2316
+ /**
2317
+ * Read and validate the handoff but write nothing. The returned {@link RunSummary} reports what
2318
+ * would have been applied, which is the safe way to inspect a handoff before trusting it.
2319
+ * Defaults to false.
2320
+ */
2030
2321
  readonly dryRun?: boolean;
2031
- /** Interchange format to read; defaults to `xlsx`, so an existing caller is unaffected. */
2322
+ /** The handoff shape to read. Defaults to `xlsx`. */
2032
2323
  readonly format?: ExchangeFormat;
2033
2324
  }
2034
- /** Composition seam for {@link importWorkbook}: inject a registry and a file system for tests. */
2325
+ /** Injectable dependencies for {@link importWorkbook}. Every field has a working default. */
2035
2326
  interface ImportWorkbookDeps {
2327
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
2036
2328
  readonly adapterRegistry?: AdapterRegistry;
2329
+ /** File-system port. Defaults to the real file system. */
2037
2330
  readonly fs?: SdkFs;
2038
2331
  }
2039
2332
  /**
2040
- * Import a filled handoff back into the locale files. Each target-locale data sheet runs the same
2041
- * source-drift, placeholder, and ICU checks as the translate flow, the accepted values are written
2042
- * through the format adapter, and the lock is updated. Returns a {@link RunSummary} structurally
2043
- * identical to `translate`'s.
2333
+ * Reads a filled translator handoff back into the locale files. It is the inbound half of the
2334
+ * exchange that {@link exportWorkbook} starts, and it calls no provider: every value comes from the
2335
+ * handoff.
2336
+ *
2337
+ * Imported values are held to the same integrity gate as provider output, so a translator who drops
2338
+ * a placeholder or breaks ICU syntax has that row refused rather than written. Each locale takes
2339
+ * its write lock, and the lock-file and translation memory are updated exactly as in a
2340
+ * {@link translate} run, so an imported translation counts as up to date afterwards.
2044
2341
  *
2045
- * Whole-run failures (unknown format, unreadable/invalid/oversized workbook, corrupt lock) throw a
2046
- * structured {@link SdkError}. A per-sheet failure (a sheet named for a locale not in config, a
2047
- * broken-round-trip key, a write failure) is isolated as that locale's `status: "failed"`, not a
2048
- * throw; per-row rejections are withheld and reported on the locale. A configured target locale with
2049
- * no sheet at all (a deleted, renamed, or reordered tab) is reconciled after the sheet loop and
2050
- * reported as that locale's `status: "failed"` (`WORKBOOK_SHEET_MISSING`) rather than silently
2051
- * dropped. For a delimited directory, a locale file the most recent export into that directory did not
2052
- * write is a leftover from an earlier, wider selection: it is never applied and is reported as that
2053
- * locale's `status: "failed"` (`HANDOFF_FILE_STALE`). Dry-run validates and reports without writing any
2054
- * locale or lock file, and skips lock acquisition (there is nothing to protect).
2342
+ * Damage is contained rather than fatal: a blank row keeps the existing translation and its
2343
+ * baseline, an unreadable row is reported as a {@link MalformedRowReport}, and a repeated key is
2344
+ * reported as a {@link DuplicateKeyReport} with the first occurrence winning. All three surface on
2345
+ * the returned {@link RunSummary} rather than aborting the import. A sheet or file naming a locale
2346
+ * that is not configured is contained the same way: that locale fails with `CONFIG_INVALID` on its
2347
+ * own {@link LocaleSummary}, so nothing is written to an unmanaged path and the configured locales
2348
+ * still import. Once the handoff has been read, every per-locale failure is isolated this way, so
2349
+ * callers should inspect {@link RunSummary.failed} rather than relying on a thrown error.
2055
2350
  *
2056
- * The lock-file is read once, up front, for every sheet's diff baseline. On a non-dry-run, each
2057
- * sheet's write-and-lock-update step then holds that locale's `withLocaleWriteLock` for its whole
2058
- * critical section, so a concurrent writer touching the same locale can never interleave with it.
2351
+ * @param input - The config and the handoff path, format, and dry-run flag.
2352
+ * @param deps - Optional adapter registry and file-system overrides.
2353
+ * @returns The per-locale account of what was applied.
2059
2354
  *
2060
- * @param input - The validated config, the workbook path, and run options.
2061
- * @param deps - Optional composition seams (registry, file system) for tests.
2062
- * @returns A {@link RunSummary} with one locale per data sheet, in workbook order.
2063
- * @throws {@link SdkError} `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`, `SOURCE_INVALID`, `LOCK_FILE_INVALID`.
2355
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
2356
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the handoff file was not found, or the source
2357
+ * locale file does not exist.
2358
+ * @throws {@link SdkError} `SOURCE_INVALID`: the handoff is oversized or could not be parsed, or
2359
+ * the source locale file could not be parsed.
2360
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
2361
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
2362
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
2363
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
2364
+ * unsupported version. Read once before any locale is applied.
2064
2365
  */
2065
2366
  declare function importWorkbook(input: ImportWorkbookInput, deps?: ImportWorkbookDeps): Promise<RunSummary>;
2066
2367
 
2067
- /** The locale styles a files pattern's `{locale}` token can expand under. */
2368
+ /** The locale spellings {@link LocaleStyle} is drawn from, in declaration order. */
2068
2369
  declare const LOCALE_STYLES: readonly ["literal", "posix", "android"];
2069
2370
  /**
2070
- * How a configured locale is spelled inside a path:
2371
+ * How a locale is spelled inside a file path, set through the config's `files.localeStyle`.
2071
2372
  *
2072
- * - `literal` (the default) expands the token to the configured tag verbatim. This covers the
2073
- * i18next, vue-i18n, next-intl, ngx-translate, XLIFF, YAML, and ARB layouts, and Apple's
2074
- * `.lproj` naming, which is the BCP-47 tag as written.
2075
- * - `posix` replaces `-` with `_`, for gettext directories (`pt_BR/LC_MESSAGES/`) and the Java
2076
- * `messages_{locale}.properties` suffix layout.
2077
- * - `android` expands the token to a complete Android resource-directory segment, `values` prefix
2078
- * included, and is the only style that spells the source locale differently from a target.
2373
+ * - `literal`: the locale is written into the path exactly as configured, so `pt-BR` yields
2374
+ * `pt-BR`. This is the default and suits the JSON and YAML layouts most web projects use.
2375
+ * - `posix`: the POSIX spelling, so `pt-BR` yields `pt_BR`. Common for gettext-influenced and
2376
+ * Java-influenced layouts.
2377
+ * - `android`: the Android resource-qualifier spelling, so `pt-BR` yields `values-pt-rBR` and the
2378
+ * source locale yields the unqualified `values`. This style expands to a whole path segment, so
2379
+ * the `{locale}` token must stand alone between separators in the pattern.
2079
2380
  *
2080
- * A style may be relaxed to accept a locale it previously refused, but the spelling it already
2081
- * produces must never change: a re-spelling orphans every file written under the old name, leaving
2082
- * a stale directory the platform still prefers.
2381
+ * A locale that has no valid spelling under the declared style, or that would expand to something
2382
+ * other than a single path segment, is rejected with `LOCALE_LAYOUT_INVALID` before any file is
2383
+ * read.
2083
2384
  */
2084
2385
  type LocaleStyle = (typeof LOCALE_STYLES)[number];
2085
2386
 
2086
2387
  /**
2087
- * The part of the project config locale-to-path resolution depends on. Deliberately structural, so a
2088
- * caller holding a full `VerbatraConfig` passes it unchanged while a test can build one inline.
2388
+ * The slice of a {@link VerbatraConfig} that determines locale-to-path mapping. It is narrower than
2389
+ * the full config on purpose, so a caller holding only these fields can build a resolver.
2089
2390
  */
2090
2391
  interface LocalePathResolverConfig {
2392
+ /** The source locale, which some styles spell differently from the targets. */
2091
2393
  readonly sourceLocale: string;
2394
+ /** Every target locale. All of them are mapped up front so collisions are caught immediately. */
2092
2395
  readonly targetLocales: readonly string[];
2396
+ /** The file layout. */
2093
2397
  readonly files: {
2398
+ /** The path pattern, which must contain the `{locale}` token. */
2094
2399
  readonly pattern: string;
2400
+ /** How a locale is spelled inside the path. Defaults to `literal`. */
2095
2401
  readonly localeStyle?: LocaleStyle | undefined;
2096
2402
  };
2097
2403
  }
2098
- /**
2099
- * The project's locale-to-path mapping, resolved once from the config and threaded through every
2100
- * flow that needs a path. Holding one object rather than a `(pattern, style, sourceLocale)` triple
2101
- * is what keeps a source-aware style from producing a wrong path in a flow that only threaded part
2102
- * of it.
2103
- */
2404
+ /** The two-way mapping between locales and their absolute file paths. */
2104
2405
  interface LocalePathResolver {
2105
2406
  /**
2106
- * The absolute file path for one locale. Never throws for a configured locale: every one of them
2107
- * is spelled and checked when the resolver is created.
2108
- *
2109
- * @throws SdkError `LOCALE_LAYOUT_INVALID` when the style cannot spell an unconfigured locale
2110
- * passed in after construction.
2407
+ * The absolute path of a locale's file. Accepts any locale string, including ones outside the
2408
+ * configured set, so it can be used for exploratory reads.
2111
2409
  */
2112
2410
  pathFor(locale: string): string;
2113
2411
  /**
2114
- * Which configured locale a path belongs to, or `undefined` when it belongs to none of them. This
2115
- * is a lookup in the forward map, never a parse: the configured locale list is the only authority
2116
- * on which locales exist. A relative argument is resolved against the same `cwd` the resolver was
2117
- * created with; the match is exact, so a path differing only in case does not resolve even on a
2118
- * case-insensitive file system.
2119
- *
2120
- * `undefined` is not an error. It means the path is not one of this project's locale files, which
2121
- * is what a watcher needs to know in order to ignore it.
2412
+ * The configured locale owning an absolute path, or `undefined` when the path belongs to no
2413
+ * configured locale. The path is normalized before lookup, so a relative or non-canonical path
2414
+ * still resolves.
2122
2415
  */
2123
2416
  localeFor(absolutePath: string): string | undefined;
2124
2417
  }
2125
2418
  /**
2126
- * Creates the project's locale-to-path resolver. Every check runs here, before any file is read and
2127
- * before any provider call: the pattern must carry the `{locale}` token and must be combinable with
2128
- * the declared style, the style must have a valid spelling for the source locale and every target,
2129
- * and no two locales may resolve to the same path.
2419
+ * Builds the project's locale-to-path mapping from the configured pattern, locales, and
2420
+ * {@link LocaleStyle}, in both directions.
2421
+ *
2422
+ * Every SDK flow resolves paths through this, and a consumer that watches or reports on locale
2423
+ * files should use it too rather than re-deriving the mapping: a watcher that guesses at paths will
2424
+ * disagree with the SDK the moment a non-literal locale style is configured.
2130
2425
  *
2131
- * Under the default `literal` style the produced path is the pattern with `{locale}` replaced by the
2132
- * configured tag, resolved against `cwd`, which is what every project without a `files.localeStyle`
2133
- * gets and is unchanged from before styles existed.
2426
+ * The whole configured locale set is mapped eagerly during construction, so an unusable layout or a
2427
+ * path collision is reported here, before any file is read and before any provider is called,
2428
+ * rather than partway through a run.
2134
2429
  *
2135
- * @param cwd - Directory the files pattern resolves against.
2136
- * @param config - The project's locales, files pattern, and locale style.
2137
- * @throws SdkError `LOCALE_LAYOUT_INVALID` when the pattern and style cannot be combined, or the
2138
- * style cannot spell a configured locale.
2139
- * @throws SdkError `LOCALE_PATH_COLLISION` when two configured locales resolve to the same path.
2430
+ * @param cwd - Directory the pattern is resolved against.
2431
+ * @param config - The source locale, target locales, and file layout.
2432
+ * @returns A resolver mapping locales to paths and paths back to locales.
2433
+ *
2434
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the pattern lacks the `{locale}` token, a
2435
+ * segment style's token does not stand alone between separators, or a configured locale has no
2436
+ * valid single-segment spelling under the declared style.
2437
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same
2438
+ * absolute path.
2140
2439
  */
2141
2440
  declare function createLocalePathResolver(cwd: string, config: LocalePathResolverConfig): LocalePathResolver;
2142
2441
 
2143
2442
  /**
2144
- * The lock-file shape: for each target locale, a map of key to the source content hash from which that
2145
- * target key was last translated. The baseline core's diff consumes to detect changed source strings.
2443
+ * The contents of `verbatra.lock.json` (see {@link LOCK_FILE_NAME}), the baseline that makes
2444
+ * staleness detectable. For each locale it records the hash of the source text each key was
2445
+ * translated from, so a later run can tell a translation that is merely present from one that is
2446
+ * still current.
2447
+ *
2448
+ * Commit it: without it, every run has to treat an existing translation as up to date and a source
2449
+ * edit would silently never be re-translated.
2146
2450
  */
2147
2451
  interface LockFile {
2452
+ /** The lock-file schema version. An unrecognized version is rejected rather than guessed at. */
2148
2453
  readonly version: number;
2454
+ /** Per locale, the source-content hash recorded for each translated key. */
2149
2455
  readonly locales: Readonly<Record<string, Readonly<Record<string, string>>>>;
2150
2456
  }
2151
2457
 
2152
- /** Input for {@link loadLockFile}: the directory the lock-file resolves against. */
2458
+ /** Input for {@link loadLockFile}. */
2153
2459
  interface LoadLockFileInput {
2154
- /** Directory the lock-file resolves against; defaults to cwd. */
2460
+ /** Directory holding the lock-file. Defaults to the process working directory. */
2155
2461
  readonly cwd?: string;
2156
2462
  }
2157
- /** Composition seam for {@link loadLockFile}: inject a file system for tests. */
2463
+ /** Injectable dependencies for {@link loadLockFile}. Every field has a working default. */
2158
2464
  interface LoadLockFileDeps {
2465
+ /** File-system port. Defaults to the real file system. */
2159
2466
  readonly fs?: SdkFs;
2160
2467
  }
2161
2468
  /**
2162
- * Read the project's lock-file: a thin public wrapper around the internal lock-file reader,
2163
- * following the same input/deps convention as {@link check}, {@link diff}, and {@link lockState}.
2164
- * A missing lock-file degrades to an empty lock (first-run), the same behavior `translate` and
2165
- * `watch` rely on internally. Callers that need to distinguish "no lock-file yet" from "an empty
2166
- * but present lock-file" should use {@link lockState} instead, which probes the file's existence
2167
- * explicitly.
2469
+ * Reads the project's lock-file. Use it when you need the recorded baseline hashes themselves;
2470
+ * {@link lockState} is the better choice when you want the drift those hashes imply.
2168
2471
  *
2169
- * @param input - The directory the lock-file resolves against.
2170
- * @param deps - Optional composition seam (file system) for tests.
2171
- * @returns The parsed lock-file, or an empty lock when none exists yet.
2172
- * @throws {@link SdkError} `LOCK_FILE_INVALID` when the lock-file is present but corrupt, oversized,
2173
- * or at an unsupported version.
2472
+ * A project with no lock-file yet reads as an empty lock-file at the current version rather than an
2473
+ * error, so a first run and a lost file behave the same way. A lock-file that exists but cannot be
2474
+ * trusted is a different matter and throws, because silently treating a corrupt baseline as empty
2475
+ * would make every key look stale and trigger a full re-translation.
2476
+ *
2477
+ * @param input - The optional working directory.
2478
+ * @param deps - Optional file-system override.
2479
+ * @returns The lock-file's contents, or an empty lock-file when none exists.
2480
+ *
2481
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file exists but is oversized, not valid
2482
+ * JSON, structurally wrong, or at an unsupported version.
2174
2483
  */
2175
2484
  declare function loadLockFile(input?: LoadLockFileInput, deps?: LoadLockFileDeps): Promise<LockFile>;
2176
2485
 
2177
- /** The committed lock-file name, chosen to be obviously JSON and to not match a `*.lock` ignore rule. */
2486
+ /**
2487
+ * The file name of the project's lock-file, resolved against the run's working directory. Commit it
2488
+ * alongside the locale files: it is the baseline that lets verbatra tell a stale translation from a
2489
+ * current one. See {@link LockFile} for its contents.
2490
+ */
2178
2491
  declare const LOCK_FILE_NAME = "verbatra.lock.json";
2179
2492
 
2180
2493
  /**
2181
- * The subset of {@link ProviderId} that `init` can scaffold: every provider with a single required
2182
- * environment variable. "openai-compatible" is deliberately excluded: unlike every other provider it
2183
- * has no single required environment variable, only a three-tier
2184
- * apiKeyEnvVar/OPENAI_COMPATIBLE_API_KEY/placeholder fallback (see resolveOpenAiCompatibleKey in
2185
- * @verbatra/ai-providers), so it does not fit the `providerEnv` table below and `init` scaffolding does
2186
- * not offer it. This is the single source of truth for the exclusion; other packages import this type
2187
- * rather than re-deriving `Exclude<ProviderId, "openai-compatible">` themselves.
2494
+ * A provider that project scaffolding can offer out of the box. It excludes `openai-compatible`,
2495
+ * which needs a `baseUrl` and a model name that only the user can supply, so there is nothing
2496
+ * sensible to prefill.
2188
2497
  */
2189
2498
  type ScaffoldableProviderId = Exclude<ProviderId, "openai-compatible">;
2190
2499
  /**
2191
- * Read-only metadata the CLI `init` scaffold derives its tables from, so the CLI never restates the
2192
- * provider, env-var, model, or format truth owned by core and ai-providers.
2500
+ * The facts a project generator needs to write a first config: which environment variable each
2501
+ * provider reads its API key from, a sensible starting model per provider, and the formats the SDK
2502
+ * can handle.
2503
+ *
2504
+ * It is exported so that the CLI's `init` command and any third-party generator prompt with the
2505
+ * same values the SDK actually enforces, rather than keeping a copy that drifts. Note the key names
2506
+ * only: no key value is present or reachable here.
2193
2507
  */
2194
2508
  declare const scaffoldingMetadata: {
2195
- /** Provider id -> the environment variable its API key is read from. Owned by ai-providers. */
2509
+ /** The environment variable each scaffoldable provider reads its API key from. */
2196
2510
  readonly providerEnv: {
2197
2511
  readonly anthropic: "ANTHROPIC_API_KEY";
2198
2512
  readonly openai: "OPENAI_API_KEY";
2199
2513
  readonly gemini: "GEMINI_API_KEY";
2200
2514
  readonly deepl: "DEEPL_API_KEY";
2201
2515
  };
2202
- /** LLM provider id -> a cosmetic default scaffold model. Owned by ai-providers. DeepL has none. */
2516
+ /** A reasonable default model to prefill per language-model provider. DeepL has none, since it takes no model. */
2203
2517
  readonly scaffoldModels: {
2204
2518
  readonly anthropic: "claude-sonnet-4-6";
2205
2519
  readonly openai: "gpt-5.4-mini";
2206
2520
  readonly gemini: "gemini-2.5-flash";
2207
2521
  };
2208
- /** The closed set of source format ids. Owned by core. */
2522
+ /**
2523
+ * The option key each language-model provider takes its output token limit under, since they do
2524
+ * not agree: Anthropic calls it `maxTokens` and the others `maxOutputTokens`. A generator that
2525
+ * prefills a token limit must read the key from here rather than assume one, because
2526
+ * {@link verbatraConfigSchema} validates each provider's options strictly and rejects the wrong
2527
+ * one. DeepL has no entry, since it takes no token limit.
2528
+ */
2529
+ readonly providerTokenLimitKeys: {
2530
+ readonly anthropic: "maxTokens";
2531
+ readonly openai: "maxOutputTokens";
2532
+ readonly gemini: "maxOutputTokens";
2533
+ };
2534
+ /** Every i18n file format the SDK can read and write. */
2209
2535
  readonly supportedFormats: readonly ["i18next-json", "vue-i18n-json", "next-intl-json", "ngx-translate-json", "xliff", "yaml", "arb", "properties"];
2210
2536
  };
2211
2537
 
2212
- /** A minimal source-change event source. Production wraps chokidar; tests inject a stub. */
2538
+ /**
2539
+ * The minimal file-watching surface {@link watch} needs. Supplying your own through
2540
+ * `deps.createWatcher` lets an embedding application reuse a watcher it already runs, or drive the
2541
+ * flow deterministically in a test.
2542
+ */
2213
2543
  interface Watcher {
2214
- /** Register a listener invoked once per coalesced source-change event. */
2544
+ /** Registers a listener called on each change to the watched paths. */
2215
2545
  onChange(listener: () => void): void;
2216
- /** Stop watching and release the underlying resources. */
2546
+ /** Stops watching and releases the underlying resources. */
2217
2547
  close(): Promise<void>;
2218
2548
  }
2219
- /** Builds a {@link Watcher} for the given paths; the seam production fills with chokidar. */
2549
+ /** Builds a {@link Watcher} over the given absolute paths. Defaults to a chokidar-backed watcher. */
2220
2550
  type CreateWatcher = (paths: readonly string[]) => Watcher;
2221
- /** The run a watch trigger performs: the one-shot translate, unchanged. */
2551
+ /** Runs one translation pass. Defaults to {@link translate}, and is the seam a test substitutes. */
2222
2552
  type RunTranslate = (input: TranslateInput) => Promise<RunSummary>;
2223
- /** The outcome of one run, surfaced to the caller; never carries a secret. */
2553
+ /**
2554
+ * The outcome of a single watch-triggered run, handed to `onRun`. A failed run is reported here
2555
+ * rather than thrown, because a long-lived watcher must survive a bad run and keep watching.
2556
+ */
2224
2557
  type WatchRunResult = {
2558
+ /** The run completed. Inspect the summary for per-locale outcomes, which may still include failures. */
2225
2559
  readonly status: "succeeded";
2560
+ /** The run's per-locale account. */
2226
2561
  readonly summary: RunSummary;
2227
2562
  } | {
2563
+ /** The run threw before producing a summary, for instance because the source became unreadable. */
2228
2564
  readonly status: "failed";
2229
- /**
2230
- * A secret-free projection of the run's failure. `code` is a preserved string (the underlying
2231
- * error's `code`, or `"WATCH_RUN_FAILED"` as a fallback), not an {@link SdkErrorCode}.
2232
- */
2565
+ /** The failure's code and message. Never carries a secret. */
2233
2566
  readonly error: {
2567
+ /** The failure's own code where it had one, and `WATCH_RUN_FAILED` otherwise. */
2234
2568
  readonly code: string;
2569
+ /** A human-readable description of the failure. Never contains a secret. */
2235
2570
  readonly message: string;
2236
2571
  };
2237
2572
  };
2238
- /** Everything watch mode needs: the config, optional cwd/debounce, and the per-run output callback. */
2573
+ /** Input for {@link watch}. */
2239
2574
  interface WatchInput {
2240
- /** The validated configuration (typically from {@link loadConfig}). */
2575
+ /** The resolved project config, normally from {@link loadConfig}. */
2241
2576
  readonly config: VerbatraConfig;
2242
- /** Directory the file pattern and lock-file resolve against; defaults to the current working directory. */
2577
+ /** Directory the `files.pattern` is resolved against. Defaults to the process working directory. */
2243
2578
  readonly cwd?: string;
2244
- /** Quiet period after the last change before a run fires; defaults to 300ms. */
2579
+ /** How long to coalesce rapid source changes before running. Defaults to 300 milliseconds. */
2245
2580
  readonly debounceMs?: number;
2246
- /** Called once per run with its result. The SDK does no logging; this is the only output. */
2247
- readonly onRun: (result: WatchRunResult) => void;
2248
2581
  /**
2249
- * Passed through to every run's {@link TranslateInput.onLockWait}: called while a locale's write lock
2250
- * is blocked on another process, so a watch caller can surface the same "still waiting" progress a
2251
- * one-shot run does.
2582
+ * Called after every run with its outcome. This is the only way to observe a watch session, since
2583
+ * {@link watch} itself resolves as soon as watching starts.
2252
2584
  */
2585
+ readonly onRun: (result: WatchRunResult) => void;
2586
+ /** Called while waiting on another process's write lock. */
2253
2587
  readonly onLockWait?: LockWaitListener;
2254
- /**
2255
- * Passed through to every run's {@link TranslateInput.onProgress}: fires per locale and per provider
2256
- * sub-batch as each run advances, so a watch caller can surface the same progress a one-shot run does.
2257
- */
2588
+ /** Called as locales and sub-batches start and finish, for progress reporting. */
2258
2589
  readonly onProgress?: ProgressListener;
2259
- /** Passed through to every run's {@link TranslateInput.lockAcquireTimeoutMs}; the lock's 10-minute default when unset. */
2590
+ /** How long to wait for a locale's write lock before that run fails. */
2260
2591
  readonly lockAcquireTimeoutMs?: number;
2261
- /**
2262
- * Passed through to every run's {@link TranslateInput.concurrency}: how many locales each cycle may
2263
- * run at once. Defaults to 1 (strictly serial). Because the per-cycle budget tracker is fresh, a
2264
- * value greater than 1 cannot be honored alongside a `maxTokens` budget; {@link watch} refuses that
2265
- * combination at startup rather than letting every cycle fail on it.
2266
- */
2592
+ /** How many locales to run at once. Validated once when watching starts, and applied to every run. */
2267
2593
  readonly concurrency?: number;
2268
- /**
2269
- * Passed through to every run's {@link TranslateInput.cache}: when false, each run bypasses the
2270
- * translation-memory cache (both read and write). On by default.
2271
- */
2594
+ /** Consult and update the translation memory. Defaults to true. */
2272
2595
  readonly cache?: boolean;
2273
2596
  }
2274
- /** Composition seam: inject the watcher and the run for deterministic, offline tests. */
2597
+ /** Injectable dependencies for {@link watch}. Every field has a working default. */
2275
2598
  interface WatchDeps {
2276
- /** Adapter registry passed through to each run; defaults to the built-in registry. */
2599
+ /** Format-adapter registry to resolve the configured format. Defaults to the built-in registry. */
2277
2600
  readonly adapterRegistry?: AdapterRegistry;
2278
- /** Provider builder passed through to each run; defaults to constructing the configured provider. */
2601
+ /** Provider factory. Defaults to constructing the provider named in the config. */
2279
2602
  readonly createProvider?: CreateProvider;
2280
- /** File system passed through to each run; defaults to the real file system. */
2603
+ /** File-system port. Defaults to the real file system. */
2281
2604
  readonly fs?: SdkFs;
2282
- /** Source-change event source; defaults to the chokidar-backed watcher. */
2605
+ /** Watcher factory. Defaults to a chokidar-backed watcher over the source file. */
2283
2606
  readonly createWatcher?: CreateWatcher;
2284
- /** The run a trigger performs; defaults to the one-shot {@link translate}. */
2607
+ /** Translation runner. Defaults to {@link translate} wired with the other dependencies. */
2285
2608
  readonly runTranslate?: RunTranslate;
2286
2609
  }
2287
- /** Handle returned by {@link watch} to stop it. */
2610
+ /** The handle returned by {@link watch}, used to end the session. */
2288
2611
  interface WatchController {
2289
- /** Stop accepting triggers, close the watcher, and await the in-flight run to completion. */
2612
+ /** Stops watching and waits for any in-flight run to settle. Safe to call more than once. */
2290
2613
  stop(): Promise<void>;
2291
2614
  }
2292
2615
  /**
2293
- * Start watching the source file and re-run the one-shot {@link translate} on each debounced change.
2294
- * Runs are serialized: changes during a run collapse into a single follow-up, so two runs never
2295
- * overlap. A missing source at startup throws; every run outcome after start is surfaced through
2296
- * `onRun` as a {@link WatchRunResult} and watching continues. Returns a controller whose `stop()`
2297
- * closes the watcher and awaits the in-flight run.
2298
- *
2299
- * Both startup refusals are ordered deliberately: `concurrency` is resolved first because it is
2300
- * decidable from the arguments alone, while the missing-source check is an I/O probe of project
2301
- * state, so argument validation runs before any I/O and before the watcher exists. The concurrency
2302
- * resolve passes `dryRun: false` unconditionally, which is exact rather than a simplification: there
2303
- * is no dry-run watch, so the budget conflict always applies to a session.
2304
- *
2305
- * Resolving it here rather than leaving it to each cycle is the point, not an optimization: a watch
2306
- * session is long-lived, so a per-cycle refusal would fail the initial run and every run after it,
2307
- * indefinitely, while the session stayed alive and looked healthy.
2308
- *
2309
- * @param input - The config, optional cwd/debounce, and the `onRun` callback that receives each result.
2310
- * @param deps - Optional composition seams (watcher, run, registry, provider builder, file system) for tests.
2311
- * @returns A {@link WatchController}; call `stop()` to close the watcher and await the in-flight run.
2312
- * @throws {@link SdkError} `CONCURRENCY_INVALID`: at startup only, when `concurrency` is defined but
2313
- * not an integer of at least 1.
2314
- * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: at startup only, when `concurrency` is
2315
- * greater than 1 while the config sets a `maxTokens` budget.
2316
- * @throws {@link SdkError} `SOURCE_UNREADABLE`: at startup only, when the source locale file is absent.
2616
+ * Watches the source locale file and re-runs {@link translate} on each debounced change. It is the
2617
+ * development-loop counterpart to a one-shot run: edit a string, and the target locales catch up
2618
+ * without a manual command.
2619
+ *
2620
+ * One run starts immediately once watching is established, without waiting for a change, so the
2621
+ * target locales are brought up to date the moment the session opens. Its outcome arrives through
2622
+ * `onRun` like any other, and it may still be in flight when the returned promise resolves.
2623
+ *
2624
+ * The returned promise resolves as soon as watching is established, not when translation finishes,
2625
+ * so every run outcome arrives through `onRun`. A run that throws is delivered as a failed
2626
+ * {@link WatchRunResult} rather than escaping the session, because a watcher that died on the first
2627
+ * bad save would be useless. Only startup problems, validated before any watching begins, throw.
2628
+ *
2629
+ * Rapid saves are coalesced by `debounceMs`, and runs never overlap, so an editor writing a file
2630
+ * several times in a moment produces one run rather than a queue of them.
2631
+ *
2632
+ * Call {@link WatchController.stop} to end the session.
2633
+ *
2634
+ * @param input - The config, the debounce window, and the run callback.
2635
+ * @param deps - Optional adapter registry, provider factory, file-system, watcher, and runner overrides.
2636
+ * @returns A controller that stops the session.
2637
+ *
2638
+ * @throws {@link SdkError} `CONCURRENCY_INVALID`: `concurrency` is not an integer of at least 1.
2639
+ * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: `concurrency` above 1 was combined with a
2640
+ * configured token budget.
2641
+ * @throws {@link SdkError} `LOCALE_LAYOUT_INVALID`: the `files.pattern` and `files.localeStyle`
2642
+ * cannot be combined, or a configured locale has no valid path spelling under that style.
2643
+ * @throws {@link SdkError} `LOCALE_PATH_COLLISION`: two configured locales resolve to the same path.
2644
+ * @throws {@link SdkError} `SOURCE_UNREADABLE`: the source locale file does not exist when watching
2645
+ * starts.
2646
+ *
2317
2647
  * @example
2318
2648
  * ```ts
2319
2649
  * import { loadConfig, watch } from "@verbatra/sdk";
2320
2650
  *
2321
- * // The provider reads its API key from the environment (e.g. ANTHROPIC_API_KEY); no key is passed here.
2322
2651
  * const config = await loadConfig();
2323
2652
  * const controller = await watch({
2324
2653
  * config,
2325
2654
  * onRun: (result) => {
2326
- * if (result.status === "succeeded") {
2327
- * console.log(`ran: ${result.summary.succeeded.length} ok, ${result.summary.failed.length} failed`);
2328
- * } else {
2329
- * // Surfaced, not thrown: code is a preserved string (WATCH_RUN_FAILED is only the fallback).
2330
- * console.error(`run failed: ${result.error.code} ${result.error.message}`);
2655
+ * if (result.status === "failed") {
2656
+ * console.error(`run failed: ${result.error.message}`);
2657
+ * return;
2331
2658
  * }
2659
+ * console.log(`translated ${result.summary.succeeded.length} locales`);
2332
2660
  * },
2333
2661
  * });
2334
2662
  *
2335
- * // Stop cleanly on Ctrl-C: closes the watcher and awaits the in-flight run.
2336
- * process.on("SIGINT", () => {
2337
- * void controller.stop();
2338
- * });
2663
+ * process.on("SIGINT", () => void controller.stop());
2339
2664
  * ```
2340
2665
  */
2341
2666
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
2342
2667
 
2343
- export { type BudgetBehavior, CACHE_FILE_NAME, type CheckDeps, type CheckInput, type CheckSummary, type ConfigSource, type CreateProvider, type CreateWatcher, DEFAULT_DELIMITED_PATH, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type DuplicateKeyReport, type EditEntryDeps, type EditEntryInput, type EditEntryResult, type ExchangeFormat, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type GlossaryProvenance, type ImportWorkbookDeps, type ImportWorkbookInput, type IntegrityGateReason, type KeyIntegrityDeps, type KeyIntegrityEntry, type KeyIntegrityInput, type KeyValueDeps, type KeyValueInput, type KeyValueResult, LOCK_FILE_NAME, type LoadConfigOptions, type LoadLockFileDeps, type LoadLockFileInput, type LoadedConfig, type LocaleCheckSummary, type LocaleDiff, type LocaleFileSnapshot, type LocaleFinishedEvent, type LocaleKeyIntegrity, type LocaleNotice, type LocalePathResolver, type LocalePathResolverConfig, type LocaleSnapshotDelta, type LocaleStartedEvent, type LocaleStyle, type LocaleSummary, type LockFile, type LockHolder, type LockLocaleState, type LockStateDeps, type LockStateInput, type LockStateResult, type LockWaitEvent, type LockWaitListener, type MalformedRowReport, type NeedsReviewEntry, type ProgressEvent, type ProgressListener, type ProviderConfig, type ProviderId, type ReadLocaleFileSnapshotDeps, type ReadLocaleFileSnapshotInput, type RetranslateEntryDeps, type RetranslateEntryInput, type RetranslateEntryResult, type ReviewReasonCode, type RunBudget, type RunFinishedEvent, type RunStatusDeps, type RunStatusFile, type RunStatusInput, type RunStatusLocale, type RunStatusResult, type RunSummary, type RunTranslate, type ScaffoldableProviderId, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type SubBatchProgressEvent, type SupportedFormat$1 as SupportedFormat, type TranslateDeps, type TranslateInput, type TranslationMemory, type UsageSummary, type VerbatraConfig, type VerbatraConfigInput, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, createLocalePathResolver, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate, verbatraConfigSchema, watch };
2668
+ export { type BudgetBehavior, CACHE_FILE_NAME, type CheckDeps, type CheckInput, type CheckSummary, type ConfigSource, type CreateProvider, type CreateWatcher, DEFAULT_DELIMITED_PATH, DEFAULT_EXCHANGE_FORMAT, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type DuplicateKeyReport, EXCHANGE_FORMATS, type EditEntryDeps, type EditEntryInput, type EditEntryResult, type ExchangeFormat, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type GlossaryProvenance, type ImportWorkbookDeps, type ImportWorkbookInput, type IntegrityGateReason, type KeyIntegrityDeps, type KeyIntegrityEntry, type KeyIntegrityInput, type KeyValueDeps, type KeyValueInput, type KeyValueResult, LOCK_FILE_NAME, type LoadConfigOptions, type LoadLockFileDeps, type LoadLockFileInput, type LoadedConfig, type LocaleCheckSummary, type LocaleDiff, type LocaleFileSnapshot, type LocaleFinishedEvent, type LocaleKeyIntegrity, type LocaleNotice, type LocalePathResolver, type LocalePathResolverConfig, type LocaleSnapshotDelta, type LocaleStartedEvent, type LocaleStyle, type LocaleSummary, type LockFile, type LockHolder, type LockLocaleState, type LockStateDeps, type LockStateInput, type LockStateResult, type LockWaitEvent, type LockWaitListener, type MalformedRowReport, type NeedsReviewEntry, type ProgressEvent, type ProgressListener, type ProviderConfig, type ProviderId, type ReadLocaleFileSnapshotDeps, type ReadLocaleFileSnapshotInput, type RetranslateEntryDeps, type RetranslateEntryInput, type RetranslateEntryResult, type ReviewReasonCode, type RunBudget, type RunFinishedEvent, type RunStatusDeps, type RunStatusFile, type RunStatusInput, type RunStatusLocale, type RunStatusResult, type RunSummary, type RunTranslate, type ScaffoldableProviderId, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type SubBatchProgressEvent, type SupportedFormat$1 as SupportedFormat, type TranslateDeps, type TranslateInput, type TranslationMemory, type UsageSummary, type VerbatraConfig, type VerbatraConfigInput, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, createLocalePathResolver, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate, verbatraConfigSchema, watch };