@verbatra/sdk 0.6.1 → 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.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var fs = require('fs');
4
3
  var path = require('path');
4
+ var zod = require('zod');
5
+ var fs = require('fs');
5
6
  var cosmiconfig = require('cosmiconfig');
6
7
  var cosmiconfigTypescriptLoader = require('cosmiconfig-typescript-loader');
7
8
  var crypto = require('crypto');
8
9
  var promises = require('fs/promises');
9
- var zod = require('zod');
10
10
  var Anthropic = require('@anthropic-ai/sdk');
11
11
  var deepl = require('deepl-node');
12
12
  var module$1 = require('module');
@@ -17,8 +17,8 @@ var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
17
17
  var xmldom = require('@xmldom/xmldom');
18
18
  var yaml = require('yaml');
19
19
  var ExcelJS = require('exceljs');
20
- var stream = require('stream');
21
20
  var JSZip = require('jszip');
21
+ var stream = require('stream');
22
22
  var chokidar = require('chokidar');
23
23
 
24
24
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -49,6 +49,96 @@ var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
49
49
  var ExcelJS__default = /*#__PURE__*/_interopDefault(ExcelJS);
50
50
  var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
51
51
 
52
+ // src/cache/translation-memory.ts
53
+ var CACHE_FILE_NAME = "verbatra.cache.json";
54
+ var CURRENT_VERSION = 1;
55
+ var EMPTY_MEMORY = { version: CURRENT_VERSION, entries: {} };
56
+ var MAX_CACHE_FILE_BYTES = 64 * 1024 * 1024;
57
+ var translationMemorySchema = zod.z.object({
58
+ version: zod.z.number().int().positive(),
59
+ entries: zod.z.record(zod.z.string(), zod.z.record(zod.z.string(), zod.z.record(zod.z.string(), zod.z.string())))
60
+ });
61
+ function cacheFilePath(cwd) {
62
+ return path.resolve(cwd, CACHE_FILE_NAME);
63
+ }
64
+ async function readTranslationMemory(path, fs) {
65
+ let read;
66
+ try {
67
+ read = await fs.readFileBounded(path, MAX_CACHE_FILE_BYTES);
68
+ } catch {
69
+ return EMPTY_MEMORY;
70
+ }
71
+ if (read.kind !== "ok") {
72
+ return EMPTY_MEMORY;
73
+ }
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(read.content);
77
+ } catch {
78
+ return EMPTY_MEMORY;
79
+ }
80
+ const result = translationMemorySchema.safeParse(parsed);
81
+ if (!result.success || result.data.version !== CURRENT_VERSION) {
82
+ return EMPTY_MEMORY;
83
+ }
84
+ return result.data;
85
+ }
86
+ function lookupMemory(memory, fingerprint, locale, contentHash2) {
87
+ return memory.entries[fingerprint]?.[locale]?.[contentHash2];
88
+ }
89
+ function applyAdditions(base, fingerprint, additionsByLocale) {
90
+ if (additionsByLocale.size === 0) {
91
+ return base;
92
+ }
93
+ const fingerprintEntries = {};
94
+ for (const [locale, hashes] of Object.entries(base.entries[fingerprint] ?? {})) {
95
+ fingerprintEntries[locale] = { ...hashes };
96
+ }
97
+ for (const [locale, hashes] of additionsByLocale) {
98
+ fingerprintEntries[locale] = { ...fingerprintEntries[locale], ...hashes };
99
+ }
100
+ return {
101
+ version: base.version,
102
+ entries: { ...base.entries, [fingerprint]: fingerprintEntries }
103
+ };
104
+ }
105
+ function additionsToRecord(additions) {
106
+ const record = {};
107
+ for (const addition of additions) {
108
+ record[addition.contentHash] = addition.value;
109
+ }
110
+ return record;
111
+ }
112
+ function sortEntries(record) {
113
+ return Object.entries(record).sort(([a], [b]) => a < b ? -1 : 1);
114
+ }
115
+ function serialize(memory) {
116
+ const entries = {};
117
+ for (const [fingerprint, locales] of sortEntries(memory.entries)) {
118
+ const localeMap = {};
119
+ for (const [locale, hashes] of sortEntries(locales)) {
120
+ localeMap[locale] = Object.fromEntries(sortEntries(hashes));
121
+ }
122
+ entries[fingerprint] = localeMap;
123
+ }
124
+ return `${JSON.stringify({ version: memory.version, entries }, null, 2)}
125
+ `;
126
+ }
127
+ async function writeTranslationMemory(path, memory, fs) {
128
+ await fs.writeFile(path, serialize(memory));
129
+ }
130
+ async function feedTranslationMemory(cwd, fs, fingerprint, additionsByLocale) {
131
+ if (additionsByLocale.size === 0) {
132
+ return;
133
+ }
134
+ try {
135
+ const path = cacheFilePath(cwd);
136
+ const memory = await readTranslationMemory(path, fs);
137
+ await writeTranslationMemory(path, applyAdditions(memory, fingerprint, additionsByLocale), fs);
138
+ } catch {
139
+ }
140
+ }
141
+
52
142
  // src/config/define-config.ts
53
143
  function defineConfig(config) {
54
144
  return config;
@@ -242,7 +332,7 @@ async function resolveGlossary(glossary, baseDir, fs) {
242
332
  var FNV_OFFSET_BASIS = 14695981039346656037n;
243
333
  var FNV_PRIME = 1099511628211n;
244
334
  var U64_MASK = (1n << 64n) - 1n;
245
- function fnv1a64(input) {
335
+ function stableStringHash(input) {
246
336
  let hash = FNV_OFFSET_BASIS;
247
337
  for (let index = 0; index < input.length; index += 1) {
248
338
  hash ^= BigInt(input.charCodeAt(index));
@@ -263,7 +353,7 @@ function canonicalize(entry) {
263
353
  ]);
264
354
  }
265
355
  function contentHash(entry) {
266
- return fnv1a64(canonicalize(entry));
356
+ return stableStringHash(canonicalize(entry));
267
357
  }
268
358
  function sorted(keys) {
269
359
  return [...keys].sort();
@@ -308,7 +398,8 @@ var SUPPORTED_FORMATS = [
308
398
  "ngx-translate-json",
309
399
  "xliff",
310
400
  "yaml",
311
- "arb"
401
+ "arb",
402
+ "properties"
312
403
  ];
313
404
  var supportedFormatSchema = zod.z.enum(SUPPORTED_FORMATS);
314
405
  var translationEntrySchema = zod.z.object({
@@ -1250,14 +1341,14 @@ function throwAbort(signal) {
1250
1341
  throw new DOMException("This operation was aborted.", "AbortError");
1251
1342
  }
1252
1343
  function delay(ms, signal) {
1253
- return new Promise((resolve9) => {
1344
+ return new Promise((resolve10) => {
1254
1345
  const onAbort = () => {
1255
1346
  clearTimeout(timer);
1256
- resolve9();
1347
+ resolve10();
1257
1348
  };
1258
1349
  const timer = setTimeout(() => {
1259
1350
  signal?.removeEventListener("abort", onAbort);
1260
- resolve9();
1351
+ resolve10();
1261
1352
  }, ms);
1262
1353
  signal?.addEventListener("abort", onAbort, { once: true });
1263
1354
  });
@@ -1995,8 +2086,8 @@ async function withLockFileGuard(cwd, fs, fn, options = {}) {
1995
2086
 
1996
2087
  // src/lock/lock-file.ts
1997
2088
  var LOCK_FILE_NAME = "verbatra.lock.json";
1998
- var CURRENT_VERSION = 1;
1999
- var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
2089
+ var CURRENT_VERSION2 = 1;
2090
+ var EMPTY_LOCK = { version: CURRENT_VERSION2, locales: {} };
2000
2091
  var MAX_LOCK_FILE_BYTES = 16 * 1024 * 1024;
2001
2092
  var lockFileSchema = zod.z.object({
2002
2093
  version: zod.z.number().int().positive(),
@@ -2025,10 +2116,10 @@ function parseLockFileRead(read, path) {
2025
2116
  if (!result.success) {
2026
2117
  throw new SdkError("LOCK_FILE_INVALID", `The lock-file at ${path} has an unexpected shape.`);
2027
2118
  }
2028
- if (result.data.version !== CURRENT_VERSION) {
2119
+ if (result.data.version !== CURRENT_VERSION2) {
2029
2120
  throw new SdkError(
2030
2121
  "LOCK_FILE_INVALID",
2031
- `The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION}.`
2122
+ `The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION2}.`
2032
2123
  );
2033
2124
  }
2034
2125
  return result.data;
@@ -2818,7 +2909,7 @@ function createTreeFileAdapter(options) {
2818
2909
  extensions,
2819
2910
  sniff,
2820
2911
  parse: parse3,
2821
- serialize,
2912
+ serialize: serialize2,
2822
2913
  deriveEntry,
2823
2914
  extractPlaceholders,
2824
2915
  computeInvalidIcuKeys,
@@ -2853,7 +2944,7 @@ function createTreeFileAdapter(options) {
2853
2944
  },
2854
2945
  async write(resource, filePath) {
2855
2946
  const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
2856
- await atomicWriteFile(filePath, serialize(tree));
2947
+ await atomicWriteFile(filePath, serialize2(tree));
2857
2948
  }
2858
2949
  };
2859
2950
  }
@@ -3110,6 +3201,393 @@ function createNgxTranslateJsonAdapter() {
3110
3201
  keyMode: "path-notation"
3111
3202
  });
3112
3203
  }
3204
+ function toEntries2(content, namespace, parseEntries) {
3205
+ try {
3206
+ return parseEntries(content, namespace);
3207
+ } catch (error) {
3208
+ rethrowStructured(error, "The file could not be parsed.");
3209
+ }
3210
+ }
3211
+ function createFlatFileAdapter(options) {
3212
+ const {
3213
+ format,
3214
+ extensions,
3215
+ sniff,
3216
+ parseEntries,
3217
+ serializeEntries,
3218
+ extractPlaceholders,
3219
+ validateMessage,
3220
+ computeInvalidIcuKeys
3221
+ } = options;
3222
+ return {
3223
+ format,
3224
+ canHandle: buildCanHandle(extensions, sniff),
3225
+ extractPlaceholders,
3226
+ validateMessage: validateMessage ?? (() => true),
3227
+ async read(filePath, locale) {
3228
+ const content = await readFileContent(filePath);
3229
+ const namespace = namespaceOf(filePath);
3230
+ const entries = toEntries2(content, namespace, parseEntries);
3231
+ const resource = { locale, namespace, format, entries };
3232
+ const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
3233
+ return { resource, invalidIcuKeys, excludedLeafPaths: [] };
3234
+ },
3235
+ async write(resource, filePath) {
3236
+ const data = await serializeEntries(resource.entries, filePath);
3237
+ await atomicWriteFile(filePath, data);
3238
+ }
3239
+ };
3240
+ }
3241
+ var SUBMESSAGE_TYPES = /* @__PURE__ */ new Set(["plural", "select", "selectordinal", "choice"]);
3242
+ var ARGUMENT_NAME = /^(?:\d+|[A-Za-z_$][\w$-]*)$/;
3243
+ function matchingBraces(value) {
3244
+ const close = new Int32Array(value.length).fill(-1);
3245
+ const open3 = [];
3246
+ for (let i = 0; i < value.length; i += 1) {
3247
+ const char = value[i];
3248
+ if (char === "{") {
3249
+ open3.push(i);
3250
+ } else if (char === "}") {
3251
+ const start = open3.pop();
3252
+ if (start !== void 0) {
3253
+ close[start] = i;
3254
+ }
3255
+ }
3256
+ }
3257
+ return close;
3258
+ }
3259
+ function findNameEnd(value, from, limit) {
3260
+ for (let i = from; i < limit; i += 1) {
3261
+ const char = value[i];
3262
+ if (char === ",") {
3263
+ return { comma: i, hasBrace: false };
3264
+ }
3265
+ if (char === "{" || char === "}") {
3266
+ return { comma: -1, hasBrace: true };
3267
+ }
3268
+ }
3269
+ return { comma: -1, hasBrace: false };
3270
+ }
3271
+ function parseArgumentAt(value, open3, close) {
3272
+ const { comma, hasBrace } = findNameEnd(value, open3 + 1, close);
3273
+ if (hasBrace) {
3274
+ return null;
3275
+ }
3276
+ const nameEnd = comma === -1 ? close : comma;
3277
+ const name = value.slice(open3 + 1, nameEnd).trim();
3278
+ if (!ARGUMENT_NAME.test(name)) {
3279
+ return null;
3280
+ }
3281
+ if (comma === -1) {
3282
+ return { name, type: null, styleStart: -1 };
3283
+ }
3284
+ const secondComma = value.indexOf(",", comma + 1);
3285
+ if (secondComma === -1 || secondComma >= close) {
3286
+ return { name, type: value.slice(comma + 1, close).trim(), styleStart: -1 };
3287
+ }
3288
+ return { name, type: value.slice(comma + 1, secondComma).trim(), styleStart: secondComma + 1 };
3289
+ }
3290
+ function canonicalToken(value, arg, close) {
3291
+ if (arg.type === null) {
3292
+ return `{${arg.name}}`;
3293
+ }
3294
+ if (arg.styleStart === -1) {
3295
+ return `{${arg.name},${arg.type}}`;
3296
+ }
3297
+ return `{${arg.name},${arg.type},${value.slice(arg.styleStart, close).trim()}}`;
3298
+ }
3299
+ function emitArgument(value, close, arg, closeIndex, out) {
3300
+ if (arg.type !== null && SUBMESSAGE_TYPES.has(arg.type)) {
3301
+ out.push(`{${arg.name},${arg.type}}`);
3302
+ if (arg.styleStart !== -1) {
3303
+ scanRange(value, close, arg.styleStart, closeIndex, false, out);
3304
+ }
3305
+ return;
3306
+ }
3307
+ out.push(canonicalToken(value, arg, closeIndex));
3308
+ }
3309
+ function scanRange(value, close, start, end, topLevel, out) {
3310
+ let i = start;
3311
+ while (i < end) {
3312
+ if (value[i] !== "{") {
3313
+ i += 1;
3314
+ continue;
3315
+ }
3316
+ if (topLevel && value[i + 1] === "{") {
3317
+ i += 2;
3318
+ continue;
3319
+ }
3320
+ const closeIndex = close[i] ?? -1;
3321
+ if (closeIndex === -1 || closeIndex >= end) {
3322
+ i += 1;
3323
+ continue;
3324
+ }
3325
+ const arg = parseArgumentAt(value, i, closeIndex);
3326
+ if (arg === null) {
3327
+ i += 1;
3328
+ continue;
3329
+ }
3330
+ emitArgument(value, close, arg, closeIndex, out);
3331
+ i = closeIndex + 1;
3332
+ }
3333
+ }
3334
+ function extractPropertiesPlaceholders(value) {
3335
+ const out = [];
3336
+ scanRange(value, matchingBraces(value), 0, value.length, true, out);
3337
+ return out;
3338
+ }
3339
+ var UNICODE_ESCAPE = /^[0-9a-fA-F]{4}$/;
3340
+ var LEADING_WHITESPACE = /^[ \t\f]+/;
3341
+ var TRAILING_TERMINATOR = /(?:\r\n|\r|\n)$/;
3342
+ function isPropertiesWhitespace(char) {
3343
+ return char === " " || char === " " || char === "\f";
3344
+ }
3345
+ function splitPhysicalLines(content) {
3346
+ if (content === "") {
3347
+ return [];
3348
+ }
3349
+ const lines = content.split(/\r\n|\r|\n/);
3350
+ if (TRAILING_TERMINATOR.test(content)) {
3351
+ lines.pop();
3352
+ }
3353
+ return lines;
3354
+ }
3355
+ function countTrailingBackslashes(line) {
3356
+ let count = 0;
3357
+ for (let i = line.length - 1; i >= 0 && line[i] === "\\"; i -= 1) {
3358
+ count += 1;
3359
+ }
3360
+ return count;
3361
+ }
3362
+ function isContinued(line) {
3363
+ return countTrailingBackslashes(line) % 2 === 1;
3364
+ }
3365
+ function isBlankLine(line) {
3366
+ return line.trim() === "";
3367
+ }
3368
+ function isCommentLine(line) {
3369
+ const trimmed = line.replace(LEADING_WHITESPACE, "");
3370
+ return trimmed.startsWith("#") || trimmed.startsWith("!");
3371
+ }
3372
+ function joinContinuation(lines, start) {
3373
+ const pieces = [];
3374
+ let index = start;
3375
+ let current = lines[start] ?? "";
3376
+ while (isContinued(current) && index + 1 < lines.length) {
3377
+ pieces.push(current.slice(0, -1));
3378
+ index += 1;
3379
+ current = (lines[index] ?? "").replace(LEADING_WHITESPACE, "");
3380
+ }
3381
+ if (isContinued(current)) {
3382
+ current = current.slice(0, -1);
3383
+ }
3384
+ pieces.push(current);
3385
+ return { logical: pieces.join(""), nextIndex: index + 1 };
3386
+ }
3387
+ function decodeSimpleEscape(char) {
3388
+ switch (char) {
3389
+ case "t":
3390
+ return " ";
3391
+ case "n":
3392
+ return "\n";
3393
+ case "r":
3394
+ return "\r";
3395
+ case "f":
3396
+ return "\f";
3397
+ default:
3398
+ return char;
3399
+ }
3400
+ }
3401
+ function decodeUnicodeEscape(raw, at) {
3402
+ const hex = raw.slice(at, at + 4);
3403
+ if (!UNICODE_ESCAPE.test(hex)) {
3404
+ throw new AdapterError("INVALID_STRUCTURE", "The file has a malformed unicode escape.");
3405
+ }
3406
+ return String.fromCharCode(Number.parseInt(hex, 16));
3407
+ }
3408
+ function decodeEscapes(raw) {
3409
+ let out = "";
3410
+ let i = 0;
3411
+ while (i < raw.length) {
3412
+ const char = raw[i];
3413
+ if (char !== "\\") {
3414
+ out += char;
3415
+ i += 1;
3416
+ continue;
3417
+ }
3418
+ const next = raw[i + 1];
3419
+ if (next === void 0) {
3420
+ break;
3421
+ }
3422
+ if (next === "u") {
3423
+ out += decodeUnicodeEscape(raw, i + 2);
3424
+ i += 6;
3425
+ continue;
3426
+ }
3427
+ out += decodeSimpleEscape(next);
3428
+ i += 2;
3429
+ }
3430
+ return out;
3431
+ }
3432
+ function keyEndIndex(logical, from) {
3433
+ let i = from;
3434
+ while (i < logical.length) {
3435
+ const char = logical[i];
3436
+ if (char === "\\") {
3437
+ i += 2;
3438
+ continue;
3439
+ }
3440
+ if (char === void 0 || isPropertiesWhitespace(char) || char === "=" || char === ":") {
3441
+ break;
3442
+ }
3443
+ i += 1;
3444
+ }
3445
+ return i;
3446
+ }
3447
+ function skipWhitespace(logical, from) {
3448
+ let i = from;
3449
+ while (i < logical.length) {
3450
+ const char = logical[i];
3451
+ if (char === void 0 || !isPropertiesWhitespace(char)) {
3452
+ break;
3453
+ }
3454
+ i += 1;
3455
+ }
3456
+ return i;
3457
+ }
3458
+ function parseEntryLine(logical) {
3459
+ const keyStart = skipWhitespace(logical, 0);
3460
+ const keyEnd = keyEndIndex(logical, keyStart);
3461
+ const afterKey = skipWhitespace(logical, keyEnd);
3462
+ const separator = logical[afterKey];
3463
+ const valueStart = separator === "=" || separator === ":" ? skipWhitespace(logical, afterKey + 1) : afterKey;
3464
+ return {
3465
+ key: decodeEscapes(logical.slice(keyStart, keyEnd)),
3466
+ value: decodeEscapes(logical.slice(valueStart))
3467
+ };
3468
+ }
3469
+ function parseItems(content) {
3470
+ const lines = splitPhysicalLines(content);
3471
+ const items = [];
3472
+ let i = 0;
3473
+ while (i < lines.length) {
3474
+ const line = lines[i] ?? "";
3475
+ if (isBlankLine(line) || isCommentLine(line)) {
3476
+ items.push({ kind: "raw", text: line });
3477
+ i += 1;
3478
+ continue;
3479
+ }
3480
+ const { logical, nextIndex } = joinContinuation(lines, i);
3481
+ items.push({ kind: "entry", ...parseEntryLine(logical) });
3482
+ i = nextIndex;
3483
+ }
3484
+ return items;
3485
+ }
3486
+ function parsePropertiesEntries(content, namespace) {
3487
+ const map = /* @__PURE__ */ new Map();
3488
+ for (const item of parseItems(content)) {
3489
+ if (item.kind === "entry") {
3490
+ map.set(item.key, {
3491
+ key: item.key,
3492
+ namespace,
3493
+ value: item.value,
3494
+ placeholders: extractPropertiesPlaceholders(item.value),
3495
+ isPlural: false
3496
+ });
3497
+ }
3498
+ }
3499
+ return map;
3500
+ }
3501
+ function unicodeEscape(code) {
3502
+ return `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
3503
+ }
3504
+ function escapeChar(char, isFirst, isKey) {
3505
+ switch (char) {
3506
+ case "\\":
3507
+ return "\\\\";
3508
+ case " ":
3509
+ return "\\t";
3510
+ case "\n":
3511
+ return "\\n";
3512
+ case "\r":
3513
+ return "\\r";
3514
+ case "\f":
3515
+ return "\\f";
3516
+ case " ":
3517
+ return isKey || isFirst ? "\\ " : " ";
3518
+ }
3519
+ if (isKey && (char === "=" || char === ":" || char === "#" || char === "!")) {
3520
+ return `\\${char}`;
3521
+ }
3522
+ const code = char.charCodeAt(0);
3523
+ return code < 32 || code > 126 ? unicodeEscape(code) : char;
3524
+ }
3525
+ function escapeString(input, isKey) {
3526
+ let out = "";
3527
+ for (let i = 0; i < input.length; i += 1) {
3528
+ const char = input[i];
3529
+ if (char !== void 0) {
3530
+ out += escapeChar(char, i === 0, isKey);
3531
+ }
3532
+ }
3533
+ return out;
3534
+ }
3535
+ function formatEntry(key, value) {
3536
+ return `${escapeString(key, true)}=${escapeString(value, false)}`;
3537
+ }
3538
+ function isFileNotFound(error) {
3539
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
3540
+ }
3541
+ async function readStructure(filePath) {
3542
+ let outcome;
3543
+ try {
3544
+ outcome = await readBounded2(filePath);
3545
+ } catch (error) {
3546
+ if (isFileNotFound(error)) {
3547
+ return [];
3548
+ }
3549
+ throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
3550
+ }
3551
+ if (outcome.kind === "not-a-file") {
3552
+ throw new AdapterError("INVALID_STRUCTURE", "The destination path is not a regular file.");
3553
+ }
3554
+ if (outcome.kind === "too-large") {
3555
+ throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
3556
+ }
3557
+ return parseItems(outcome.content);
3558
+ }
3559
+ async function serializePropertiesEntries(entries, filePath) {
3560
+ const structure = await readStructure(filePath);
3561
+ const emitted = /* @__PURE__ */ new Set();
3562
+ const lines = [];
3563
+ for (const item of structure) {
3564
+ if (item.kind === "raw") {
3565
+ lines.push(item.text);
3566
+ continue;
3567
+ }
3568
+ const entry = entries.get(item.key);
3569
+ if (entry !== void 0 && !emitted.has(item.key)) {
3570
+ lines.push(formatEntry(item.key, entry.value));
3571
+ emitted.add(item.key);
3572
+ }
3573
+ }
3574
+ for (const [key, entry] of entries) {
3575
+ if (!emitted.has(key)) {
3576
+ lines.push(formatEntry(key, entry.value));
3577
+ }
3578
+ }
3579
+ return lines.length === 0 ? "" : `${lines.join("\n")}
3580
+ `;
3581
+ }
3582
+ function createPropertiesAdapter() {
3583
+ return createFlatFileAdapter({
3584
+ format: "properties",
3585
+ extensions: [".properties"],
3586
+ parseEntries: parsePropertiesEntries,
3587
+ serializeEntries: serializePropertiesEntries,
3588
+ extractPlaceholders: extractPropertiesPlaceholders
3589
+ });
3590
+ }
3113
3591
  var AdapterRegistry = class {
3114
3592
  adapters = [];
3115
3593
  /**
@@ -3187,43 +3665,6 @@ function createVueI18nJsonAdapter() {
3187
3665
  })
3188
3666
  });
3189
3667
  }
3190
- function toEntries2(content, namespace, parseEntries) {
3191
- try {
3192
- return parseEntries(content, namespace);
3193
- } catch (error) {
3194
- rethrowStructured(error, "The file could not be parsed.");
3195
- }
3196
- }
3197
- function createFlatFileAdapter(options) {
3198
- const {
3199
- format,
3200
- extensions,
3201
- sniff,
3202
- parseEntries,
3203
- serializeEntries,
3204
- extractPlaceholders,
3205
- validateMessage,
3206
- computeInvalidIcuKeys
3207
- } = options;
3208
- return {
3209
- format,
3210
- canHandle: buildCanHandle(extensions, sniff),
3211
- extractPlaceholders,
3212
- validateMessage: validateMessage ?? (() => true),
3213
- async read(filePath, locale) {
3214
- const content = await readFileContent(filePath);
3215
- const namespace = namespaceOf(filePath);
3216
- const entries = toEntries2(content, namespace, parseEntries);
3217
- const resource = { locale, namespace, format, entries };
3218
- const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
3219
- return { resource, invalidIcuKeys, excludedLeafPaths: [] };
3220
- },
3221
- async write(resource, filePath) {
3222
- const data = await serializeEntries(resource.entries, filePath);
3223
- await atomicWriteFile(filePath, data);
3224
- }
3225
- };
3226
- }
3227
3668
  var XLIFF_PATTERN = /<(?:x|g|bx|ex|ph|it|mrk)\b[^>]*>|\{[^{}]+\}/g;
3228
3669
  function extractXliffPlaceholders(value) {
3229
3670
  const result = [];
@@ -3534,7 +3975,7 @@ function createYamlAdapter() {
3534
3975
  });
3535
3976
  }
3536
3977
  function createDefaultRegistry() {
3537
- return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter());
3978
+ return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter()).register(createPropertiesAdapter());
3538
3979
  }
3539
3980
 
3540
3981
  // src/selection/select-adapter.ts
@@ -3655,6 +4096,28 @@ async function diff(input, deps = {}) {
3655
4096
  return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
3656
4097
  }
3657
4098
 
4099
+ // src/cache/fingerprint.ts
4100
+ function fingerprintModel(provider) {
4101
+ const options = provider.options;
4102
+ const model = options.model;
4103
+ return typeof model === "string" ? model : null;
4104
+ }
4105
+ function sortGlossary(glossary) {
4106
+ if (glossary === void 0) {
4107
+ return {};
4108
+ }
4109
+ return Object.fromEntries(Object.entries(glossary).sort(([a], [b]) => a < b ? -1 : 1));
4110
+ }
4111
+ function computeFingerprint(config) {
4112
+ const canonical = JSON.stringify({
4113
+ provider: config.provider.id,
4114
+ model: fingerprintModel(config.provider),
4115
+ tone: config.tone ?? null,
4116
+ glossary: sortGlossary(config.glossary)
4117
+ });
4118
+ return stableStringHash(canonical);
4119
+ }
4120
+
3658
4121
  // src/flow/integrity-gate.ts
3659
4122
  function gateCandidateValue(sourceEntry, candidateValue, adapter) {
3660
4123
  const placeholderResult = adapter.comparePlaceholders?.(sourceEntry.value, candidateValue) ?? checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(candidateValue));
@@ -3705,6 +4168,12 @@ async function editEntry(input, deps = {}) {
3705
4168
  mode: "merge",
3706
4169
  entries: { [input.key]: contentHash(sourceEntry) }
3707
4170
  });
4171
+ await feedTranslationMemory(
4172
+ cwd,
4173
+ fs,
4174
+ computeFingerprint(config),
4175
+ /* @__PURE__ */ new Map([[locale, { [contentHash(sourceEntry)]: input.value }]])
4176
+ );
3708
4177
  return { accepted: true, value: input.value };
3709
4178
  });
3710
4179
  }
@@ -3922,13 +4391,19 @@ async function retranslateEntry(input, deps = {}) {
3922
4391
  mode: "merge",
3923
4392
  entries: { [input.key]: contentHash(sourceEntry) }
3924
4393
  });
4394
+ await feedTranslationMemory(
4395
+ cwd,
4396
+ fs,
4397
+ computeFingerprint(config),
4398
+ /* @__PURE__ */ new Map([[locale, { [contentHash(sourceEntry)]: value }]])
4399
+ );
3925
4400
  const reviewReasons = result.reviewFlags?.get(input.key)?.reasons ?? [];
3926
4401
  return { accepted: true, value, reviewReasons };
3927
4402
  });
3928
4403
  }
3929
4404
  var RUN_STATUS_DIR_NAME = ".verbatra-local";
3930
4405
  var RUN_STATUS_FILE_NAME = "run-status.json";
3931
- var CURRENT_VERSION2 = 1;
4406
+ var CURRENT_VERSION3 = 1;
3932
4407
  var MAX_RUN_STATUS_FILE_BYTES = 16 * 1024 * 1024;
3933
4408
  var reviewReasonCodeSchema = zod.z.enum([
3934
4409
  "LENGTH_RATIO_OUTLIER",
@@ -3978,7 +4453,7 @@ function toRunStatusLocale(locale) {
3978
4453
  }
3979
4454
  function buildRunStatusFile(summary, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3980
4455
  return {
3981
- version: CURRENT_VERSION2,
4456
+ version: CURRENT_VERSION3,
3982
4457
  generatedAt,
3983
4458
  ...summary.usage !== void 0 ? { usage: summary.usage } : {},
3984
4459
  ...summary.budget !== void 0 ? { budget: summary.budget } : {},
@@ -4011,7 +4486,7 @@ async function readRunStatusFile(path, fs) {
4011
4486
  return void 0;
4012
4487
  }
4013
4488
  const result = runStatusFileSchema.safeParse(parsed);
4014
- if (!result.success || result.data.version !== CURRENT_VERSION2) {
4489
+ if (!result.success || result.data.version !== CURRENT_VERSION3) {
4015
4490
  return void 0;
4016
4491
  }
4017
4492
  return fromParsed(result.data);
@@ -4090,12 +4565,16 @@ function failureSummary(locale, error) {
4090
4565
  orphaned: [],
4091
4566
  pruned: [],
4092
4567
  invalidIcuSource: [],
4568
+ cacheHits: [],
4093
4569
  integrityMismatches: [],
4094
4570
  providerFailures: [],
4095
4571
  budgetWithheld: [],
4096
4572
  generated: [],
4097
4573
  notices: [],
4098
4574
  needsReview: [],
4575
+ unfilled: [],
4576
+ malformedRows: [],
4577
+ duplicateKeys: [],
4099
4578
  error: describeError(error)
4100
4579
  };
4101
4580
  }
@@ -4104,7 +4583,7 @@ function deriveLocaleStatus(parts) {
4104
4583
  if (!withheld) {
4105
4584
  return "succeeded";
4106
4585
  }
4107
- const accepted = parts.translated.length > 0 || parts.generated.length > 0;
4586
+ const accepted = parts.translated.length > 0 || parts.cacheHits.length > 0 || parts.generated.length > 0;
4108
4587
  return accepted ? "partial" : "failed";
4109
4588
  }
4110
4589
  function partition(locales) {
@@ -4424,6 +4903,97 @@ function foldGenerationItem(item, result, adapter, accepted, withheld, providerF
4424
4903
  }
4425
4904
 
4426
4905
  // src/flow/locale-run.ts
4906
+ function partitionCacheHits(params, toTranslate) {
4907
+ const cache = params.cache;
4908
+ const hits = /* @__PURE__ */ new Map();
4909
+ if (cache === void 0) {
4910
+ return { hits, misses: toTranslate };
4911
+ }
4912
+ const misses = [];
4913
+ for (const key of toTranslate) {
4914
+ const source = params.source.entries.get(key);
4915
+ if (source === void 0) {
4916
+ continue;
4917
+ }
4918
+ const cached = lookupMemory(
4919
+ cache.snapshot,
4920
+ cache.fingerprint,
4921
+ params.targetLocale,
4922
+ contentHash(source)
4923
+ );
4924
+ if (cached !== void 0 && gateCandidateValue(source, cached, params.adapter).accepted) {
4925
+ hits.set(key, { value: cached, source });
4926
+ } else {
4927
+ misses.push(key);
4928
+ }
4929
+ }
4930
+ return { hits, misses };
4931
+ }
4932
+ function collectCacheAdditions(params, accepted, cacheHitKeys) {
4933
+ if (params.cache === void 0) {
4934
+ return [];
4935
+ }
4936
+ const additions = [];
4937
+ for (const [key, entry] of accepted) {
4938
+ if (!cacheHitKeys.has(key)) {
4939
+ additions.push({ contentHash: contentHash(entry.source), value: entry.value });
4940
+ }
4941
+ }
4942
+ return additions;
4943
+ }
4944
+ function groupMissesByContent(params, misses) {
4945
+ const byHash = /* @__PURE__ */ new Map();
4946
+ for (const key of misses) {
4947
+ const source = params.source.entries.get(key);
4948
+ if (source === void 0) {
4949
+ continue;
4950
+ }
4951
+ const existing = byHash.get(contentHash(source));
4952
+ if (existing === void 0) {
4953
+ byHash.set(contentHash(source), { representative: key, duplicates: [] });
4954
+ } else {
4955
+ existing.duplicates.push(key);
4956
+ }
4957
+ }
4958
+ return [...byHash.values()];
4959
+ }
4960
+ function fanOutContentDuplicates(params, groups, outcome) {
4961
+ for (const group of groups) {
4962
+ if (group.duplicates.length > 0) {
4963
+ applyGroupOutcome(params, group, outcome);
4964
+ }
4965
+ }
4966
+ }
4967
+ function applyGroupOutcome(params, group, outcome) {
4968
+ const acceptedRepresentative = outcome.accepted.get(group.representative);
4969
+ if (acceptedRepresentative !== void 0) {
4970
+ fanOutAccepted(params, group, acceptedRepresentative, outcome);
4971
+ return;
4972
+ }
4973
+ withheldBucketFor(group.representative, outcome).push(...group.duplicates);
4974
+ }
4975
+ function fanOutAccepted(params, group, acceptedRepresentative, outcome) {
4976
+ const flag = outcome.reviewFlags.get(group.representative);
4977
+ for (const key of group.duplicates) {
4978
+ const source = params.source.entries.get(key);
4979
+ if (source === void 0) {
4980
+ continue;
4981
+ }
4982
+ outcome.accepted.set(key, { value: acceptedRepresentative.value, source });
4983
+ if (flag !== void 0) {
4984
+ outcome.reviewFlags.set(key, flag);
4985
+ }
4986
+ }
4987
+ }
4988
+ function withheldBucketFor(representative, outcome) {
4989
+ if (outcome.integrityMismatches.includes(representative)) {
4990
+ return outcome.integrityMismatches;
4991
+ }
4992
+ if (outcome.budgetWithheld.includes(representative)) {
4993
+ return outcome.budgetWithheld;
4994
+ }
4995
+ return outcome.providerFailures;
4996
+ }
4427
4997
  function buildRequest3(params, entries) {
4428
4998
  return {
4429
4999
  sourceLocale: params.sourceLocale,
@@ -4466,6 +5036,7 @@ async function runLocale(params) {
4466
5036
  orphaned,
4467
5037
  invalidIcuSource,
4468
5038
  translated: toTranslate,
5039
+ cacheHits: [],
4469
5040
  generated: [],
4470
5041
  integrityMismatches: [],
4471
5042
  providerFailures: [],
@@ -4473,12 +5044,16 @@ async function runLocale(params) {
4473
5044
  pruned,
4474
5045
  notices: sdkNotices
4475
5046
  }),
4476
- lockEntries: {}
5047
+ lockEntries: {},
5048
+ cacheAdditions: []
4477
5049
  };
4478
5050
  }
4479
- const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
5051
+ const partition2 = partitionCacheHits(params, toTranslate);
5052
+ const cacheHitKeys = new Set(partition2.hits.keys());
5053
+ const missGroups = groupMissesByContent(params, partition2.misses);
5054
+ const entries = missGroups.map((group) => params.source.entries.get(group.representative)).filter((entry) => entry !== void 0);
4480
5055
  const startedStopped = params.budget.stopped;
4481
- const accepted = /* @__PURE__ */ new Map();
5056
+ const accepted = new Map(partition2.hits);
4482
5057
  const integrityMismatches = [];
4483
5058
  const providerFailures = [];
4484
5059
  const budgetWithheld = [];
@@ -4493,6 +5068,13 @@ async function runLocale(params) {
4493
5068
  budgetWithheld,
4494
5069
  reviewFlags
4495
5070
  );
5071
+ fanOutContentDuplicates(params, missGroups, {
5072
+ accepted,
5073
+ integrityMismatches,
5074
+ providerFailures,
5075
+ budgetWithheld,
5076
+ reviewFlags
5077
+ });
4496
5078
  const merged = new Map(target.entries);
4497
5079
  for (const key of pruned) {
4498
5080
  merged.delete(key);
@@ -4539,7 +5121,8 @@ async function runLocale(params) {
4539
5121
  unchanged: diff2.unchanged,
4540
5122
  orphaned,
4541
5123
  invalidIcuSource,
4542
- translated: [...accepted.keys()],
5124
+ translated: [...accepted.keys()].filter((key) => !cacheHitKeys.has(key)),
5125
+ cacheHits: [...cacheHitKeys].sort(),
4543
5126
  generated: generation.accepted.map((form) => form.targetKey).sort(),
4544
5127
  integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
4545
5128
  providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
@@ -4549,7 +5132,8 @@ async function runLocale(params) {
4549
5132
  needsReview: needsReviewFor(accepted.keys(), reviewFlags),
4550
5133
  ...localeUsage !== void 0 ? { usage: localeUsage } : {}
4551
5134
  }),
4552
- lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
5135
+ lockEntries: computeLockEntries(params, merged, withheld, generation.accepted),
5136
+ cacheAdditions: collectCacheAdditions(params, accepted, cacheHitKeys)
4553
5137
  };
4554
5138
  }
4555
5139
  var NO_GENERATION_RESULT = {
@@ -4600,12 +5184,16 @@ function baseSummary(parts) {
4600
5184
  orphaned: parts.orphaned,
4601
5185
  pruned: parts.pruned,
4602
5186
  invalidIcuSource: parts.invalidIcuSource,
5187
+ cacheHits: parts.cacheHits,
4603
5188
  integrityMismatches: parts.integrityMismatches,
4604
5189
  providerFailures: parts.providerFailures,
4605
5190
  budgetWithheld: parts.budgetWithheld,
4606
5191
  generated: parts.generated,
4607
5192
  notices: parts.notices,
4608
5193
  needsReview: parts.needsReview ?? [],
5194
+ unfilled: [],
5195
+ malformedRows: [],
5196
+ duplicateKeys: [],
4609
5197
  ...parts.usage !== void 0 ? { usage: parts.usage } : {}
4610
5198
  };
4611
5199
  }
@@ -4623,7 +5211,16 @@ async function translateAndCheck(provider, params, entries, accepted, integrityM
4623
5211
  const notices = [];
4624
5212
  const usage = createUsageAccumulator();
4625
5213
  let tripped = false;
4626
- for (const batch of chunk(entries, params.maxBatchSize)) {
5214
+ const batches = chunk(entries, params.maxBatchSize);
5215
+ let batchIndex = 0;
5216
+ for (const batch of batches) {
5217
+ batchIndex += 1;
5218
+ params.onProgress?.({
5219
+ type: "sub-batch",
5220
+ locale: params.targetLocale,
5221
+ batchIndex,
5222
+ totalBatches: batches.length
5223
+ });
4627
5224
  if (params.budget.stopped) {
4628
5225
  for (const entry of batch) {
4629
5226
  budgetWithheld.push(entry.key);
@@ -4769,6 +5366,26 @@ async function recordRunStatus(cwd, dryRun, summary, fs) {
4769
5366
  } catch {
4770
5367
  }
4771
5368
  }
5369
+ async function createRunCacheState(input, config, cwd, dryRun, fs) {
5370
+ if (dryRun || input.cache === false) {
5371
+ return void 0;
5372
+ }
5373
+ return {
5374
+ memory: await readTranslationMemory(cacheFilePath(cwd), fs),
5375
+ fingerprint: computeFingerprint(config),
5376
+ additions: /* @__PURE__ */ new Map()
5377
+ };
5378
+ }
5379
+ async function recordCacheAdditions(cwd, cache, fs) {
5380
+ if (cache === void 0 || cache.additions.size === 0) {
5381
+ return;
5382
+ }
5383
+ try {
5384
+ const merged = applyAdditions(cache.memory, cache.fingerprint, cache.additions);
5385
+ await writeTranslationMemory(cacheFilePath(cwd), merged, fs);
5386
+ } catch {
5387
+ }
5388
+ }
4772
5389
  function buildLocaleRunParams(context, targetLocale, baseline) {
4773
5390
  return {
4774
5391
  source: context.source.resource,
@@ -4787,7 +5404,9 @@ function buildLocaleRunParams(context, targetLocale, baseline) {
4787
5404
  generatePlurals: context.generatePlurals,
4788
5405
  maxBatchSize: context.maxBatchSize,
4789
5406
  fs: context.fs,
4790
- budget: context.budget
5407
+ budget: context.budget,
5408
+ ...context.cache !== void 0 ? { cache: { snapshot: context.cache.memory, fingerprint: context.cache.fingerprint } } : {},
5409
+ ...context.onProgress !== void 0 ? { onProgress: context.onProgress } : {}
4791
5410
  };
4792
5411
  }
4793
5412
  async function runDryLocale(context, targetLocale, lock) {
@@ -4811,6 +5430,9 @@ async function runLiveLocale(context, targetLocale) {
4811
5430
  mode: "replace",
4812
5431
  entries: result.lockEntries
4813
5432
  });
5433
+ if (context.cache !== void 0 && result.cacheAdditions.length > 0) {
5434
+ context.cache.additions.set(targetLocale, additionsToRecord(result.cacheAdditions));
5435
+ }
4814
5436
  return result.summary;
4815
5437
  },
4816
5438
  lockOptions
@@ -4826,27 +5448,88 @@ async function runOneLocale(targetLocale, run) {
4826
5448
  return failureSummary(targetLocale, error);
4827
5449
  }
4828
5450
  }
4829
- async function runAllLocalesDry(context, targetLocales) {
5451
+ async function runLocaleAt(context, targetLocales, localeIndex, runOne, results) {
5452
+ const targetLocale = targetLocales[localeIndex];
5453
+ if (targetLocale === void 0) {
5454
+ return;
5455
+ }
5456
+ context.onProgress?.({
5457
+ type: "locale-started",
5458
+ locale: targetLocale,
5459
+ localeIndex,
5460
+ totalLocales: targetLocales.length
5461
+ });
5462
+ const summary = await runOneLocale(targetLocale, () => runOne(targetLocale));
5463
+ results[localeIndex] = summary;
5464
+ context.onProgress?.({
5465
+ type: "locale-finished",
5466
+ locale: targetLocale,
5467
+ translated: summary.translated.length
5468
+ });
5469
+ }
5470
+ async function runLocalesWithProgress(context, targetLocales, runOne, concurrency) {
5471
+ const totalLocales = targetLocales.length;
5472
+ const results = new Array(totalLocales);
5473
+ let nextIndex = 0;
5474
+ async function worker() {
5475
+ while (nextIndex < totalLocales) {
5476
+ const localeIndex = nextIndex;
5477
+ nextIndex += 1;
5478
+ await runLocaleAt(context, targetLocales, localeIndex, runOne, results);
5479
+ }
5480
+ }
5481
+ const workerCount = Math.min(concurrency, totalLocales);
5482
+ const workers = [];
5483
+ for (let index = 0; index < workerCount; index += 1) {
5484
+ workers.push(worker());
5485
+ }
5486
+ await Promise.all(workers);
5487
+ return results.filter((summary) => summary !== void 0);
5488
+ }
5489
+ async function runAllLocalesDry(context, targetLocales, concurrency) {
4830
5490
  const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
4831
- const summaries = [];
4832
- for (const targetLocale of targetLocales) {
4833
- summaries.push(
4834
- await runOneLocale(targetLocale, () => runDryLocale(context, targetLocale, lock))
5491
+ return runLocalesWithProgress(
5492
+ context,
5493
+ targetLocales,
5494
+ (targetLocale) => runDryLocale(context, targetLocale, lock),
5495
+ concurrency
5496
+ );
5497
+ }
5498
+ async function runAllLocalesLive(context, targetLocales, concurrency) {
5499
+ return runLocalesWithProgress(
5500
+ context,
5501
+ targetLocales,
5502
+ (targetLocale) => runLiveLocale(context, targetLocale),
5503
+ concurrency
5504
+ );
5505
+ }
5506
+ function resolveConcurrency(value) {
5507
+ if (value === void 0) {
5508
+ return 1;
5509
+ }
5510
+ if (!Number.isInteger(value) || value < 1) {
5511
+ throw new SdkError(
5512
+ "CONCURRENCY_INVALID",
5513
+ `The concurrency option must be an integer of at least 1, got ${value}.`
4835
5514
  );
4836
5515
  }
4837
- return summaries;
5516
+ return value;
4838
5517
  }
4839
- async function runAllLocalesLive(context, targetLocales) {
4840
- const summaries = [];
4841
- for (const targetLocale of targetLocales) {
4842
- summaries.push(await runOneLocale(targetLocale, () => runLiveLocale(context, targetLocale)));
5518
+ function resolveRunConcurrency(value, dryRun, config) {
5519
+ const concurrency = resolveConcurrency(value);
5520
+ if (!dryRun && concurrency > 1 && config.maxTokens !== void 0) {
5521
+ throw new SdkError(
5522
+ "CONCURRENCY_BUDGET_CONFLICT",
5523
+ "A token budget (maxTokens) and concurrency greater than 1 cannot be combined on a live run: concurrent locales would overshoot the budget nondeterministically. Set concurrency to 1, remove maxTokens, or use --dry-run."
5524
+ );
4843
5525
  }
4844
- return summaries;
5526
+ return concurrency;
4845
5527
  }
4846
5528
  async function translate2(input, deps = {}) {
4847
5529
  const config = input.config;
4848
5530
  const cwd = input.cwd ?? process.cwd();
4849
5531
  const dryRun = input.dryRun ?? false;
5532
+ const concurrency = resolveRunConcurrency(input.concurrency, dryRun, config);
4850
5533
  const prune = input.prune ?? config.prune ?? false;
4851
5534
  const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
4852
5535
  const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
@@ -4858,6 +5541,7 @@ async function translate2(input, deps = {}) {
4858
5541
  const adapter = selectAdapter(config.format, deps.adapterRegistry);
4859
5542
  const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
4860
5543
  const source = await readSource(config, cwd, fs, adapter);
5544
+ const cache = await createRunCacheState(input, config, cwd, dryRun, fs);
4861
5545
  const context = {
4862
5546
  source,
4863
5547
  adapter,
@@ -4869,10 +5553,13 @@ async function translate2(input, deps = {}) {
4869
5553
  maxBatchSize,
4870
5554
  fs,
4871
5555
  budget,
5556
+ cache,
4872
5557
  ...input.onLockWait !== void 0 ? { onLockWait: input.onLockWait } : {},
5558
+ ...input.onProgress !== void 0 ? { onProgress: input.onProgress } : {},
4873
5559
  ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {}
4874
5560
  };
4875
- const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales) : await runAllLocalesLive(context, config.targetLocales);
5561
+ const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales, concurrency) : await runAllLocalesLive(context, config.targetLocales, concurrency);
5562
+ input.onProgress?.({ type: "run-finished", localesCompleted: summaries.length });
4876
5563
  const { succeeded, partial, failed } = partition(summaries);
4877
5564
  const usage = summaries.reduce(
4878
5565
  (total, summary2) => combineUsage(total, summary2.usage),
@@ -4888,6 +5575,7 @@ async function translate2(input, deps = {}) {
4888
5575
  ...usage !== void 0 ? { usage } : {},
4889
5576
  ...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
4890
5577
  };
5578
+ await recordCacheAdditions(cwd, cache, fs);
4891
5579
  await recordRunStatus(cwd, dryRun, summary, fs);
4892
5580
  return summary;
4893
5581
  }
@@ -4903,8 +5591,12 @@ var INSTRUCTIONS_LINES = [
4903
5591
  "How to use this workbook",
4904
5592
  "",
4905
5593
  "1. There is one sheet per language. Open the sheet named for the language you are translating.",
5594
+ " Do not rename, delete, or reorder the language tabs. verbatra matches each tab to a language by",
5595
+ " its exact name; a renamed or missing tab is reported and its translations are not imported.",
4906
5596
  "2. Fill ONLY the 'Translation' column. Leave every other column unchanged.",
4907
- "3. An empty 'Translation' cell means 'not translated yet'. It is skipped, never written as an empty string.",
5597
+ "3. An empty 'Translation' cell means 'not translated yet'. It is skipped, never written as an empty",
5598
+ " string. A cell that contains only spaces is treated the same as an empty cell.",
5599
+ " To deliberately clear an existing value (set it to empty), type exactly [[CLEAR]] in the cell.",
4908
5600
  "4. Keep placeholders exactly as they appear. A token such as {name} or {count} must stay verbatim,",
4909
5601
  " in your translation, with the same spelling. Do not translate or remove it.",
4910
5602
  "5. For ICU messages (for example {count, plural, one {# item} other {# items}}), keep the ICU",
@@ -5069,6 +5761,26 @@ function buildInstructionsSheet(workbook) {
5069
5761
  }
5070
5762
  sheet.getRow(1).font = { bold: true };
5071
5763
  }
5764
+ var WORKBOOK_PROTECTION_XML = '<workbookProtection lockStructure="1" lockWindows="0"/>';
5765
+ function spliceWorkbookProtection(xml) {
5766
+ const anchor = xml.includes("<bookViews") ? "<bookViews" : "<sheets";
5767
+ return xml.replace(anchor, `${WORKBOOK_PROTECTION_XML}${anchor}`);
5768
+ }
5769
+ async function protectWorkbookStructure(bytes) {
5770
+ let zip;
5771
+ try {
5772
+ zip = await JSZip__default.default.loadAsync(bytes);
5773
+ } catch {
5774
+ throw new ExchangeError("WORKBOOK_INVALID", "The workbook could not be serialized.");
5775
+ }
5776
+ const workbookPart = zip.file("xl/workbook.xml");
5777
+ if (workbookPart === null) {
5778
+ return bytes;
5779
+ }
5780
+ const xml = await workbookPart.async("string");
5781
+ zip.file("xl/workbook.xml", spliceWorkbookProtection(xml));
5782
+ return zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
5783
+ }
5072
5784
  async function buildWorkbook(model) {
5073
5785
  assertNoWorksheetNameCollisions(model.sheets);
5074
5786
  const workbook = new ExcelJS__default.default.Workbook();
@@ -5076,13 +5788,15 @@ async function buildWorkbook(model) {
5076
5788
  for (const sheet of model.sheets) {
5077
5789
  await buildDataSheet(workbook, sheet);
5078
5790
  }
5791
+ let serialized;
5079
5792
  try {
5080
5793
  const buffer = await workbook.xlsx.writeBuffer();
5081
- const view = buffer;
5082
- return Uint8Array.prototype.slice.call(view);
5794
+ serialized = buffer;
5083
5795
  } catch {
5084
5796
  throw new ExchangeError("WORKBOOK_INVALID", "The workbook could not be serialized.");
5085
5797
  }
5798
+ const protectedBytes = await protectWorkbookStructure(serialized);
5799
+ return Uint8Array.prototype.slice.call(protectedBytes);
5086
5800
  }
5087
5801
  var DEFAULT_WORKBOOK_LIMITS = {
5088
5802
  maxDecompressedBytes: 64 * 1024 * 1024,
@@ -5202,26 +5916,24 @@ function assertHeader(sheet) {
5202
5916
  );
5203
5917
  }
5204
5918
  }
5205
- function parseRow(sheet, row) {
5919
+ var MALFORMED_ROW_COLUMN = "Status";
5920
+ function parseRow(row) {
5206
5921
  const candidate = {
5207
5922
  key: cellString(row.getCell(COLUMN.key)),
5208
5923
  source: cellString(row.getCell(COLUMN.source)),
5209
5924
  currentTarget: cellString(row.getCell(COLUMN.current)),
5210
5925
  status: cellString(row.getCell(COLUMN.status)),
5211
5926
  sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
5212
- translation: cellString(row.getCell(COLUMN.translation)),
5927
+ translation: cellString(row.getCell(COLUMN.translation)).trim(),
5213
5928
  context: cellString(row.getCell(COLUMN.context)),
5214
5929
  reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
5215
5930
  reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
5216
5931
  };
5217
5932
  const result = rowSchema.safeParse(candidate);
5218
5933
  if (!result.success) {
5219
- throw new ExchangeError(
5220
- "WORKBOOK_INVALID",
5221
- `The sheet "${sheet.name}" has a row with a missing key or an unrecognized status.`
5222
- );
5934
+ return { ok: false, column: MALFORMED_ROW_COLUMN };
5223
5935
  }
5224
- return result.data;
5936
+ return { ok: true, row: result.data };
5225
5937
  }
5226
5938
  function readDataSheet(sheet, limits) {
5227
5939
  assertHeader(sheet);
@@ -5232,6 +5944,9 @@ function readDataSheet(sheet, limits) {
5232
5944
  );
5233
5945
  }
5234
5946
  const rows = [];
5947
+ const malformed = [];
5948
+ const duplicates = [];
5949
+ const seenKeys = /* @__PURE__ */ new Set();
5235
5950
  for (let rowNumber = HEADER_ROW + 1; rowNumber <= sheet.rowCount; rowNumber += 1) {
5236
5951
  const row = sheet.getRow(rowNumber);
5237
5952
  if (row.cellCount > limits.maxCellsPerRow) {
@@ -5243,9 +5958,19 @@ function readDataSheet(sheet, limits) {
5243
5958
  if (cellString(row.getCell(COLUMN.key)) === "") {
5244
5959
  continue;
5245
5960
  }
5246
- rows.push(parseRow(sheet, row));
5961
+ const outcome = parseRow(row);
5962
+ if (!outcome.ok) {
5963
+ malformed.push({ locale: sheet.name, row: rowNumber, column: outcome.column });
5964
+ continue;
5965
+ }
5966
+ if (seenKeys.has(outcome.row.key)) {
5967
+ duplicates.push({ locale: sheet.name, key: outcome.row.key, row: rowNumber });
5968
+ continue;
5969
+ }
5970
+ seenKeys.add(outcome.row.key);
5971
+ rows.push(outcome.row);
5247
5972
  }
5248
- return { locale: sheet.name, rows };
5973
+ return { sheet: { locale: sheet.name, rows }, malformed, duplicates };
5249
5974
  }
5250
5975
  async function loadWorkbook(bytes) {
5251
5976
  const workbook = new ExcelJS__default.default.Workbook();
@@ -5268,13 +5993,18 @@ async function readWorkbook(bytes, options = {}) {
5268
5993
  );
5269
5994
  }
5270
5995
  const sheets = [];
5996
+ const malformedRows = [];
5997
+ const duplicateKeys = [];
5271
5998
  for (const sheet of workbook.worksheets) {
5272
5999
  if (sheet.name === INSTRUCTIONS_SHEET_NAME) {
5273
6000
  continue;
5274
6001
  }
5275
- sheets.push(readDataSheet(sheet, limits));
6002
+ const read = readDataSheet(sheet, limits);
6003
+ sheets.push(read.sheet);
6004
+ malformedRows.push(...read.malformed);
6005
+ duplicateKeys.push(...read.duplicates);
5276
6006
  }
5277
- return { sheets };
6007
+ return { sheets, malformedRows, duplicateKeys };
5278
6008
  }
5279
6009
 
5280
6010
  // src/flow/workbook/export-workbook.ts
@@ -5375,6 +6105,7 @@ async function exportWorkbook(input, deps = {}) {
5375
6105
  }
5376
6106
 
5377
6107
  // src/flow/workbook/import-locale.ts
6108
+ var CLEAR_SENTINEL = "[[CLEAR]]";
5378
6109
  var UnknownKeyError = class extends Error {
5379
6110
  key;
5380
6111
  constructor(key) {
@@ -5403,9 +6134,20 @@ function trackBlankDrift(row, params, buckets) {
5403
6134
  buckets.blankDrifted.add(row.key);
5404
6135
  }
5405
6136
  }
6137
+ function classifyClear(row, sourceEntry, buckets) {
6138
+ if (contentHash(sourceEntry) !== row.sourceHash) {
6139
+ buckets.mismatches.push(row.key);
6140
+ buckets.withheld.add(row.key);
6141
+ return;
6142
+ }
6143
+ buckets.accepted.set(row.key, { value: "", source: sourceEntry });
6144
+ }
5406
6145
  function classifyRows(params, buckets) {
5407
6146
  for (const row of params.sheet.rows) {
5408
6147
  if (row.translation === "") {
6148
+ if (row.status === "changed") {
6149
+ buckets.unfilled.push(row.key);
6150
+ }
5409
6151
  trackBlankDrift(row, params, buckets);
5410
6152
  continue;
5411
6153
  }
@@ -5416,6 +6158,10 @@ function classifyRows(params, buckets) {
5416
6158
  if (sourceEntry === void 0) {
5417
6159
  continue;
5418
6160
  }
6161
+ if (row.translation === CLEAR_SENTINEL) {
6162
+ classifyClear(row, sourceEntry, buckets);
6163
+ continue;
6164
+ }
5419
6165
  const reason = judge(row, sourceEntry, params.adapter);
5420
6166
  if (reason === void 0) {
5421
6167
  buckets.accepted.set(row.key, { value: row.translation, source: sourceEntry });
@@ -5437,7 +6183,8 @@ function importLocale(params) {
5437
6183
  accepted: /* @__PURE__ */ new Map(),
5438
6184
  mismatches: [],
5439
6185
  withheld: /* @__PURE__ */ new Set(),
5440
- blankDrifted: /* @__PURE__ */ new Set()
6186
+ blankDrifted: /* @__PURE__ */ new Set(),
6187
+ unfilled: []
5441
6188
  };
5442
6189
  classifyRows(params, buckets);
5443
6190
  const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
@@ -5448,6 +6195,7 @@ function importLocale(params) {
5448
6195
  locale: params.sheet.locale,
5449
6196
  status: deriveLocaleStatus({
5450
6197
  translated,
6198
+ cacheHits: [],
5451
6199
  generated: [],
5452
6200
  integrityMismatches,
5453
6201
  providerFailures: [],
@@ -5458,12 +6206,16 @@ function importLocale(params) {
5458
6206
  orphaned: diff2.orphaned,
5459
6207
  pruned: [],
5460
6208
  invalidIcuSource,
6209
+ cacheHits: [],
5461
6210
  integrityMismatches,
5462
6211
  providerFailures: [],
5463
6212
  budgetWithheld: [],
5464
6213
  generated: [],
5465
6214
  notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
5466
- needsReview: []
6215
+ needsReview: [],
6216
+ unfilled: [...new Set(buckets.unfilled)].sort(),
6217
+ malformedRows: params.malformedRows,
6218
+ duplicateKeys: params.duplicateKeys
5467
6219
  };
5468
6220
  return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
5469
6221
  }
@@ -5490,6 +6242,19 @@ function mergeAccepted(target, accepted) {
5490
6242
  }
5491
6243
  return merged;
5492
6244
  }
6245
+ function sheetCacheAdditions(accepted) {
6246
+ const record = {};
6247
+ for (const [, { value, source }] of accepted) {
6248
+ record[contentHash(source)] = value;
6249
+ }
6250
+ return record;
6251
+ }
6252
+ function collectSheetAdditions(byLocale, locale, additions) {
6253
+ if (Object.keys(additions).length === 0) {
6254
+ return;
6255
+ }
6256
+ byLocale.set(locale, { ...byLocale.get(locale), ...additions });
6257
+ }
5493
6258
  function computeLockEntries2(source, merged, baseline, accepted) {
5494
6259
  const entries = {};
5495
6260
  for (const key of merged.keys()) {
@@ -5506,11 +6271,20 @@ function computeLockEntries2(source, merged, baseline, accepted) {
5506
6271
  }
5507
6272
  return entries;
5508
6273
  }
6274
+ var MissingSheetError = class extends Error {
6275
+ code = "WORKBOOK_SHEET_MISSING";
6276
+ constructor(locale) {
6277
+ super(
6278
+ `The workbook has no sheet (tab) for the configured target locale "${locale}". The tab may have been renamed, deleted, or reordered out of the workbook.`
6279
+ );
6280
+ this.name = "MissingSheetError";
6281
+ }
6282
+ };
5509
6283
  async function runSheet(ctx, sheet, lock) {
5510
6284
  if (!ctx.config.targetLocales.includes(sheet.locale)) {
5511
6285
  throw new SdkError(
5512
6286
  "CONFIG_INVALID",
5513
- `The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
6287
+ `The workbook has a sheet named "${sheet.locale}", which is not a configured target locale. It may be a renamed, added, or reordered tab; leave every language tab named exactly as exported.`
5514
6288
  );
5515
6289
  }
5516
6290
  const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
@@ -5521,10 +6295,12 @@ async function runSheet(ctx, sheet, lock) {
5521
6295
  target,
5522
6296
  baseline,
5523
6297
  adapter: ctx.adapter,
5524
- sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys
6298
+ sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys,
6299
+ malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column })),
6300
+ duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row }))
5525
6301
  });
5526
6302
  if (ctx.dryRun) {
5527
- return { summary, lockEntries: {} };
6303
+ return { summary, lockEntries: {}, cacheAdditions: {} };
5528
6304
  }
5529
6305
  const merged = mergeAccepted(target, accepted);
5530
6306
  if (accepted.size > 0) {
@@ -5539,7 +6315,11 @@ async function runSheet(ctx, sheet, lock) {
5539
6315
  path
5540
6316
  );
5541
6317
  }
5542
- return { summary, lockEntries: computeLockEntries2(ctx.source, merged, baseline, accepted) };
6318
+ return {
6319
+ summary,
6320
+ lockEntries: computeLockEntries2(ctx.source, merged, baseline, accepted),
6321
+ cacheAdditions: sheetCacheAdditions(accepted)
6322
+ };
5543
6323
  }
5544
6324
  async function importWorkbook(input, deps = {}) {
5545
6325
  const config = input.config;
@@ -5564,9 +6344,12 @@ async function importWorkbook(input, deps = {}) {
5564
6344
  fs,
5565
6345
  source: source.resource,
5566
6346
  sourceInvalidIcuKeys: source.invalidIcuKeys,
5567
- dryRun
6347
+ dryRun,
6348
+ malformedRows: data.malformedRows,
6349
+ duplicateKeys: data.duplicateKeys
5568
6350
  };
5569
6351
  const summaries = [];
6352
+ const cacheAdditions = /* @__PURE__ */ new Map();
5570
6353
  for (const sheet of data.sheets) {
5571
6354
  try {
5572
6355
  let summary;
@@ -5579,6 +6362,7 @@ async function importWorkbook(input, deps = {}) {
5579
6362
  mode: "replace",
5580
6363
  entries: result.lockEntries
5581
6364
  });
6365
+ collectSheetAdditions(cacheAdditions, sheet.locale, result.cacheAdditions);
5582
6366
  return result.summary;
5583
6367
  });
5584
6368
  }
@@ -5587,6 +6371,15 @@ async function importWorkbook(input, deps = {}) {
5587
6371
  summaries.push(failureSummary(sheet.locale, error));
5588
6372
  }
5589
6373
  }
6374
+ const presentLocales = new Set(data.sheets.map((sheet) => sheet.locale));
6375
+ for (const locale of config.targetLocales) {
6376
+ if (!presentLocales.has(locale)) {
6377
+ summaries.push(failureSummary(locale, new MissingSheetError(locale)));
6378
+ }
6379
+ }
6380
+ if (!dryRun) {
6381
+ await feedTranslationMemory(cwd, fs, computeFingerprint(config), cacheAdditions);
6382
+ }
5590
6383
  const { succeeded, partial, failed } = partition(summaries);
5591
6384
  return { dryRun, locales: summaries, succeeded, partial, failed };
5592
6385
  }
@@ -5650,7 +6443,10 @@ async function watch(input, deps = {}) {
5650
6443
  config: input.config,
5651
6444
  cwd,
5652
6445
  ...input.onLockWait !== void 0 ? { onLockWait: input.onLockWait } : {},
5653
- ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {}
6446
+ ...input.onProgress !== void 0 ? { onProgress: input.onProgress } : {},
6447
+ ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {},
6448
+ ...input.concurrency !== void 0 ? { concurrency: input.concurrency } : {},
6449
+ ...input.cache !== void 0 ? { cache: input.cache } : {}
5654
6450
  };
5655
6451
  let state = "idle";
5656
6452
  let pending = false;
@@ -5718,6 +6514,7 @@ async function watch(input, deps = {}) {
5718
6514
  return { stop };
5719
6515
  }
5720
6516
 
6517
+ exports.CACHE_FILE_NAME = CACHE_FILE_NAME;
5721
6518
  exports.DEFAULT_WORKBOOK_PATH = DEFAULT_WORKBOOK_PATH;
5722
6519
  exports.LOCK_FILE_NAME = LOCK_FILE_NAME;
5723
6520
  exports.SdkError = SdkError;