@verbatra/sdk 0.6.0 → 0.6.2

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.ts CHANGED
@@ -231,10 +231,81 @@ declare const supportedFormatSchema$1: z.ZodEnum<{
231
231
  xliff: "xliff";
232
232
  yaml: "yaml";
233
233
  arb: "arb";
234
+ properties: "properties";
234
235
  }>;
235
236
  /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
236
237
  type SupportedFormat$1 = z.infer<typeof supportedFormatSchema$1>;
237
238
 
239
+ /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
240
+ type BoundedFileRead = {
241
+ readonly kind: "ok";
242
+ readonly content: string;
243
+ } | {
244
+ readonly kind: "missing";
245
+ } | {
246
+ readonly kind: "too-large";
247
+ };
248
+ /** Outcome of a bounded binary read: the bytes, or why they could not be read in bounds. */
249
+ type BoundedBytesRead = {
250
+ readonly kind: "ok";
251
+ readonly bytes: Uint8Array;
252
+ } | {
253
+ readonly kind: "missing";
254
+ } | {
255
+ readonly kind: "too-large";
256
+ };
257
+ /**
258
+ * The minimal file-system surface the SDK needs. Reads are bounded and writes are atomic. Injectable so
259
+ * tests stay deterministic; the format adapters do their own file IO and bypass this seam.
260
+ */
261
+ interface SdkFs {
262
+ /** Whether a readable file exists at the path. */
263
+ fileExists(path: string): Promise<boolean>;
264
+ /**
265
+ * Read a file as UTF-8 through a single handle, bounded to maxBytes. TOCTOU-safe: the handle is
266
+ * fstat'd and the read never advances past the sized length, so swapping in a larger file cannot
267
+ * bypass the cap. A missing or unreadable path is "missing"; a file over the cap is "too-large".
268
+ */
269
+ readFileBounded(path: string, maxBytes: number): Promise<BoundedFileRead>;
270
+ /** Read a file as raw bytes with the same TOCTOU-safe, bounded discipline as {@link readFileBounded}. */
271
+ readBytesBounded(path: string, maxBytes: number): Promise<BoundedBytesRead>;
272
+ /** Write atomically: a temp file in the same directory, then rename over the target. */
273
+ writeFile(path: string, data: string): Promise<void>;
274
+ /** Write raw bytes atomically (temp file, then rename over the target). Used for the workbook. */
275
+ writeBytes(path: string, data: Uint8Array): Promise<void>;
276
+ /**
277
+ * Atomically create a file only if it does not already exist (O_EXCL semantics), creating any
278
+ * missing parent directory first. Returns true if this call created the file, false if a file
279
+ * was already present (no write performed). The backbone of {@link withLocaleWriteLock}'s
280
+ * exclusive-create lock: the parent-directory creation lives here so that primitive never touches
281
+ * `node:fs` directly and a fake `SdkFs` in tests never needs a real directory to exist.
282
+ */
283
+ createExclusive(path: string, data: string): Promise<boolean>;
284
+ /** Delete a file if present; a no-op if it is already absent. */
285
+ deleteFile(path: string): Promise<void>;
286
+ }
287
+
288
+ /**
289
+ * The content-addressed translation-memory (TM) cache: a local, gitignored, regenerable sidecar to
290
+ * the lock file (`verbatra.cache.json`). It lets a translation whose source content is unchanged be
291
+ * reused for free, even under a renamed key or across two keys with byte-identical source text,
292
+ * instead of being re-sent to the provider.
293
+ *
294
+ * Nesting is `entries[fingerprint][targetLocale][contentHash] = value`. The fingerprint scopes every
295
+ * entry to one translation context (provider, model, tone, glossary), so a tone or glossary change
296
+ * never serves a stale value. Unlike the lock file, a corrupt or wrong-version cache degrades to an
297
+ * empty cache and never fails a run: it is a cost optimization, not a correctness record.
298
+ */
299
+ interface TranslationMemory {
300
+ /** The cache schema version; a value this build does not recognize degrades the whole file to empty. */
301
+ readonly version: number;
302
+ /** Cached values, nested fingerprint to target locale to source content hash to translated value. */
303
+ readonly entries: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, string>>>>>>;
304
+ }
305
+
306
+ /** The cache file's name, a sibling of the lock file and obviously JSON. */
307
+ declare const CACHE_FILE_NAME = "verbatra.cache.json";
308
+
238
309
  /**
239
310
  * The provider section of the config: a discriminated union over the provider id, reusing each
240
311
  * provider's own config schema. There is no key field anywhere in this union except
@@ -246,23 +317,27 @@ declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
246
317
  options: z.ZodObject<{
247
318
  model: z.ZodString;
248
319
  maxTokens: z.ZodNumber;
320
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
249
321
  }, z.core.$strict>;
250
322
  }, z.core.$strip>, z.ZodObject<{
251
323
  id: z.ZodLiteral<"openai">;
252
324
  options: z.ZodObject<{
253
325
  model: z.ZodString;
254
326
  maxOutputTokens: z.ZodNumber;
327
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
255
328
  }, z.core.$strict>;
256
329
  }, z.core.$strip>, z.ZodObject<{
257
330
  id: z.ZodLiteral<"gemini">;
258
331
  options: z.ZodObject<{
259
332
  model: z.ZodString;
260
333
  maxOutputTokens: z.ZodNumber;
334
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
261
335
  }, z.core.$strict>;
262
336
  }, z.core.$strip>, z.ZodObject<{
263
337
  id: z.ZodLiteral<"deepl">;
264
338
  options: z.ZodObject<{
265
339
  glossaryId: z.ZodOptional<z.ZodString>;
340
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
266
341
  }, z.core.$strict>;
267
342
  }, z.core.$strip>, z.ZodObject<{
268
343
  id: z.ZodLiteral<"openai-compatible">;
@@ -271,6 +346,7 @@ declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
271
346
  model: z.ZodString;
272
347
  maxOutputTokens: z.ZodNumber;
273
348
  apiKeyEnvVar: z.ZodOptional<z.ZodString>;
349
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
274
350
  }, z.core.$strict>;
275
351
  }, z.core.$strip>], "id">;
276
352
  /** One validated provider section: the provider id plus that provider's own options. */
@@ -294,6 +370,7 @@ declare const verbatraConfigSchema: z.ZodObject<{
294
370
  xliff: "xliff";
295
371
  yaml: "yaml";
296
372
  arb: "arb";
373
+ properties: "properties";
297
374
  }>;
298
375
  files: z.ZodObject<{
299
376
  pattern: z.ZodString;
@@ -303,23 +380,27 @@ declare const verbatraConfigSchema: z.ZodObject<{
303
380
  options: z.ZodObject<{
304
381
  model: z.ZodString;
305
382
  maxTokens: z.ZodNumber;
383
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
306
384
  }, z.core.$strict>;
307
385
  }, z.core.$strip>, z.ZodObject<{
308
386
  id: z.ZodLiteral<"openai">;
309
387
  options: z.ZodObject<{
310
388
  model: z.ZodString;
311
389
  maxOutputTokens: z.ZodNumber;
390
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
312
391
  }, z.core.$strict>;
313
392
  }, z.core.$strip>, z.ZodObject<{
314
393
  id: z.ZodLiteral<"gemini">;
315
394
  options: z.ZodObject<{
316
395
  model: z.ZodString;
317
396
  maxOutputTokens: z.ZodNumber;
397
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
318
398
  }, z.core.$strict>;
319
399
  }, z.core.$strip>, z.ZodObject<{
320
400
  id: z.ZodLiteral<"deepl">;
321
401
  options: z.ZodObject<{
322
402
  glossaryId: z.ZodOptional<z.ZodString>;
403
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
323
404
  }, z.core.$strict>;
324
405
  }, z.core.$strip>, z.ZodObject<{
325
406
  id: z.ZodLiteral<"openai-compatible">;
@@ -328,6 +409,7 @@ declare const verbatraConfigSchema: z.ZodObject<{
328
409
  model: z.ZodString;
329
410
  maxOutputTokens: z.ZodNumber;
330
411
  apiKeyEnvVar: z.ZodOptional<z.ZodString>;
412
+ requestTimeoutMs: z.ZodOptional<z.ZodNumber>;
331
413
  }, z.core.$strict>;
332
414
  }, z.core.$strip>], "id">;
333
415
  glossary: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodString>, z.ZodString]>>;
@@ -435,55 +517,6 @@ declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraCon
435
517
  declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfigInput;
436
518
  declare function defineConfig(config: AuthoringConfig): VerbatraConfigInput;
437
519
 
438
- /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
439
- type BoundedFileRead = {
440
- readonly kind: "ok";
441
- readonly content: string;
442
- } | {
443
- readonly kind: "missing";
444
- } | {
445
- readonly kind: "too-large";
446
- };
447
- /** Outcome of a bounded binary read: the bytes, or why they could not be read in bounds. */
448
- type BoundedBytesRead = {
449
- readonly kind: "ok";
450
- readonly bytes: Uint8Array;
451
- } | {
452
- readonly kind: "missing";
453
- } | {
454
- readonly kind: "too-large";
455
- };
456
- /**
457
- * The minimal file-system surface the SDK needs. Reads are bounded and writes are atomic. Injectable so
458
- * tests stay deterministic; the format adapters do their own file IO and bypass this seam.
459
- */
460
- interface SdkFs {
461
- /** Whether a readable file exists at the path. */
462
- fileExists(path: string): Promise<boolean>;
463
- /**
464
- * Read a file as UTF-8 through a single handle, bounded to maxBytes. TOCTOU-safe: the handle is
465
- * fstat'd and the read never advances past the sized length, so swapping in a larger file cannot
466
- * bypass the cap. A missing or unreadable path is "missing"; a file over the cap is "too-large".
467
- */
468
- readFileBounded(path: string, maxBytes: number): Promise<BoundedFileRead>;
469
- /** Read a file as raw bytes with the same TOCTOU-safe, bounded discipline as {@link readFileBounded}. */
470
- readBytesBounded(path: string, maxBytes: number): Promise<BoundedBytesRead>;
471
- /** Write atomically: a temp file in the same directory, then rename over the target. */
472
- writeFile(path: string, data: string): Promise<void>;
473
- /** Write raw bytes atomically (temp file, then rename over the target). Used for the workbook. */
474
- writeBytes(path: string, data: Uint8Array): Promise<void>;
475
- /**
476
- * Atomically create a file only if it does not already exist (O_EXCL semantics), creating any
477
- * missing parent directory first. Returns true if this call created the file, false if a file
478
- * was already present (no write performed). The backbone of {@link withLocaleWriteLock}'s
479
- * exclusive-create lock: the parent-directory creation lives here so that primitive never touches
480
- * `node:fs` directly and a fake `SdkFs` in tests never needs a real directory to exist.
481
- */
482
- createExclusive(path: string, data: string): Promise<boolean>;
483
- /** Delete a file if present; a no-op if it is already absent. */
484
- deleteFile(path: string): Promise<void>;
485
- }
486
-
487
520
  /** Where a config's resolved glossary came from: absent, inline, or a file at the recorded path. */
488
521
  type GlossaryProvenance = {
489
522
  readonly source: "none";
@@ -610,11 +643,18 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
610
643
  * its timeout elapsed, because another process is holding it (or an orphaned lock file was left
611
644
  * behind by a killed process); the message names the lock file's path (thrown by `translate`,
612
645
  * `importWorkbook`, `editEntry`, and `retranslateEntry`).
646
+ * - `CONCURRENCY_INVALID`: the `concurrency` input is not an integer of at least 1 (thrown by
647
+ * `translate` and, per run, by `watch`, before any locale runs).
648
+ * - `CONCURRENCY_BUDGET_CONFLICT`: a live run requested `concurrency` greater than 1 while a token
649
+ * budget (`maxTokens`) is configured; the two are mutually exclusive because concurrency makes the
650
+ * budget's stop guarantee nondeterministic (thrown by `translate` before any provider call; a dry
651
+ * run is exempt, since it never folds usage into or consults the budget tracker, so the conflict
652
+ * cannot arise).
613
653
  * - `LOCALE_FAILED` (NOT thrown): the fallback `code` recorded on a failed `LocaleSummary` when a
614
654
  * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction
615
655
  * on `translate`.
616
656
  */
617
- 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_FAILED";
657
+ 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" | "CONCURRENCY_INVALID" | "CONCURRENCY_BUDGET_CONFLICT" | "LOCALE_FAILED";
618
658
  /** The single structured error the SDK throws or records. Never carries a secret. */
619
659
  declare class SdkError extends Error {
620
660
  /** The stable {@link SdkErrorCode} for this failure; branch on this, not the message. */
@@ -635,6 +675,7 @@ declare const supportedFormatSchema: z.ZodEnum<{
635
675
  xliff: "xliff";
636
676
  yaml: "yaml";
637
677
  arb: "arb";
678
+ properties: "properties";
638
679
  }>;
639
680
  /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
640
681
  type SupportedFormat = z.infer<typeof supportedFormatSchema>;
@@ -930,6 +971,9 @@ interface DiffDeps {
930
971
  */
931
972
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
932
973
 
974
+ /** Why {@link gateCandidateValue} rejected a candidate value. */
975
+ type IntegrityGateReason = "placeholder" | "icu" | "degenerate";
976
+
933
977
  /** Input for {@link editEntry}: the validated config, exactly one target locale/key pair, and the human-typed replacement value. */
934
978
  interface EditEntryInput {
935
979
  /** The validated configuration (typically from {@link loadConfig}). */
@@ -959,7 +1003,7 @@ type EditEntryResult = {
959
1003
  readonly value: string;
960
1004
  } | {
961
1005
  readonly accepted: false;
962
- readonly reason: "placeholder" | "icu";
1006
+ readonly reason: IntegrityGateReason;
963
1007
  readonly value: string;
964
1008
  };
965
1009
  /**
@@ -968,7 +1012,10 @@ type EditEntryResult = {
968
1012
  * locale file (merging just this one key into its current entries, every other key untouched) and
969
1013
  * updates the lock entry for this key only (see {@link updateLockFileLocale}'s `"merge"` mode); on
970
1014
  * rejection, writes nothing. Never calls a provider: {@link EditEntryDeps} has no provider seam,
971
- * so there is no way to construct or call one.
1015
+ * so there is no way to construct or call one. On acceptance it also feeds the accepted value into
1016
+ * the local translation-memory cache (best-effort, keyed by the source content hash and the config
1017
+ * fingerprint), so a later run can reuse it for free; a cache failure is swallowed and never fails
1018
+ * the edit.
972
1019
  *
973
1020
  * `locale` and `key` are resolved fresh on every call, never from a cache: `locale` against
974
1021
  * `config.targetLocales`, `key` against the source resource read fresh from disk.
@@ -1248,7 +1295,7 @@ type RetranslateEntryResult = {
1248
1295
  readonly reviewReasons: readonly ReviewReasonCode[];
1249
1296
  } | {
1250
1297
  readonly accepted: false;
1251
- readonly reason: "placeholder" | "icu";
1298
+ readonly reason: IntegrityGateReason;
1252
1299
  readonly value: string;
1253
1300
  };
1254
1301
  /**
@@ -1259,6 +1306,9 @@ type RetranslateEntryResult = {
1259
1306
  * the lock entry for this key only (see {@link updateLockFileLocale}'s `"merge"` mode); on rejection,
1260
1307
  * writes nothing. Reuses exactly the building blocks `translate()` already uses
1261
1308
  * (`selectProvider`/`provider.translateBatch`) and nothing else: no bespoke per-provider plumbing.
1309
+ * Always calls the provider (it never consults the cache for a hit, since its whole purpose is a
1310
+ * fresh translation), then feeds the accepted value into the translation-memory cache best-effort so
1311
+ * a later run can reuse it; a cache failure is swallowed and never fails the retranslate.
1262
1312
  *
1263
1313
  * `locale` and `key` are resolved fresh on every call, never cached: `locale` against
1264
1314
  * `config.targetLocales` (`UNKNOWN_LOCALE`), `key` against the source resource's own keys, read
@@ -1344,12 +1394,47 @@ interface NeedsReviewEntry {
1344
1394
  /** Every {@link ReviewReasonCode} that applies to this key. */
1345
1395
  readonly reasons: readonly ReviewReasonCode[];
1346
1396
  }
1397
+ /**
1398
+ * One row a workbook import could not read: the 1-based worksheet row number and the header label of
1399
+ * the offending column. Carries no cell content, so untrusted workbook text never reaches the summary.
1400
+ */
1401
+ interface MalformedRowReport {
1402
+ /** The 1-based worksheet row number of the malformed row. */
1403
+ readonly row: number;
1404
+ /** The header label of the column the row was rejected on (for example "Status"). */
1405
+ readonly column: string;
1406
+ }
1407
+ /**
1408
+ * One duplicate-key conflict a workbook import found in a locale's sheet: the duplicated key and the
1409
+ * 1-based worksheet row number of the later (losing) occurrence. The first occurrence is the winner.
1410
+ */
1411
+ interface DuplicateKeyReport {
1412
+ /** The key that appeared more than once in the locale's sheet. */
1413
+ readonly key: string;
1414
+ /** The 1-based worksheet row number of the later occurrence that lost to the first. */
1415
+ readonly row: number;
1416
+ }
1347
1417
  /** Structured outcome for one target locale; surfaced as data on the run, never thrown. */
1348
1418
  interface LocaleSummary {
1349
1419
  /** The target locale this summary is for. */
1350
1420
  readonly locale: string;
1351
- /** Whether this locale's run succeeded or failed (a failure does not abort the run). */
1352
- readonly status: "succeeded" | "failed";
1421
+ /**
1422
+ * This locale's honest run status (a non-success does not abort the run). `"succeeded"` when
1423
+ * nothing was withheld, including a genuine no-op with no candidate keys at all. `"partial"` when
1424
+ * at least one key was accepted and written (a {@link LocaleSummary.translated} key or a
1425
+ * {@link LocaleSummary.generated} plural form) but at least one was withheld (any of
1426
+ * {@link LocaleSummary.integrityMismatches}, {@link LocaleSummary.providerFailures}, or
1427
+ * {@link LocaleSummary.budgetWithheld} non-empty). `"failed"` when the locale had candidate keys
1428
+ * but accepted nothing at all (neither translated nor generated) while withholding at least one.
1429
+ * Withheld keys keep their prior lock hash and retry next run.
1430
+ *
1431
+ * A workbook import's structural findings, {@link LocaleSummary.unfilled},
1432
+ * {@link LocaleSummary.malformedRows}, and {@link LocaleSummary.duplicateKeys}, deliberately do NOT
1433
+ * feed this status: they are surfaced in their own lists (and the CLI) but are not withholdings of
1434
+ * an attempted translation, so a locale that dropped a malformed row, collapsed a duplicate key, or
1435
+ * left a `changed` row unfilled while accepting its other rows can still be `"succeeded"`.
1436
+ */
1437
+ readonly status: "succeeded" | "partial" | "failed";
1353
1438
  /**
1354
1439
  * Keys translated and written this run. In dry-run, the keys that would be translated
1355
1440
  * (the provider is not called and nothing is written).
@@ -1367,6 +1452,15 @@ interface LocaleSummary {
1367
1452
  readonly pruned: readonly string[];
1368
1453
  /** Source keys flagged invalid-ICU that were skipped for translation this run. */
1369
1454
  readonly invalidIcuSource: readonly string[];
1455
+ /**
1456
+ * Keys served from the local translation-memory cache instead of the provider: their source content
1457
+ * was unchanged (possibly under a renamed key or shared with another key) and the cached value passed
1458
+ * the integrity gate, so no provider request was made for them. They are written and lock-advanced
1459
+ * exactly like a translated key, but kept distinct from {@link LocaleSummary.translated} because they
1460
+ * cost no provider usage. Always present, empty by default; empty in a dry-run and when the cache is
1461
+ * bypassed. Sorted by key.
1462
+ */
1463
+ readonly cacheHits: readonly string[];
1370
1464
  /**
1371
1465
  * Translated keys that failed the placeholder-integrity check and were withheld. Never includes a
1372
1466
  * key withheld because the provider call itself failed; see {@link LocaleSummary.providerFailures}
@@ -1419,9 +1513,34 @@ interface LocaleSummary {
1419
1513
  */
1420
1514
  readonly needsReview: readonly NeedsReviewEntry[];
1421
1515
  /**
1422
- * Present only when status is "failed": a structured, secret-free error. `code` is a preserved string
1423
- * (the underlying provider/adapter error's `code`, or `"LOCALE_FAILED"` as a fallback), intentionally
1424
- * wider than {@link SdkErrorCode}, so do not treat it as a closed set.
1516
+ * Keys the workbook exported as `changed` (needing an updated translation) that the translator left
1517
+ * blank, drifted or not, sorted by key. Pending work made visible: the row is skipped (nothing is
1518
+ * written and the prior lock baseline is kept), but the key is surfaced so an unfinished import is
1519
+ * never silently reported as done. Only a workbook import populates this; the provider path, which
1520
+ * never leaves a candidate key unfilled by a person, always leaves it empty.
1521
+ */
1522
+ readonly unfilled: readonly string[];
1523
+ /**
1524
+ * Rows a workbook import could not read for this locale, each by worksheet row number and offending
1525
+ * column, in row order. The sheet's other rows still import; a malformed row is reported, not
1526
+ * written. Only a workbook import populates this; the provider path always leaves it empty.
1527
+ */
1528
+ readonly malformedRows: readonly MalformedRowReport[];
1529
+ /**
1530
+ * Duplicate-key conflicts a workbook import found in this locale's sheet, in row order. The rule is
1531
+ * first occurrence wins: the first row for a key is judged and its later occurrences are reported
1532
+ * here and otherwise ignored. Only a workbook import populates this; the provider path always leaves
1533
+ * it empty.
1534
+ */
1535
+ readonly duplicateKeys: readonly DuplicateKeyReport[];
1536
+ /**
1537
+ * A structured, secret-free error for a locale that threw (an adapter/lock/provider-construction
1538
+ * failure isolated as data): present only on a "failed" locale, and only that throw path sets it.
1539
+ * A locale that is "failed" because every candidate key was withheld (all under `providerFailures`,
1540
+ * `integrityMismatches`, or `budgetWithheld`) carries those lists and any notices instead, and has
1541
+ * no `error`. `code` is a preserved string (the underlying provider/adapter error's `code`, or
1542
+ * `"LOCALE_FAILED"` as a fallback), intentionally wider than {@link SdkErrorCode}, so do not treat
1543
+ * it as a closed set.
1425
1544
  */
1426
1545
  readonly error?: {
1427
1546
  readonly code: string;
@@ -1434,9 +1553,15 @@ interface RunSummary {
1434
1553
  readonly dryRun: boolean;
1435
1554
  /** One {@link LocaleSummary} per target locale, in config order. */
1436
1555
  readonly locales: readonly LocaleSummary[];
1437
- /** Locales whose run succeeded. */
1556
+ /** Locales whose run succeeded with nothing withheld (status `"succeeded"`). */
1438
1557
  readonly succeeded: readonly string[];
1439
- /** Locales whose run failed (see each locale's `error`). */
1558
+ /**
1559
+ * Locales that wrote at least one translation but withheld at least one candidate key (status
1560
+ * `"partial"`). A partial locale still exits the CLI `0`: it made progress, and its withheld keys
1561
+ * retry next run. Never overlaps `succeeded` or `failed`.
1562
+ */
1563
+ readonly partial: readonly string[];
1564
+ /** Locales whose run failed, having accepted nothing (status `"failed"`; see each locale's `error`). */
1440
1565
  readonly failed: readonly string[];
1441
1566
  /**
1442
1567
  * Summed token usage across every locale's {@link LocaleSummary.usage}. Absent when no locale in the
@@ -1449,12 +1574,12 @@ interface RunSummary {
1449
1574
 
1450
1575
  /**
1451
1576
  * One target locale's snapshot inside the persisted run-status file. `status` is carried over from
1452
- * `LocaleSummary.status` unchanged, so an empty `needsReview` on a failed locale is never misread as
1453
- * "this locale ran clean with nothing to review".
1577
+ * `LocaleSummary.status` unchanged, so an empty `needsReview` on a failed or partial locale is never
1578
+ * misread as "this locale ran clean with nothing to review".
1454
1579
  */
1455
1580
  interface RunStatusLocale {
1456
1581
  readonly locale: string;
1457
- readonly status: "succeeded" | "failed";
1582
+ readonly status: "succeeded" | "partial" | "failed";
1458
1583
  /** The full, unmodified list from `LocaleSummary.needsReview`; already bounded and content-free. */
1459
1584
  readonly needsReview: readonly NeedsReviewEntry[];
1460
1585
  /** Absent exactly when the source `LocaleSummary.usage` was absent; never a fabricated zero. */
@@ -1510,6 +1635,85 @@ type RunStatusResult = {
1510
1635
  */
1511
1636
  declare function runStatus(input?: RunStatusInput, deps?: RunStatusDeps): Promise<RunStatusResult>;
1512
1637
 
1638
+ /** Diagnostic details, read leniently from a held lock file, about the process currently holding it. */
1639
+ interface LockHolder {
1640
+ /** The holder's process id, when the lock file could be read and carried a numeric `pid`. */
1641
+ readonly pid?: number;
1642
+ /** The holder's acquire time as an ISO-8601 string, when the lock file carried a string `acquiredAt`. */
1643
+ readonly acquiredAt?: string;
1644
+ }
1645
+ /** One wait-progress notification emitted while an acquire is blocked on a held lock. */
1646
+ interface LockWaitEvent {
1647
+ /** The on-disk path of the contended lock file, the exact path a person can delete if it is orphaned. */
1648
+ readonly lockPath: string;
1649
+ /** Milliseconds elapsed since this acquire started waiting, for a "still waiting" progress line. */
1650
+ readonly elapsedMs: number;
1651
+ /**
1652
+ * Diagnostics about the holding process, present only when the lock file could be read and parsed.
1653
+ * Purely for messaging; never consulted for any acquire decision.
1654
+ */
1655
+ readonly holder?: LockHolder;
1656
+ }
1657
+ /** Called while an acquire is blocked, to surface wait progress; the lock module itself writes no output. */
1658
+ type LockWaitListener = (event: LockWaitEvent) => void;
1659
+
1660
+ /**
1661
+ * One structured progress notification emitted while a run advances through its locales and their
1662
+ * provider sub-batches. Emitted to a caller-supplied {@link ProgressListener}; the SDK itself writes
1663
+ * nothing to any stream, so a caller (the CLI) is free to render these to stderr and keep stdout a
1664
+ * clean summary or NDJSON stream. Discriminated on `type`, mirroring the lock-wait event contract.
1665
+ */
1666
+ type ProgressEvent = LocaleStartedEvent | SubBatchProgressEvent | LocaleFinishedEvent | RunFinishedEvent;
1667
+ /** A locale is about to be processed: its position in the run's target-locale order. */
1668
+ interface LocaleStartedEvent {
1669
+ readonly type: "locale-started";
1670
+ /** The target locale about to run. */
1671
+ readonly locale: string;
1672
+ /** Zero-based index of this locale in the run's target-locale order. */
1673
+ readonly localeIndex: number;
1674
+ /** Total number of target locales in this run. */
1675
+ readonly totalLocales: number;
1676
+ }
1677
+ /**
1678
+ * One sub-batch of a locale's main translation loop has been reached. Emitted once per loop
1679
+ * iteration, in order, so a caller can render a "batch N of M" line. Fired at the top of the
1680
+ * iteration regardless of whether the batch then makes a provider call: a budget-withheld batch
1681
+ * (the run's token budget already tripped) still emits this event, which keeps `batchIndex` 1-based
1682
+ * and contiguous against the total. A dry-run never reaches this loop and so never emits it.
1683
+ */
1684
+ interface SubBatchProgressEvent {
1685
+ readonly type: "sub-batch";
1686
+ /** The locale whose entries this sub-batch carries. */
1687
+ readonly locale: string;
1688
+ /** One-based index of this sub-batch within the locale. */
1689
+ readonly batchIndex: number;
1690
+ /** Total number of sub-batches this locale's main translation loop runs. */
1691
+ readonly totalBatches: number;
1692
+ }
1693
+ /** A locale finished processing: how many keys it accepted (translated). */
1694
+ interface LocaleFinishedEvent {
1695
+ readonly type: "locale-finished";
1696
+ /** The target locale that finished. */
1697
+ readonly locale: string;
1698
+ /** Number of keys accepted (translated) for this locale. In a dry-run, the keys that would be translated. */
1699
+ readonly translated: number;
1700
+ }
1701
+ /** The run reached the end of its locale loop: how many locales it processed. */
1702
+ interface RunFinishedEvent {
1703
+ readonly type: "run-finished";
1704
+ /**
1705
+ * Number of locales the run processed (including failed locales: a per-locale failure is isolated
1706
+ * as a summary rather than thrown, so it is still counted here).
1707
+ */
1708
+ readonly localesCompleted: number;
1709
+ }
1710
+ /**
1711
+ * Called with each {@link ProgressEvent} as a run advances; the SDK emits no output of its own. A
1712
+ * caller (the CLI) uses this to render progress, always to stderr, so stdout stays byte-identical to
1713
+ * a run with no listener attached.
1714
+ */
1715
+ type ProgressListener = (event: ProgressEvent) => void;
1716
+
1513
1717
  /** Everything the one-shot run needs: the validated config and where/how to run it. */
1514
1718
  interface TranslateInput {
1515
1719
  /** The validated configuration (typically from {@link loadConfig}). */
@@ -1531,6 +1735,50 @@ interface TranslateInput {
1531
1735
  * unset, the config's `generatePlurals` applies.
1532
1736
  */
1533
1737
  readonly generatePlurals?: boolean;
1738
+ /**
1739
+ * Called while a locale's write lock is blocked on another process holding it: once after the first
1740
+ * failed acquire (with the holder's pid/acquiredAt when the lock file is readable), then periodically
1741
+ * with the growing elapsed time. Never called for an uncontended run. The SDK writes no output; this
1742
+ * is the only wait-progress signal (the CLI renders its "still waiting" line from it).
1743
+ */
1744
+ readonly onLockWait?: LockWaitListener;
1745
+ /**
1746
+ * Called as the run advances: once per locale before it starts and once after it finishes, once
1747
+ * per provider sub-batch within a locale, and once when the whole locale loop ends. Never fires a
1748
+ * sub-batch event on a dry-run (no provider call is made). The SDK writes no output; this is the
1749
+ * only progress signal (the CLI renders it to stderr, keeping stdout byte-identical).
1750
+ *
1751
+ * Pairing is not guaranteed when the run throws: a whole-run failure (for example a corrupt
1752
+ * lock-file surfacing as `LOCK_FILE_INVALID`, which re-throws instead of being isolated per locale)
1753
+ * can emit a `locale-started` event with no matching `locale-finished`, and no `run-finished` at
1754
+ * all. A per-locale failure that is isolated (not re-thrown) still emits both, and is counted in
1755
+ * `run-finished`.
1756
+ */
1757
+ readonly onProgress?: ProgressListener;
1758
+ /**
1759
+ * Override how long a locale's write lock keeps retrying before failing with `LOCK_CONTENDED`, in
1760
+ * milliseconds. Defaults to the lock's own 10-minute default when unset (surfaced to the CLI as
1761
+ * `--lock-timeout`).
1762
+ */
1763
+ readonly lockAcquireTimeoutMs?: number;
1764
+ /**
1765
+ * How many target locales may run at once, a positive integer (surfaced to the CLI as
1766
+ * `--concurrency`). Defaults to 1, which runs locales strictly in sequence and is byte-identical
1767
+ * to a run with this option unset. A value below 1 or a non-integer is rejected with a whole-run
1768
+ * `CONCURRENCY_INVALID` error. On a live run, a value greater than 1 is rejected with a whole-run
1769
+ * `CONCURRENCY_BUDGET_CONFLICT` error when the config sets a `maxTokens` budget, because
1770
+ * concurrency would make the budget's stop guarantee nondeterministic; a dry run is exempt. The
1771
+ * per-locale write locks already isolate concurrent locales on disk, so no extra locking is added.
1772
+ */
1773
+ readonly concurrency?: number;
1774
+ /**
1775
+ * Whether to use the local content-addressed translation-memory cache (`verbatra.cache.json`). On by
1776
+ * default; the CLI exposes it as `--no-cache`. When false, both the cache read and the end-of-run
1777
+ * cache write are skipped: the run makes exactly the provider calls it would with no cache present,
1778
+ * and leaves any existing cache file untouched. Ignored on a dry-run, which never reads or writes the
1779
+ * cache regardless. See the cache module for the fingerprint and degrade-to-empty contract.
1780
+ */
1781
+ readonly cache?: boolean;
1534
1782
  }
1535
1783
  /** Composition seam: inject a registry, a provider builder, and a file system for tests. */
1536
1784
  interface TranslateDeps {
@@ -1568,7 +1816,7 @@ interface TranslateDeps {
1568
1816
  *
1569
1817
  * @param input - The validated config and run options (cwd, dryRun, prune, generatePlurals).
1570
1818
  * @param deps - Optional composition seams (registry, provider builder, file system) for tests.
1571
- * @returns A {@link RunSummary}: the per-locale {@link LocaleSummary}s and the succeeded/failed locale lists.
1819
+ * @returns A {@link RunSummary}: the per-locale {@link LocaleSummary}s and the succeeded/partial/failed locale lists.
1572
1820
  * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1573
1821
  * @throws {@link SdkError} `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw (this wraps the
1574
1822
  * provider's own error, including a missing `*_API_KEY` reported as `MISSING_API_KEY`); only on a
@@ -1577,6 +1825,11 @@ interface TranslateDeps {
1577
1825
  * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be read or parsed (wraps the
1578
1826
  * adapter read error).
1579
1827
  * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is present but corrupt or oversized.
1828
+ * @throws {@link SdkError} `CONCURRENCY_INVALID`: `concurrency` is defined but not an integer of at
1829
+ * least 1; raised before any locale runs.
1830
+ * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: a live run set `concurrency` greater than 1
1831
+ * while the config configures a `maxTokens` budget; raised before any provider call (a dry run is
1832
+ * exempt).
1580
1833
  * @example
1581
1834
  * ```ts
1582
1835
  * import { loadConfig, translate } from "@verbatra/sdk";
@@ -1668,10 +1921,13 @@ interface ImportWorkbookDeps {
1668
1921
  * identical to `translate`'s.
1669
1922
  *
1670
1923
  * Whole-run failures (unknown format, unreadable/invalid/oversized workbook, corrupt lock) throw a
1671
- * structured {@link SdkError}. A per-sheet failure (a locale not in config, a broken-round-trip key,
1672
- * a write failure) is isolated as that locale's `status: "failed"`, not a throw; per-row rejections
1673
- * are withheld and reported on the locale. Dry-run validates and reports without writing any locale or
1674
- * lock file, and skips lock acquisition (there is nothing to protect).
1924
+ * structured {@link SdkError}. A per-sheet failure (a sheet named for a locale not in config, a
1925
+ * broken-round-trip key, a write failure) is isolated as that locale's `status: "failed"`, not a
1926
+ * throw; per-row rejections are withheld and reported on the locale. A configured target locale with
1927
+ * no sheet at all (a deleted, renamed, or reordered tab) is reconciled after the sheet loop and
1928
+ * reported as that locale's `status: "failed"` (`WORKBOOK_SHEET_MISSING`) rather than silently
1929
+ * dropped. Dry-run validates and reports without writing any locale or lock file, and skips lock
1930
+ * acquisition (there is nothing to protect).
1675
1931
  *
1676
1932
  * The lock-file is read once, up front, for every sheet's diff baseline. On a non-dry-run, each
1677
1933
  * sheet's write-and-lock-update step then holds that locale's `withLocaleWriteLock` for its whole
@@ -1750,7 +2006,7 @@ declare const scaffoldingMetadata: {
1750
2006
  readonly gemini: "gemini-2.5-flash";
1751
2007
  };
1752
2008
  /** The closed set of source format ids. Owned by core. */
1753
- readonly supportedFormats: readonly ["i18next-json", "vue-i18n-json", "next-intl-json", "ngx-translate-json", "xliff", "yaml", "arb"];
2009
+ readonly supportedFormats: readonly ["i18next-json", "vue-i18n-json", "next-intl-json", "ngx-translate-json", "xliff", "yaml", "arb", "properties"];
1754
2010
  };
1755
2011
 
1756
2012
  /** A minimal source-change event source. Production wraps chokidar; tests inject a stub. */
@@ -1789,6 +2045,30 @@ interface WatchInput {
1789
2045
  readonly debounceMs?: number;
1790
2046
  /** Called once per run with its result. The SDK does no logging; this is the only output. */
1791
2047
  readonly onRun: (result: WatchRunResult) => void;
2048
+ /**
2049
+ * Passed through to every run's {@link TranslateInput.onLockWait}: called while a locale's write lock
2050
+ * is blocked on another process, so a watch caller can surface the same "still waiting" progress a
2051
+ * one-shot run does.
2052
+ */
2053
+ readonly onLockWait?: LockWaitListener;
2054
+ /**
2055
+ * Passed through to every run's {@link TranslateInput.onProgress}: fires per locale and per provider
2056
+ * sub-batch as each run advances, so a watch caller can surface the same progress a one-shot run does.
2057
+ */
2058
+ readonly onProgress?: ProgressListener;
2059
+ /** Passed through to every run's {@link TranslateInput.lockAcquireTimeoutMs}; the lock's 10-minute default when unset. */
2060
+ readonly lockAcquireTimeoutMs?: number;
2061
+ /**
2062
+ * Passed through to every run's {@link TranslateInput.concurrency}: how many locales each cycle may
2063
+ * run at once. Defaults to 1 (strictly serial). The per-cycle tracker is fresh, so a budgeted watch
2064
+ * with concurrency greater than 1 still fails each run with `CONCURRENCY_BUDGET_CONFLICT`.
2065
+ */
2066
+ readonly concurrency?: number;
2067
+ /**
2068
+ * Passed through to every run's {@link TranslateInput.cache}: when false, each run bypasses the
2069
+ * translation-memory cache (both read and write). On by default.
2070
+ */
2071
+ readonly cache?: boolean;
1792
2072
  }
1793
2073
  /** Composition seam: inject the watcher and the run for deterministic, offline tests. */
1794
2074
  interface WatchDeps {
@@ -1845,4 +2125,4 @@ interface WatchController {
1845
2125
  */
1846
2126
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
1847
2127
 
1848
- export { type BudgetBehavior, type CheckDeps, type CheckInput, type CheckSummary, type ConfigSource, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type EditEntryDeps, type EditEntryInput, type EditEntryResult, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type GlossaryProvenance, type ImportWorkbookDeps, type ImportWorkbookInput, 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 LocaleKeyIntegrity, type LocaleNotice, type LocaleSnapshotDelta, type LocaleSummary, type LockFile, type LockLocaleState, type LockStateDeps, type LockStateInput, type LockStateResult, type NeedsReviewEntry, type ProviderConfig, type ProviderId, type ReadLocaleFileSnapshotDeps, type ReadLocaleFileSnapshotInput, type RetranslateEntryDeps, type RetranslateEntryInput, type RetranslateEntryResult, type ReviewReasonCode, type RunBudget, 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 SupportedFormat$1 as SupportedFormat, type TranslateDeps, type TranslateInput, type UsageSummary, type VerbatraConfig, type VerbatraConfigInput, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate, verbatraConfigSchema, watch };
2128
+ export { type BudgetBehavior, CACHE_FILE_NAME, type CheckDeps, type CheckInput, type CheckSummary, type ConfigSource, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type DuplicateKeyReport, type EditEntryDeps, type EditEntryInput, type EditEntryResult, 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 LocaleSnapshotDelta, type LocaleStartedEvent, 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, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate, verbatraConfigSchema, watch };