@verbatra/sdk 0.6.1 → 0.6.3

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
@@ -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
@@ -299,6 +370,7 @@ declare const verbatraConfigSchema: z.ZodObject<{
299
370
  xliff: "xliff";
300
371
  yaml: "yaml";
301
372
  arb: "arb";
373
+ properties: "properties";
302
374
  }>;
303
375
  files: z.ZodObject<{
304
376
  pattern: z.ZodString;
@@ -445,55 +517,6 @@ declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraCon
445
517
  declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfigInput;
446
518
  declare function defineConfig(config: AuthoringConfig): VerbatraConfigInput;
447
519
 
448
- /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
449
- type BoundedFileRead = {
450
- readonly kind: "ok";
451
- readonly content: string;
452
- } | {
453
- readonly kind: "missing";
454
- } | {
455
- readonly kind: "too-large";
456
- };
457
- /** Outcome of a bounded binary read: the bytes, or why they could not be read in bounds. */
458
- type BoundedBytesRead = {
459
- readonly kind: "ok";
460
- readonly bytes: Uint8Array;
461
- } | {
462
- readonly kind: "missing";
463
- } | {
464
- readonly kind: "too-large";
465
- };
466
- /**
467
- * The minimal file-system surface the SDK needs. Reads are bounded and writes are atomic. Injectable so
468
- * tests stay deterministic; the format adapters do their own file IO and bypass this seam.
469
- */
470
- interface SdkFs {
471
- /** Whether a readable file exists at the path. */
472
- fileExists(path: string): Promise<boolean>;
473
- /**
474
- * Read a file as UTF-8 through a single handle, bounded to maxBytes. TOCTOU-safe: the handle is
475
- * fstat'd and the read never advances past the sized length, so swapping in a larger file cannot
476
- * bypass the cap. A missing or unreadable path is "missing"; a file over the cap is "too-large".
477
- */
478
- readFileBounded(path: string, maxBytes: number): Promise<BoundedFileRead>;
479
- /** Read a file as raw bytes with the same TOCTOU-safe, bounded discipline as {@link readFileBounded}. */
480
- readBytesBounded(path: string, maxBytes: number): Promise<BoundedBytesRead>;
481
- /** Write atomically: a temp file in the same directory, then rename over the target. */
482
- writeFile(path: string, data: string): Promise<void>;
483
- /** Write raw bytes atomically (temp file, then rename over the target). Used for the workbook. */
484
- writeBytes(path: string, data: Uint8Array): Promise<void>;
485
- /**
486
- * Atomically create a file only if it does not already exist (O_EXCL semantics), creating any
487
- * missing parent directory first. Returns true if this call created the file, false if a file
488
- * was already present (no write performed). The backbone of {@link withLocaleWriteLock}'s
489
- * exclusive-create lock: the parent-directory creation lives here so that primitive never touches
490
- * `node:fs` directly and a fake `SdkFs` in tests never needs a real directory to exist.
491
- */
492
- createExclusive(path: string, data: string): Promise<boolean>;
493
- /** Delete a file if present; a no-op if it is already absent. */
494
- deleteFile(path: string): Promise<void>;
495
- }
496
-
497
520
  /** Where a config's resolved glossary came from: absent, inline, or a file at the recorded path. */
498
521
  type GlossaryProvenance = {
499
522
  readonly source: "none";
@@ -620,11 +643,18 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
620
643
  * its timeout elapsed, because another process is holding it (or an orphaned lock file was left
621
644
  * behind by a killed process); the message names the lock file's path (thrown by `translate`,
622
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).
623
653
  * - `LOCALE_FAILED` (NOT thrown): the fallback `code` recorded on a failed `LocaleSummary` when a
624
654
  * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction
625
655
  * on `translate`.
626
656
  */
627
- 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";
628
658
  /** The single structured error the SDK throws or records. Never carries a secret. */
629
659
  declare class SdkError extends Error {
630
660
  /** The stable {@link SdkErrorCode} for this failure; branch on this, not the message. */
@@ -645,6 +675,7 @@ declare const supportedFormatSchema: z.ZodEnum<{
645
675
  xliff: "xliff";
646
676
  yaml: "yaml";
647
677
  arb: "arb";
678
+ properties: "properties";
648
679
  }>;
649
680
  /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
650
681
  type SupportedFormat = z.infer<typeof supportedFormatSchema>;
@@ -941,7 +972,7 @@ interface DiffDeps {
941
972
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
942
973
 
943
974
  /** Why {@link gateCandidateValue} rejected a candidate value. */
944
- type IntegrityGateReason = "placeholder" | "icu" | "degenerate";
975
+ type IntegrityGateReason = "placeholder" | "icu" | "degenerate" | "empty";
945
976
 
946
977
  /** Input for {@link editEntry}: the validated config, exactly one target locale/key pair, and the human-typed replacement value. */
947
978
  interface EditEntryInput {
@@ -981,7 +1012,10 @@ type EditEntryResult = {
981
1012
  * locale file (merging just this one key into its current entries, every other key untouched) and
982
1013
  * updates the lock entry for this key only (see {@link updateLockFileLocale}'s `"merge"` mode); on
983
1014
  * rejection, writes nothing. Never calls a provider: {@link EditEntryDeps} has no provider seam,
984
- * 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.
985
1019
  *
986
1020
  * `locale` and `key` are resolved fresh on every call, never from a cache: `locale` against
987
1021
  * `config.targetLocales`, `key` against the source resource read fresh from disk.
@@ -1272,6 +1306,9 @@ type RetranslateEntryResult = {
1272
1306
  * the lock entry for this key only (see {@link updateLockFileLocale}'s `"merge"` mode); on rejection,
1273
1307
  * writes nothing. Reuses exactly the building blocks `translate()` already uses
1274
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.
1275
1312
  *
1276
1313
  * `locale` and `key` are resolved fresh on every call, never cached: `locale` against
1277
1314
  * `config.targetLocales` (`UNKNOWN_LOCALE`), `key` against the source resource's own keys, read
@@ -1311,7 +1348,7 @@ type RetranslateEntryResult = {
1311
1348
  declare function retranslateEntry(input: RetranslateEntryInput, deps?: RetranslateEntryDeps): Promise<RetranslateEntryResult>;
1312
1349
 
1313
1350
  /** A stable code for an SDK-originated notice (not a provider notice). */
1314
- type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED";
1351
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED" | "CACHE_VERSION_UNRECOGNIZED";
1315
1352
  /** Summed token usage across every provider call in a scope (one locale, or the whole run). */
1316
1353
  interface UsageSummary {
1317
1354
  readonly inputTokens: number;
@@ -1357,6 +1394,26 @@ interface NeedsReviewEntry {
1357
1394
  /** Every {@link ReviewReasonCode} that applies to this key. */
1358
1395
  readonly reasons: readonly ReviewReasonCode[];
1359
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
+ }
1360
1417
  /** Structured outcome for one target locale; surfaced as data on the run, never thrown. */
1361
1418
  interface LocaleSummary {
1362
1419
  /** The target locale this summary is for. */
@@ -1370,6 +1427,32 @@ interface LocaleSummary {
1370
1427
  * {@link LocaleSummary.budgetWithheld} non-empty). `"failed"` when the locale had candidate keys
1371
1428
  * but accepted nothing at all (neither translated nor generated) while withholding at least one.
1372
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 row unfilled while accepting its other rows can still be `"succeeded"`.
1436
+ *
1437
+ * This is a settled decision, not an omission. Recording the reasoning here so the question stops
1438
+ * recurring:
1439
+ *
1440
+ * 1. Two questions, two commands. `import`'s exit code answers "did the import apply what the
1441
+ * translator gave me". `check` and `diff` answer "is this project fully translated", and both
1442
+ * already exit non-zero or report pending in every one of these cases. Folding pending human
1443
+ * work into the import exit code duplicates an existing gate rather than adding one.
1444
+ * 2. It would break the staged handoff. With `de` and `fr` configured and only `de` filled in,
1445
+ * failing on unfilled work makes `fr` fail and takes the whole import with it, even though `de`
1446
+ * imported perfectly. Locale-at-a-time handoff is this feature's most common workflow.
1447
+ * 3. {@link LocaleSummary.malformedRows} structurally cannot signal lost work. The Status column is
1448
+ * the only reachable rejection in the workbook reader; every other field is an unconstrained
1449
+ * string or a tolerant parse. So a row is malformed on its Status cell alone, entirely
1450
+ * independently of whether its Translation cell was filled. An untouched sheet with a mangled
1451
+ * Status column (spreadsheet autocorrect, a CAT-tool round trip) is all-malformed with zero lost
1452
+ * work. A bucket that cannot distinguish dropped work from absent work cannot honestly drive a
1453
+ * failure status.
1454
+ * 4. {@link LocaleSummary.duplicateKeys} never could: the first occurrence wins and the intent is
1455
+ * honoured.
1373
1456
  */
1374
1457
  readonly status: "succeeded" | "partial" | "failed";
1375
1458
  /**
@@ -1389,6 +1472,15 @@ interface LocaleSummary {
1389
1472
  readonly pruned: readonly string[];
1390
1473
  /** Source keys flagged invalid-ICU that were skipped for translation this run. */
1391
1474
  readonly invalidIcuSource: readonly string[];
1475
+ /**
1476
+ * Keys served from the local translation-memory cache instead of the provider: their source content
1477
+ * was unchanged (possibly under a renamed key or shared with another key) and the cached value passed
1478
+ * the integrity gate, so no provider request was made for them. They are written and lock-advanced
1479
+ * exactly like a translated key, but kept distinct from {@link LocaleSummary.translated} because they
1480
+ * cost no provider usage. Always present, empty by default; empty in a dry-run and when the cache is
1481
+ * bypassed. Sorted by key.
1482
+ */
1483
+ readonly cacheHits: readonly string[];
1392
1484
  /**
1393
1485
  * Translated keys that failed the placeholder-integrity check and were withheld. Never includes a
1394
1486
  * key withheld because the provider call itself failed; see {@link LocaleSummary.providerFailures}
@@ -1440,6 +1532,32 @@ interface LocaleSummary {
1440
1532
  * flags on its own path).
1441
1533
  */
1442
1534
  readonly needsReview: readonly NeedsReviewEntry[];
1535
+ /**
1536
+ * Keys the translator left blank that still need a translation, drifted or not, sorted by key.
1537
+ * Pending work made visible: the row is skipped (nothing is written and the prior lock baseline is
1538
+ * kept), but the key is surfaced so an unfinished import is never silently reported as done. Only a
1539
+ * workbook import populates this; the provider path, which never leaves a candidate key unfilled by
1540
+ * a person, always leaves it empty.
1541
+ *
1542
+ * Membership is decided by the import-time diff, not by the `status` the row was exported with: a
1543
+ * never-translated key exports as `new`, and a first handoff is all-new, which is the most common
1544
+ * unfilled case there is. A row exported as `changed` whose key no longer needs work by import time
1545
+ * is correspondingly excluded.
1546
+ */
1547
+ readonly unfilled: readonly string[];
1548
+ /**
1549
+ * Rows a workbook import could not read for this locale, each by worksheet row number and offending
1550
+ * column, in row order. The sheet's other rows still import; a malformed row is reported, not
1551
+ * written. Only a workbook import populates this; the provider path always leaves it empty.
1552
+ */
1553
+ readonly malformedRows: readonly MalformedRowReport[];
1554
+ /**
1555
+ * Duplicate-key conflicts a workbook import found in this locale's sheet, in row order. The rule is
1556
+ * first occurrence wins: the first row for a key is judged and its later occurrences are reported
1557
+ * here and otherwise ignored. Only a workbook import populates this; the provider path always leaves
1558
+ * it empty.
1559
+ */
1560
+ readonly duplicateKeys: readonly DuplicateKeyReport[];
1443
1561
  /**
1444
1562
  * A structured, secret-free error for a locale that threw (an adapter/lock/provider-construction
1445
1563
  * failure isolated as data): present only on a "failed" locale, and only that throw path sets it.
@@ -1564,6 +1682,76 @@ interface LockWaitEvent {
1564
1682
  /** Called while an acquire is blocked, to surface wait progress; the lock module itself writes no output. */
1565
1683
  type LockWaitListener = (event: LockWaitEvent) => void;
1566
1684
 
1685
+ /**
1686
+ * One structured progress notification emitted while a run advances through its locales and their
1687
+ * provider sub-batches. Emitted to a caller-supplied {@link ProgressListener}; the SDK itself writes
1688
+ * nothing to any stream, so a caller (the CLI) is free to render these to stderr and keep stdout a
1689
+ * clean summary or NDJSON stream. Discriminated on `type`, mirroring the lock-wait event contract.
1690
+ */
1691
+ type ProgressEvent = LocaleStartedEvent | SubBatchProgressEvent | LocaleFinishedEvent | RunFinishedEvent;
1692
+ /** A locale is about to be processed: its position in the run's target-locale order. */
1693
+ interface LocaleStartedEvent {
1694
+ readonly type: "locale-started";
1695
+ /** The target locale about to run. */
1696
+ readonly locale: string;
1697
+ /** Zero-based index of this locale in the run's target-locale order. */
1698
+ readonly localeIndex: number;
1699
+ /** Total number of target locales in this run. */
1700
+ readonly totalLocales: number;
1701
+ }
1702
+ /**
1703
+ * One sub-batch of a locale's main translation loop has been reached. Emitted once per loop
1704
+ * iteration, in order, so a caller can render a "batch N of M" line. Fired at the top of the
1705
+ * iteration regardless of whether the batch then makes a provider call: a budget-withheld batch
1706
+ * (the run's token budget already tripped) still emits this event, which keeps `batchIndex` 1-based
1707
+ * and contiguous against the total. A dry-run never reaches this loop and so never emits it.
1708
+ */
1709
+ interface SubBatchProgressEvent {
1710
+ readonly type: "sub-batch";
1711
+ /** The locale whose entries this sub-batch carries. */
1712
+ readonly locale: string;
1713
+ /** One-based index of this sub-batch within the locale. */
1714
+ readonly batchIndex: number;
1715
+ /** Total number of sub-batches this locale's main translation loop runs. */
1716
+ readonly totalBatches: number;
1717
+ }
1718
+ /** A locale finished processing: how many keys it accepted (translated). */
1719
+ interface LocaleFinishedEvent {
1720
+ readonly type: "locale-finished";
1721
+ /** The target locale that finished. */
1722
+ readonly locale: string;
1723
+ /** Number of keys accepted (translated) for this locale. In a dry-run, the keys that would be translated. */
1724
+ readonly translated: number;
1725
+ /**
1726
+ * Zero-based index of this locale in the run's target-locale order, carrying the same value as the
1727
+ * matching {@link LocaleStartedEvent}. This is a correlation key, so a consumer can pair a finish
1728
+ * with its start without matching on the locale name.
1729
+ *
1730
+ * It is deliberately NOT a progress counter. The worker pool claims indices up front, so this is a
1731
+ * claim ordinal rather than a completion ordinal: under concurrency the locales finish in a
1732
+ * different order, and rendering `localeIndex + 1` of `totalLocales` on a finish produces a
1733
+ * counter that jumps around and never reaches its own total in order. Do not reintroduce one.
1734
+ */
1735
+ readonly localeIndex: number;
1736
+ /** Total number of target locales in this run, mirroring {@link LocaleStartedEvent}. */
1737
+ readonly totalLocales: number;
1738
+ }
1739
+ /** The run reached the end of its locale loop: how many locales it processed. */
1740
+ interface RunFinishedEvent {
1741
+ readonly type: "run-finished";
1742
+ /**
1743
+ * Number of locales the run processed (including failed locales: a per-locale failure is isolated
1744
+ * as a summary rather than thrown, so it is still counted here).
1745
+ */
1746
+ readonly localesCompleted: number;
1747
+ }
1748
+ /**
1749
+ * Called with each {@link ProgressEvent} as a run advances; the SDK emits no output of its own. A
1750
+ * caller (the CLI) uses this to render progress, always to stderr, so stdout stays byte-identical to
1751
+ * a run with no listener attached.
1752
+ */
1753
+ type ProgressListener = (event: ProgressEvent) => void;
1754
+
1567
1755
  /** Everything the one-shot run needs: the validated config and where/how to run it. */
1568
1756
  interface TranslateInput {
1569
1757
  /** The validated configuration (typically from {@link loadConfig}). */
@@ -1592,12 +1780,43 @@ interface TranslateInput {
1592
1780
  * is the only wait-progress signal (the CLI renders its "still waiting" line from it).
1593
1781
  */
1594
1782
  readonly onLockWait?: LockWaitListener;
1783
+ /**
1784
+ * Called as the run advances: once per locale before it starts and once after it finishes, once
1785
+ * per provider sub-batch within a locale, and once when the whole locale loop ends. Never fires a
1786
+ * sub-batch event on a dry-run (no provider call is made). The SDK writes no output; this is the
1787
+ * only progress signal (the CLI renders it to stderr, keeping stdout byte-identical).
1788
+ *
1789
+ * Pairing is not guaranteed when the run throws: a whole-run failure (for example a corrupt
1790
+ * lock-file surfacing as `LOCK_FILE_INVALID`, which re-throws instead of being isolated per locale)
1791
+ * can emit a `locale-started` event with no matching `locale-finished`, and no `run-finished` at
1792
+ * all. A per-locale failure that is isolated (not re-thrown) still emits both, and is counted in
1793
+ * `run-finished`.
1794
+ */
1795
+ readonly onProgress?: ProgressListener;
1595
1796
  /**
1596
1797
  * Override how long a locale's write lock keeps retrying before failing with `LOCK_CONTENDED`, in
1597
1798
  * milliseconds. Defaults to the lock's own 10-minute default when unset (surfaced to the CLI as
1598
1799
  * `--lock-timeout`).
1599
1800
  */
1600
1801
  readonly lockAcquireTimeoutMs?: number;
1802
+ /**
1803
+ * How many target locales may run at once, a positive integer (surfaced to the CLI as
1804
+ * `--concurrency`). Defaults to 1, which runs locales strictly in sequence and is byte-identical
1805
+ * to a run with this option unset. A value below 1 or a non-integer is rejected with a whole-run
1806
+ * `CONCURRENCY_INVALID` error. On a live run, a value greater than 1 is rejected with a whole-run
1807
+ * `CONCURRENCY_BUDGET_CONFLICT` error when the config sets a `maxTokens` budget, because
1808
+ * concurrency would make the budget's stop guarantee nondeterministic; a dry run is exempt. The
1809
+ * per-locale write locks already isolate concurrent locales on disk, so no extra locking is added.
1810
+ */
1811
+ readonly concurrency?: number;
1812
+ /**
1813
+ * Whether to use the local content-addressed translation-memory cache (`verbatra.cache.json`). On by
1814
+ * default; the CLI exposes it as `--no-cache`. When false, both the cache read and the end-of-run
1815
+ * cache write are skipped: the run makes exactly the provider calls it would with no cache present,
1816
+ * and leaves any existing cache file untouched. Ignored on a dry-run, which never reads or writes the
1817
+ * cache regardless. See the cache module for the fingerprint and degrade-to-empty contract.
1818
+ */
1819
+ readonly cache?: boolean;
1601
1820
  }
1602
1821
  /** Composition seam: inject a registry, a provider builder, and a file system for tests. */
1603
1822
  interface TranslateDeps {
@@ -1644,6 +1863,11 @@ interface TranslateDeps {
1644
1863
  * @throws {@link SdkError} `SOURCE_INVALID`: the source locale file could not be read or parsed (wraps the
1645
1864
  * adapter read error).
1646
1865
  * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is present but corrupt or oversized.
1866
+ * @throws {@link SdkError} `CONCURRENCY_INVALID`: `concurrency` is defined but not an integer of at
1867
+ * least 1; raised before any locale runs.
1868
+ * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: a live run set `concurrency` greater than 1
1869
+ * while the config configures a `maxTokens` budget; raised before any provider call (a dry run is
1870
+ * exempt).
1647
1871
  * @example
1648
1872
  * ```ts
1649
1873
  * import { loadConfig, translate } from "@verbatra/sdk";
@@ -1735,10 +1959,13 @@ interface ImportWorkbookDeps {
1735
1959
  * identical to `translate`'s.
1736
1960
  *
1737
1961
  * Whole-run failures (unknown format, unreadable/invalid/oversized workbook, corrupt lock) throw a
1738
- * structured {@link SdkError}. A per-sheet failure (a locale not in config, a broken-round-trip key,
1739
- * a write failure) is isolated as that locale's `status: "failed"`, not a throw; per-row rejections
1740
- * are withheld and reported on the locale. Dry-run validates and reports without writing any locale or
1741
- * lock file, and skips lock acquisition (there is nothing to protect).
1962
+ * structured {@link SdkError}. A per-sheet failure (a sheet named for a locale not in config, a
1963
+ * broken-round-trip key, a write failure) is isolated as that locale's `status: "failed"`, not a
1964
+ * throw; per-row rejections are withheld and reported on the locale. A configured target locale with
1965
+ * no sheet at all (a deleted, renamed, or reordered tab) is reconciled after the sheet loop and
1966
+ * reported as that locale's `status: "failed"` (`WORKBOOK_SHEET_MISSING`) rather than silently
1967
+ * dropped. Dry-run validates and reports without writing any locale or lock file, and skips lock
1968
+ * acquisition (there is nothing to protect).
1742
1969
  *
1743
1970
  * The lock-file is read once, up front, for every sheet's diff baseline. On a non-dry-run, each
1744
1971
  * sheet's write-and-lock-update step then holds that locale's `withLocaleWriteLock` for its whole
@@ -1817,7 +2044,7 @@ declare const scaffoldingMetadata: {
1817
2044
  readonly gemini: "gemini-2.5-flash";
1818
2045
  };
1819
2046
  /** The closed set of source format ids. Owned by core. */
1820
- readonly supportedFormats: readonly ["i18next-json", "vue-i18n-json", "next-intl-json", "ngx-translate-json", "xliff", "yaml", "arb"];
2047
+ readonly supportedFormats: readonly ["i18next-json", "vue-i18n-json", "next-intl-json", "ngx-translate-json", "xliff", "yaml", "arb", "properties"];
1821
2048
  };
1822
2049
 
1823
2050
  /** A minimal source-change event source. Production wraps chokidar; tests inject a stub. */
@@ -1862,8 +2089,25 @@ interface WatchInput {
1862
2089
  * one-shot run does.
1863
2090
  */
1864
2091
  readonly onLockWait?: LockWaitListener;
2092
+ /**
2093
+ * Passed through to every run's {@link TranslateInput.onProgress}: fires per locale and per provider
2094
+ * sub-batch as each run advances, so a watch caller can surface the same progress a one-shot run does.
2095
+ */
2096
+ readonly onProgress?: ProgressListener;
1865
2097
  /** Passed through to every run's {@link TranslateInput.lockAcquireTimeoutMs}; the lock's 10-minute default when unset. */
1866
2098
  readonly lockAcquireTimeoutMs?: number;
2099
+ /**
2100
+ * Passed through to every run's {@link TranslateInput.concurrency}: how many locales each cycle may
2101
+ * run at once. Defaults to 1 (strictly serial). Because the per-cycle budget tracker is fresh, a
2102
+ * value greater than 1 cannot be honored alongside a `maxTokens` budget; {@link watch} refuses that
2103
+ * combination at startup rather than letting every cycle fail on it.
2104
+ */
2105
+ readonly concurrency?: number;
2106
+ /**
2107
+ * Passed through to every run's {@link TranslateInput.cache}: when false, each run bypasses the
2108
+ * translation-memory cache (both read and write). On by default.
2109
+ */
2110
+ readonly cache?: boolean;
1867
2111
  }
1868
2112
  /** Composition seam: inject the watcher and the run for deterministic, offline tests. */
1869
2113
  interface WatchDeps {
@@ -1890,9 +2134,23 @@ interface WatchController {
1890
2134
  * `onRun` as a {@link WatchRunResult} and watching continues. Returns a controller whose `stop()`
1891
2135
  * closes the watcher and awaits the in-flight run.
1892
2136
  *
2137
+ * Both startup refusals are ordered deliberately: `concurrency` is resolved first because it is
2138
+ * decidable from the arguments alone, while the missing-source check is an I/O probe of project
2139
+ * state, so argument validation runs before any I/O and before the watcher exists. The concurrency
2140
+ * resolve passes `dryRun: false` unconditionally, which is exact rather than a simplification: there
2141
+ * is no dry-run watch, so the budget conflict always applies to a session.
2142
+ *
2143
+ * Resolving it here rather than leaving it to each cycle is the point, not an optimization: a watch
2144
+ * session is long-lived, so a per-cycle refusal would fail the initial run and every run after it,
2145
+ * indefinitely, while the session stayed alive and looked healthy.
2146
+ *
1893
2147
  * @param input - The config, optional cwd/debounce, and the `onRun` callback that receives each result.
1894
2148
  * @param deps - Optional composition seams (watcher, run, registry, provider builder, file system) for tests.
1895
2149
  * @returns A {@link WatchController}; call `stop()` to close the watcher and await the in-flight run.
2150
+ * @throws {@link SdkError} `CONCURRENCY_INVALID`: at startup only, when `concurrency` is defined but
2151
+ * not an integer of at least 1.
2152
+ * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: at startup only, when `concurrency` is
2153
+ * greater than 1 while the config sets a `maxTokens` budget.
1896
2154
  * @throws {@link SdkError} `SOURCE_UNREADABLE`: at startup only, when the source locale file is absent.
1897
2155
  * @example
1898
2156
  * ```ts
@@ -1920,4 +2178,4 @@ interface WatchController {
1920
2178
  */
1921
2179
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
1922
2180
 
1923
- 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 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 LocaleKeyIntegrity, type LocaleNotice, type LocaleSnapshotDelta, type LocaleSummary, type LockFile, type LockHolder, type LockLocaleState, type LockStateDeps, type LockStateInput, type LockStateResult, type LockWaitEvent, type LockWaitListener, 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 };
2181
+ 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 };