@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.js CHANGED
@@ -1,10 +1,10 @@
1
- import { existsSync } from 'fs';
2
1
  import { dirname, resolve, join, basename, extname } from 'path';
2
+ import { z } from 'zod';
3
+ import { existsSync } from 'fs';
3
4
  import { cosmiconfig } from 'cosmiconfig';
4
5
  import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
5
6
  import { randomUUID } from 'crypto';
6
7
  import { rm, access, mkdir, open, writeFile, rename } from 'fs/promises';
7
- import { z } from 'zod';
8
8
  import Anthropic from '@anthropic-ai/sdk';
9
9
  import * as deepl from 'deepl-node';
10
10
  import { createRequire } from 'module';
@@ -15,10 +15,107 @@ import { parse, TYPE } from '@formatjs/icu-messageformat-parser';
15
15
  import { XMLSerializer, DOMParser } from '@xmldom/xmldom';
16
16
  import { stringify, parse as parse$1 } from 'yaml';
17
17
  import ExcelJS from 'exceljs';
18
- import { Readable } from 'stream';
19
18
  import JSZip from 'jszip';
19
+ import { Readable } from 'stream';
20
20
  import { watch as watch$1 } from 'chokidar';
21
21
 
22
+ // src/cache/translation-memory.ts
23
+ var CACHE_FILE_NAME = "verbatra.cache.json";
24
+ var CURRENT_VERSION = 1;
25
+ var EMPTY_MEMORY = { version: CURRENT_VERSION, entries: {} };
26
+ var MAX_CACHE_FILE_BYTES = 64 * 1024 * 1024;
27
+ var translationMemorySchema = z.object({
28
+ version: z.number().int().positive(),
29
+ entries: z.record(z.string(), z.record(z.string(), z.record(z.string(), z.string())))
30
+ });
31
+ function cacheFilePath(cwd) {
32
+ return resolve(cwd, CACHE_FILE_NAME);
33
+ }
34
+ var UNUSABLE = { memory: EMPTY_MEMORY, writable: true };
35
+ async function readTranslationMemory(path, fs) {
36
+ let read;
37
+ try {
38
+ read = await fs.readFileBounded(path, MAX_CACHE_FILE_BYTES);
39
+ } catch {
40
+ return UNUSABLE;
41
+ }
42
+ if (read.kind !== "ok") {
43
+ return UNUSABLE;
44
+ }
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(read.content);
48
+ } catch {
49
+ return UNUSABLE;
50
+ }
51
+ const result = translationMemorySchema.safeParse(parsed);
52
+ if (!result.success) {
53
+ return UNUSABLE;
54
+ }
55
+ if (result.data.version !== CURRENT_VERSION) {
56
+ return { memory: EMPTY_MEMORY, writable: false };
57
+ }
58
+ return { memory: result.data, writable: true };
59
+ }
60
+ function lookupMemory(memory, fingerprint, locale, contentHash2) {
61
+ return memory.entries[fingerprint]?.[locale]?.[contentHash2];
62
+ }
63
+ function applyAdditions(base, fingerprint, additionsByLocale) {
64
+ if (additionsByLocale.size === 0) {
65
+ return base;
66
+ }
67
+ const fingerprintEntries = {};
68
+ for (const [locale, hashes] of Object.entries(base.entries[fingerprint] ?? {})) {
69
+ fingerprintEntries[locale] = { ...hashes };
70
+ }
71
+ for (const [locale, hashes] of additionsByLocale) {
72
+ fingerprintEntries[locale] = { ...fingerprintEntries[locale], ...hashes };
73
+ }
74
+ return {
75
+ version: base.version,
76
+ entries: { ...base.entries, [fingerprint]: fingerprintEntries }
77
+ };
78
+ }
79
+ function additionsToRecord(additions) {
80
+ const record = {};
81
+ for (const addition of additions) {
82
+ record[addition.contentHash] = addition.value;
83
+ }
84
+ return record;
85
+ }
86
+ function sortEntries(record) {
87
+ return Object.entries(record).sort(([a], [b]) => a < b ? -1 : 1);
88
+ }
89
+ function serialize(memory) {
90
+ const entries = {};
91
+ for (const [fingerprint, locales] of sortEntries(memory.entries)) {
92
+ const localeMap = {};
93
+ for (const [locale, hashes] of sortEntries(locales)) {
94
+ localeMap[locale] = Object.fromEntries(sortEntries(hashes));
95
+ }
96
+ entries[fingerprint] = localeMap;
97
+ }
98
+ return `${JSON.stringify({ version: memory.version, entries }, null, 2)}
99
+ `;
100
+ }
101
+ async function writeTranslationMemory(path, memory, fs) {
102
+ await fs.writeFile(path, serialize(memory));
103
+ }
104
+ async function feedTranslationMemory(cwd, fs, fingerprint, additionsByLocale) {
105
+ if (additionsByLocale.size === 0) {
106
+ return;
107
+ }
108
+ try {
109
+ const path = cacheFilePath(cwd);
110
+ const { memory, writable } = await readTranslationMemory(path, fs);
111
+ if (!writable) {
112
+ return;
113
+ }
114
+ await writeTranslationMemory(path, applyAdditions(memory, fingerprint, additionsByLocale), fs);
115
+ } catch {
116
+ }
117
+ }
118
+
22
119
  // src/config/define-config.ts
23
120
  function defineConfig(config) {
24
121
  return config;
@@ -212,7 +309,7 @@ async function resolveGlossary(glossary, baseDir, fs) {
212
309
  var FNV_OFFSET_BASIS = 14695981039346656037n;
213
310
  var FNV_PRIME = 1099511628211n;
214
311
  var U64_MASK = (1n << 64n) - 1n;
215
- function fnv1a64(input) {
312
+ function stableStringHash(input) {
216
313
  let hash = FNV_OFFSET_BASIS;
217
314
  for (let index = 0; index < input.length; index += 1) {
218
315
  hash ^= BigInt(input.charCodeAt(index));
@@ -233,7 +330,7 @@ function canonicalize(entry) {
233
330
  ]);
234
331
  }
235
332
  function contentHash(entry) {
236
- return fnv1a64(canonicalize(entry));
333
+ return stableStringHash(canonicalize(entry));
237
334
  }
238
335
  function sorted(keys) {
239
336
  return [...keys].sort();
@@ -278,7 +375,8 @@ var SUPPORTED_FORMATS = [
278
375
  "ngx-translate-json",
279
376
  "xliff",
280
377
  "yaml",
281
- "arb"
378
+ "arb",
379
+ "properties"
282
380
  ];
283
381
  var supportedFormatSchema = z.enum(SUPPORTED_FORMATS);
284
382
  var translationEntrySchema = z.object({
@@ -1220,14 +1318,14 @@ function throwAbort(signal) {
1220
1318
  throw new DOMException("This operation was aborted.", "AbortError");
1221
1319
  }
1222
1320
  function delay(ms, signal) {
1223
- return new Promise((resolve9) => {
1321
+ return new Promise((resolve10) => {
1224
1322
  const onAbort = () => {
1225
1323
  clearTimeout(timer);
1226
- resolve9();
1324
+ resolve10();
1227
1325
  };
1228
1326
  const timer = setTimeout(() => {
1229
1327
  signal?.removeEventListener("abort", onAbort);
1230
- resolve9();
1328
+ resolve10();
1231
1329
  }, ms);
1232
1330
  signal?.addEventListener("abort", onAbort, { once: true });
1233
1331
  });
@@ -1965,8 +2063,8 @@ async function withLockFileGuard(cwd, fs, fn, options = {}) {
1965
2063
 
1966
2064
  // src/lock/lock-file.ts
1967
2065
  var LOCK_FILE_NAME = "verbatra.lock.json";
1968
- var CURRENT_VERSION = 1;
1969
- var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
2066
+ var CURRENT_VERSION2 = 1;
2067
+ var EMPTY_LOCK = { version: CURRENT_VERSION2, locales: {} };
1970
2068
  var MAX_LOCK_FILE_BYTES = 16 * 1024 * 1024;
1971
2069
  var lockFileSchema = z.object({
1972
2070
  version: z.number().int().positive(),
@@ -1995,10 +2093,10 @@ function parseLockFileRead(read, path) {
1995
2093
  if (!result.success) {
1996
2094
  throw new SdkError("LOCK_FILE_INVALID", `The lock-file at ${path} has an unexpected shape.`);
1997
2095
  }
1998
- if (result.data.version !== CURRENT_VERSION) {
2096
+ if (result.data.version !== CURRENT_VERSION2) {
1999
2097
  throw new SdkError(
2000
2098
  "LOCK_FILE_INVALID",
2001
- `The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION}.`
2099
+ `The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION2}.`
2002
2100
  );
2003
2101
  }
2004
2102
  return result.data;
@@ -2788,7 +2886,7 @@ function createTreeFileAdapter(options) {
2788
2886
  extensions,
2789
2887
  sniff,
2790
2888
  parse: parse3,
2791
- serialize,
2889
+ serialize: serialize2,
2792
2890
  deriveEntry,
2793
2891
  extractPlaceholders,
2794
2892
  computeInvalidIcuKeys,
@@ -2823,7 +2921,7 @@ function createTreeFileAdapter(options) {
2823
2921
  },
2824
2922
  async write(resource, filePath) {
2825
2923
  const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
2826
- await atomicWriteFile(filePath, serialize(tree));
2924
+ await atomicWriteFile(filePath, serialize2(tree));
2827
2925
  }
2828
2926
  };
2829
2927
  }
@@ -3080,6 +3178,401 @@ function createNgxTranslateJsonAdapter() {
3080
3178
  keyMode: "path-notation"
3081
3179
  });
3082
3180
  }
3181
+ function toEntries2(content, namespace, parseEntries) {
3182
+ try {
3183
+ return parseEntries(content, namespace);
3184
+ } catch (error) {
3185
+ rethrowStructured(error, "The file could not be parsed.");
3186
+ }
3187
+ }
3188
+ function createFlatFileAdapter(options) {
3189
+ const {
3190
+ format,
3191
+ extensions,
3192
+ sniff,
3193
+ parseEntries,
3194
+ serializeEntries,
3195
+ extractPlaceholders,
3196
+ validateMessage,
3197
+ computeInvalidIcuKeys
3198
+ } = options;
3199
+ return {
3200
+ format,
3201
+ canHandle: buildCanHandle(extensions, sniff),
3202
+ extractPlaceholders,
3203
+ validateMessage: validateMessage ?? (() => true),
3204
+ async read(filePath, locale) {
3205
+ const content = await readFileContent(filePath);
3206
+ const namespace = namespaceOf(filePath);
3207
+ const entries = toEntries2(content, namespace, parseEntries);
3208
+ const resource = { locale, namespace, format, entries };
3209
+ const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
3210
+ return { resource, invalidIcuKeys, excludedLeafPaths: [] };
3211
+ },
3212
+ async write(resource, filePath) {
3213
+ const data = await serializeEntries(resource.entries, filePath);
3214
+ await atomicWriteFile(filePath, data);
3215
+ }
3216
+ };
3217
+ }
3218
+ var SUBMESSAGE_TYPES = /* @__PURE__ */ new Set(["plural", "select", "selectordinal", "choice"]);
3219
+ var ARGUMENT_NAME = /^(?:\d+|[A-Za-z_$][\w$-]*)$/;
3220
+ function matchingBraces(value) {
3221
+ const close = new Int32Array(value.length).fill(-1);
3222
+ const open3 = [];
3223
+ for (let i = 0; i < value.length; i += 1) {
3224
+ const char = value[i];
3225
+ if (char === "{") {
3226
+ open3.push(i);
3227
+ } else if (char === "}") {
3228
+ const start = open3.pop();
3229
+ if (start !== void 0) {
3230
+ close[start] = i;
3231
+ }
3232
+ }
3233
+ }
3234
+ return close;
3235
+ }
3236
+ function findNameEnd(value, from, limit) {
3237
+ for (let i = from; i < limit; i += 1) {
3238
+ const char = value[i];
3239
+ if (char === ",") {
3240
+ return { comma: i, hasBrace: false };
3241
+ }
3242
+ if (char === "{" || char === "}") {
3243
+ return { comma: -1, hasBrace: true };
3244
+ }
3245
+ }
3246
+ return { comma: -1, hasBrace: false };
3247
+ }
3248
+ function parseArgumentAt(value, open3, close) {
3249
+ const { comma, hasBrace } = findNameEnd(value, open3 + 1, close);
3250
+ if (hasBrace) {
3251
+ return null;
3252
+ }
3253
+ const nameEnd = comma === -1 ? close : comma;
3254
+ const name = value.slice(open3 + 1, nameEnd).trim();
3255
+ if (!ARGUMENT_NAME.test(name)) {
3256
+ return null;
3257
+ }
3258
+ if (comma === -1) {
3259
+ return { name, type: null, styleStart: -1 };
3260
+ }
3261
+ const secondComma = value.indexOf(",", comma + 1);
3262
+ if (secondComma === -1 || secondComma >= close) {
3263
+ return { name, type: value.slice(comma + 1, close).trim(), styleStart: -1 };
3264
+ }
3265
+ return { name, type: value.slice(comma + 1, secondComma).trim(), styleStart: secondComma + 1 };
3266
+ }
3267
+ function canonicalToken(value, arg, close) {
3268
+ if (arg.type === null) {
3269
+ return `{${arg.name}}`;
3270
+ }
3271
+ if (arg.styleStart === -1) {
3272
+ return `{${arg.name},${arg.type}}`;
3273
+ }
3274
+ return `{${arg.name},${arg.type},${value.slice(arg.styleStart, close).trim()}}`;
3275
+ }
3276
+ function emitArgument(value, close, arg, closeIndex, out) {
3277
+ if (arg.type !== null && SUBMESSAGE_TYPES.has(arg.type)) {
3278
+ out.push(`{${arg.name},${arg.type}}`);
3279
+ if (arg.styleStart !== -1) {
3280
+ scanRange(value, close, arg.styleStart, closeIndex, false, out);
3281
+ }
3282
+ return;
3283
+ }
3284
+ out.push(canonicalToken(value, arg, closeIndex));
3285
+ }
3286
+ function scanRange(value, close, start, end, topLevel, out) {
3287
+ let i = start;
3288
+ while (i < end) {
3289
+ if (value[i] !== "{") {
3290
+ i += 1;
3291
+ continue;
3292
+ }
3293
+ if (topLevel && value[i + 1] === "{") {
3294
+ i += 2;
3295
+ continue;
3296
+ }
3297
+ const closeIndex = close[i] ?? -1;
3298
+ if (closeIndex === -1 || closeIndex >= end) {
3299
+ i += 1;
3300
+ continue;
3301
+ }
3302
+ const arg = parseArgumentAt(value, i, closeIndex);
3303
+ if (arg === null) {
3304
+ i += 1;
3305
+ continue;
3306
+ }
3307
+ emitArgument(value, close, arg, closeIndex, out);
3308
+ i = closeIndex + 1;
3309
+ }
3310
+ }
3311
+ function extractPropertiesPlaceholders(value) {
3312
+ const out = [];
3313
+ scanRange(value, matchingBraces(value), 0, value.length, true, out);
3314
+ return out;
3315
+ }
3316
+ var UNICODE_ESCAPE = /^[0-9a-fA-F]{4}$/;
3317
+ var LEADING_WHITESPACE = /^[ \t\f]+/;
3318
+ var TRAILING_TERMINATOR = /(?:\r\n|\r|\n)$/;
3319
+ function detectLineTerminator(content) {
3320
+ if (content.includes("\r\n")) {
3321
+ return "\r\n";
3322
+ }
3323
+ return content.includes("\r") ? "\r" : "\n";
3324
+ }
3325
+ function isPropertiesWhitespace(char) {
3326
+ return char === " " || char === " " || char === "\f";
3327
+ }
3328
+ function splitPhysicalLines(content) {
3329
+ if (content === "") {
3330
+ return [];
3331
+ }
3332
+ const lines = content.split(/\r\n|\r|\n/);
3333
+ if (TRAILING_TERMINATOR.test(content)) {
3334
+ lines.pop();
3335
+ }
3336
+ return lines;
3337
+ }
3338
+ function countTrailingBackslashes(line) {
3339
+ let count = 0;
3340
+ for (let i = line.length - 1; i >= 0 && line[i] === "\\"; i -= 1) {
3341
+ count += 1;
3342
+ }
3343
+ return count;
3344
+ }
3345
+ function isContinued(line) {
3346
+ return countTrailingBackslashes(line) % 2 === 1;
3347
+ }
3348
+ function isBlankLine(line) {
3349
+ return line.trim() === "";
3350
+ }
3351
+ function isCommentLine(line) {
3352
+ const trimmed = line.replace(LEADING_WHITESPACE, "");
3353
+ return trimmed.startsWith("#") || trimmed.startsWith("!");
3354
+ }
3355
+ function joinContinuation(lines, start) {
3356
+ const pieces = [];
3357
+ let index = start;
3358
+ let current = lines[start] ?? "";
3359
+ while (isContinued(current) && index + 1 < lines.length) {
3360
+ pieces.push(current.slice(0, -1));
3361
+ index += 1;
3362
+ current = (lines[index] ?? "").replace(LEADING_WHITESPACE, "");
3363
+ }
3364
+ if (isContinued(current)) {
3365
+ current = current.slice(0, -1);
3366
+ }
3367
+ pieces.push(current);
3368
+ return { logical: pieces.join(""), nextIndex: index + 1 };
3369
+ }
3370
+ function decodeSimpleEscape(char) {
3371
+ switch (char) {
3372
+ case "t":
3373
+ return " ";
3374
+ case "n":
3375
+ return "\n";
3376
+ case "r":
3377
+ return "\r";
3378
+ case "f":
3379
+ return "\f";
3380
+ default:
3381
+ return char;
3382
+ }
3383
+ }
3384
+ function decodeUnicodeEscape(raw, at) {
3385
+ const hex = raw.slice(at, at + 4);
3386
+ if (!UNICODE_ESCAPE.test(hex)) {
3387
+ throw new AdapterError("INVALID_STRUCTURE", "The file has a malformed unicode escape.");
3388
+ }
3389
+ return String.fromCharCode(Number.parseInt(hex, 16));
3390
+ }
3391
+ function decodeEscapes(raw) {
3392
+ let out = "";
3393
+ let i = 0;
3394
+ while (i < raw.length) {
3395
+ const char = raw[i];
3396
+ if (char !== "\\") {
3397
+ out += char;
3398
+ i += 1;
3399
+ continue;
3400
+ }
3401
+ const next = raw[i + 1];
3402
+ if (next === void 0) {
3403
+ break;
3404
+ }
3405
+ if (next === "u") {
3406
+ out += decodeUnicodeEscape(raw, i + 2);
3407
+ i += 6;
3408
+ continue;
3409
+ }
3410
+ out += decodeSimpleEscape(next);
3411
+ i += 2;
3412
+ }
3413
+ return out;
3414
+ }
3415
+ function keyEndIndex(logical, from) {
3416
+ let i = from;
3417
+ while (i < logical.length) {
3418
+ const char = logical[i];
3419
+ if (char === "\\") {
3420
+ i += 2;
3421
+ continue;
3422
+ }
3423
+ if (char === void 0 || isPropertiesWhitespace(char) || char === "=" || char === ":") {
3424
+ break;
3425
+ }
3426
+ i += 1;
3427
+ }
3428
+ return i;
3429
+ }
3430
+ function skipWhitespace(logical, from) {
3431
+ let i = from;
3432
+ while (i < logical.length) {
3433
+ const char = logical[i];
3434
+ if (char === void 0 || !isPropertiesWhitespace(char)) {
3435
+ break;
3436
+ }
3437
+ i += 1;
3438
+ }
3439
+ return i;
3440
+ }
3441
+ function parseEntryLine(logical) {
3442
+ const keyStart = skipWhitespace(logical, 0);
3443
+ const keyEnd = keyEndIndex(logical, keyStart);
3444
+ const afterKey = skipWhitespace(logical, keyEnd);
3445
+ const separator = logical[afterKey];
3446
+ const valueStart = separator === "=" || separator === ":" ? skipWhitespace(logical, afterKey + 1) : afterKey;
3447
+ return {
3448
+ key: decodeEscapes(logical.slice(keyStart, keyEnd)),
3449
+ value: decodeEscapes(logical.slice(valueStart))
3450
+ };
3451
+ }
3452
+ function parseItems(content) {
3453
+ const lines = splitPhysicalLines(content);
3454
+ const items = [];
3455
+ let i = 0;
3456
+ while (i < lines.length) {
3457
+ const line = lines[i] ?? "";
3458
+ if (isBlankLine(line) || isCommentLine(line)) {
3459
+ items.push({ kind: "raw", text: line });
3460
+ i += 1;
3461
+ continue;
3462
+ }
3463
+ const { logical, nextIndex } = joinContinuation(lines, i);
3464
+ items.push({ kind: "entry", ...parseEntryLine(logical) });
3465
+ i = nextIndex;
3466
+ }
3467
+ return items;
3468
+ }
3469
+ function parsePropertiesEntries(content, namespace) {
3470
+ const map = /* @__PURE__ */ new Map();
3471
+ for (const item of parseItems(content)) {
3472
+ if (item.kind === "entry") {
3473
+ map.set(item.key, {
3474
+ key: item.key,
3475
+ namespace,
3476
+ value: item.value,
3477
+ placeholders: extractPropertiesPlaceholders(item.value),
3478
+ isPlural: false
3479
+ });
3480
+ }
3481
+ }
3482
+ return map;
3483
+ }
3484
+ function unicodeEscape(code) {
3485
+ return `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
3486
+ }
3487
+ function escapeChar(char, isFirst, isKey) {
3488
+ switch (char) {
3489
+ case "\\":
3490
+ return "\\\\";
3491
+ case " ":
3492
+ return "\\t";
3493
+ case "\n":
3494
+ return "\\n";
3495
+ case "\r":
3496
+ return "\\r";
3497
+ case "\f":
3498
+ return "\\f";
3499
+ case " ":
3500
+ return isKey || isFirst ? "\\ " : " ";
3501
+ }
3502
+ if (isKey && (char === "=" || char === ":" || char === "#" || char === "!")) {
3503
+ return `\\${char}`;
3504
+ }
3505
+ const code = char.charCodeAt(0);
3506
+ return code < 32 || code > 126 ? unicodeEscape(code) : char;
3507
+ }
3508
+ function escapeString(input, isKey) {
3509
+ let out = "";
3510
+ for (let i = 0; i < input.length; i += 1) {
3511
+ const char = input[i];
3512
+ if (char !== void 0) {
3513
+ out += escapeChar(char, i === 0, isKey);
3514
+ }
3515
+ }
3516
+ return out;
3517
+ }
3518
+ function formatEntry(key, value) {
3519
+ return `${escapeString(key, true)}=${escapeString(value, false)}`;
3520
+ }
3521
+ function isFileNotFound(error) {
3522
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
3523
+ }
3524
+ async function readStructure(filePath) {
3525
+ let outcome;
3526
+ try {
3527
+ outcome = await readBounded2(filePath);
3528
+ } catch (error) {
3529
+ if (isFileNotFound(error)) {
3530
+ return { items: [], terminator: "\n" };
3531
+ }
3532
+ throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
3533
+ }
3534
+ if (outcome.kind === "not-a-file") {
3535
+ throw new AdapterError("INVALID_STRUCTURE", "The destination path is not a regular file.");
3536
+ }
3537
+ if (outcome.kind === "too-large") {
3538
+ throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
3539
+ }
3540
+ return {
3541
+ items: parseItems(outcome.content),
3542
+ terminator: detectLineTerminator(outcome.content)
3543
+ };
3544
+ }
3545
+ async function serializePropertiesEntries(entries, filePath) {
3546
+ const { items, terminator } = await readStructure(filePath);
3547
+ const emitted = /* @__PURE__ */ new Set();
3548
+ const lines = [];
3549
+ for (const item of items) {
3550
+ if (item.kind === "raw") {
3551
+ lines.push(item.text);
3552
+ continue;
3553
+ }
3554
+ const entry = entries.get(item.key);
3555
+ if (entry !== void 0 && !emitted.has(item.key)) {
3556
+ lines.push(formatEntry(item.key, entry.value));
3557
+ emitted.add(item.key);
3558
+ }
3559
+ }
3560
+ for (const [key, entry] of entries) {
3561
+ if (!emitted.has(key)) {
3562
+ lines.push(formatEntry(key, entry.value));
3563
+ }
3564
+ }
3565
+ return lines.length === 0 ? "" : `${lines.join(terminator)}${terminator}`;
3566
+ }
3567
+ function createPropertiesAdapter() {
3568
+ return createFlatFileAdapter({
3569
+ format: "properties",
3570
+ extensions: [".properties"],
3571
+ parseEntries: parsePropertiesEntries,
3572
+ serializeEntries: serializePropertiesEntries,
3573
+ extractPlaceholders: extractPropertiesPlaceholders
3574
+ });
3575
+ }
3083
3576
  var AdapterRegistry = class {
3084
3577
  adapters = [];
3085
3578
  /**
@@ -3157,43 +3650,6 @@ function createVueI18nJsonAdapter() {
3157
3650
  })
3158
3651
  });
3159
3652
  }
3160
- function toEntries2(content, namespace, parseEntries) {
3161
- try {
3162
- return parseEntries(content, namespace);
3163
- } catch (error) {
3164
- rethrowStructured(error, "The file could not be parsed.");
3165
- }
3166
- }
3167
- function createFlatFileAdapter(options) {
3168
- const {
3169
- format,
3170
- extensions,
3171
- sniff,
3172
- parseEntries,
3173
- serializeEntries,
3174
- extractPlaceholders,
3175
- validateMessage,
3176
- computeInvalidIcuKeys
3177
- } = options;
3178
- return {
3179
- format,
3180
- canHandle: buildCanHandle(extensions, sniff),
3181
- extractPlaceholders,
3182
- validateMessage: validateMessage ?? (() => true),
3183
- async read(filePath, locale) {
3184
- const content = await readFileContent(filePath);
3185
- const namespace = namespaceOf(filePath);
3186
- const entries = toEntries2(content, namespace, parseEntries);
3187
- const resource = { locale, namespace, format, entries };
3188
- const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
3189
- return { resource, invalidIcuKeys, excludedLeafPaths: [] };
3190
- },
3191
- async write(resource, filePath) {
3192
- const data = await serializeEntries(resource.entries, filePath);
3193
- await atomicWriteFile(filePath, data);
3194
- }
3195
- };
3196
- }
3197
3653
  var XLIFF_PATTERN = /<(?:x|g|bx|ex|ph|it|mrk)\b[^>]*>|\{[^{}]+\}/g;
3198
3654
  function extractXliffPlaceholders(value) {
3199
3655
  const result = [];
@@ -3504,7 +3960,7 @@ function createYamlAdapter() {
3504
3960
  });
3505
3961
  }
3506
3962
  function createDefaultRegistry() {
3507
- return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter());
3963
+ return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter()).register(createPropertiesAdapter());
3508
3964
  }
3509
3965
 
3510
3966
  // src/selection/select-adapter.ts
@@ -3625,6 +4081,28 @@ async function diff(input, deps = {}) {
3625
4081
  return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
3626
4082
  }
3627
4083
 
4084
+ // src/cache/fingerprint.ts
4085
+ function fingerprintModel(provider) {
4086
+ const options = provider.options;
4087
+ const model = options.model;
4088
+ return typeof model === "string" ? model : null;
4089
+ }
4090
+ function sortGlossary(glossary) {
4091
+ if (glossary === void 0) {
4092
+ return {};
4093
+ }
4094
+ return Object.fromEntries(Object.entries(glossary).sort(([a], [b]) => a < b ? -1 : 1));
4095
+ }
4096
+ function computeFingerprint(config) {
4097
+ const canonical = JSON.stringify({
4098
+ provider: config.provider.id,
4099
+ model: fingerprintModel(config.provider),
4100
+ tone: config.tone ?? null,
4101
+ glossary: sortGlossary(config.glossary)
4102
+ });
4103
+ return stableStringHash(canonical);
4104
+ }
4105
+
3628
4106
  // src/flow/integrity-gate.ts
3629
4107
  function gateCandidateValue(sourceEntry, candidateValue, adapter) {
3630
4108
  const placeholderResult = adapter.comparePlaceholders?.(sourceEntry.value, candidateValue) ?? checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(candidateValue));
@@ -3637,6 +4115,9 @@ function gateCandidateValue(sourceEntry, candidateValue, adapter) {
3637
4115
  if (assessValueDegeneracy(sourceEntry.value, candidateValue).degenerate) {
3638
4116
  return { accepted: false, reason: "degenerate" };
3639
4117
  }
4118
+ if (sourceEntry.value.trim() !== "" && candidateValue.trim() === "") {
4119
+ return { accepted: false, reason: "empty" };
4120
+ }
3640
4121
  return { accepted: true };
3641
4122
  }
3642
4123
 
@@ -3675,6 +4156,12 @@ async function editEntry(input, deps = {}) {
3675
4156
  mode: "merge",
3676
4157
  entries: { [input.key]: contentHash(sourceEntry) }
3677
4158
  });
4159
+ await feedTranslationMemory(
4160
+ cwd,
4161
+ fs,
4162
+ computeFingerprint(config),
4163
+ /* @__PURE__ */ new Map([[locale, { [contentHash(sourceEntry)]: input.value }]])
4164
+ );
3678
4165
  return { accepted: true, value: input.value };
3679
4166
  });
3680
4167
  }
@@ -3892,13 +4379,19 @@ async function retranslateEntry(input, deps = {}) {
3892
4379
  mode: "merge",
3893
4380
  entries: { [input.key]: contentHash(sourceEntry) }
3894
4381
  });
4382
+ await feedTranslationMemory(
4383
+ cwd,
4384
+ fs,
4385
+ computeFingerprint(config),
4386
+ /* @__PURE__ */ new Map([[locale, { [contentHash(sourceEntry)]: value }]])
4387
+ );
3895
4388
  const reviewReasons = result.reviewFlags?.get(input.key)?.reasons ?? [];
3896
4389
  return { accepted: true, value, reviewReasons };
3897
4390
  });
3898
4391
  }
3899
4392
  var RUN_STATUS_DIR_NAME = ".verbatra-local";
3900
4393
  var RUN_STATUS_FILE_NAME = "run-status.json";
3901
- var CURRENT_VERSION2 = 1;
4394
+ var CURRENT_VERSION3 = 1;
3902
4395
  var MAX_RUN_STATUS_FILE_BYTES = 16 * 1024 * 1024;
3903
4396
  var reviewReasonCodeSchema = z.enum([
3904
4397
  "LENGTH_RATIO_OUTLIER",
@@ -3948,7 +4441,7 @@ function toRunStatusLocale(locale) {
3948
4441
  }
3949
4442
  function buildRunStatusFile(summary, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3950
4443
  return {
3951
- version: CURRENT_VERSION2,
4444
+ version: CURRENT_VERSION3,
3952
4445
  generatedAt,
3953
4446
  ...summary.usage !== void 0 ? { usage: summary.usage } : {},
3954
4447
  ...summary.budget !== void 0 ? { budget: summary.budget } : {},
@@ -3981,7 +4474,7 @@ async function readRunStatusFile(path, fs) {
3981
4474
  return void 0;
3982
4475
  }
3983
4476
  const result = runStatusFileSchema.safeParse(parsed);
3984
- if (!result.success || result.data.version !== CURRENT_VERSION2) {
4477
+ if (!result.success || result.data.version !== CURRENT_VERSION3) {
3985
4478
  return void 0;
3986
4479
  }
3987
4480
  return fromParsed(result.data);
@@ -4060,12 +4553,16 @@ function failureSummary(locale, error) {
4060
4553
  orphaned: [],
4061
4554
  pruned: [],
4062
4555
  invalidIcuSource: [],
4556
+ cacheHits: [],
4063
4557
  integrityMismatches: [],
4064
4558
  providerFailures: [],
4065
4559
  budgetWithheld: [],
4066
4560
  generated: [],
4067
4561
  notices: [],
4068
4562
  needsReview: [],
4563
+ unfilled: [],
4564
+ malformedRows: [],
4565
+ duplicateKeys: [],
4069
4566
  error: describeError(error)
4070
4567
  };
4071
4568
  }
@@ -4074,7 +4571,7 @@ function deriveLocaleStatus(parts) {
4074
4571
  if (!withheld) {
4075
4572
  return "succeeded";
4076
4573
  }
4077
- const accepted = parts.translated.length > 0 || parts.generated.length > 0;
4574
+ const accepted = parts.translated.length > 0 || parts.cacheHits.length > 0 || parts.generated.length > 0;
4078
4575
  return accepted ? "partial" : "failed";
4079
4576
  }
4080
4577
  function partition(locales) {
@@ -4394,6 +4891,101 @@ function foldGenerationItem(item, result, adapter, accepted, withheld, providerF
4394
4891
  }
4395
4892
 
4396
4893
  // src/flow/locale-run.ts
4894
+ function partitionCacheHits(params, toTranslate) {
4895
+ const cache = params.cache;
4896
+ const hits = /* @__PURE__ */ new Map();
4897
+ if (cache === void 0) {
4898
+ return { hits, misses: toTranslate };
4899
+ }
4900
+ const misses = [];
4901
+ for (const key of toTranslate) {
4902
+ const source = params.source.entries.get(key);
4903
+ if (source === void 0) {
4904
+ continue;
4905
+ }
4906
+ const cached = lookupMemory(
4907
+ cache.snapshot,
4908
+ cache.fingerprint,
4909
+ params.targetLocale,
4910
+ contentHash(source)
4911
+ );
4912
+ if (cached !== void 0 && gateCandidateValue(source, cached, params.adapter).accepted) {
4913
+ hits.set(key, { value: cached, source });
4914
+ } else {
4915
+ misses.push(key);
4916
+ }
4917
+ }
4918
+ return { hits, misses };
4919
+ }
4920
+ function collectCacheAdditions(params, accepted, cacheHitKeys) {
4921
+ if (params.cache === void 0) {
4922
+ return [];
4923
+ }
4924
+ const additions = [];
4925
+ for (const [key, entry] of accepted) {
4926
+ if (!cacheHitKeys.has(key)) {
4927
+ additions.push({ contentHash: contentHash(entry.source), value: entry.value });
4928
+ }
4929
+ }
4930
+ return additions;
4931
+ }
4932
+ function groupMissesByContent(params, misses) {
4933
+ const byHash = /* @__PURE__ */ new Map();
4934
+ for (const key of misses) {
4935
+ const source = params.source.entries.get(key);
4936
+ if (source === void 0) {
4937
+ continue;
4938
+ }
4939
+ const existing = byHash.get(contentHash(source));
4940
+ if (existing === void 0) {
4941
+ byHash.set(contentHash(source), { representative: key, duplicates: [] });
4942
+ } else {
4943
+ existing.duplicates.push(key);
4944
+ }
4945
+ }
4946
+ return [...byHash.values()];
4947
+ }
4948
+ function fanOutContentDuplicates(params, groups, outcome) {
4949
+ for (const group of groups) {
4950
+ if (group.duplicates.length > 0) {
4951
+ applyGroupOutcome(params, group, outcome);
4952
+ }
4953
+ }
4954
+ }
4955
+ function applyGroupOutcome(params, group, outcome) {
4956
+ const acceptedRepresentative = outcome.accepted.get(group.representative);
4957
+ if (acceptedRepresentative !== void 0) {
4958
+ fanOutAccepted(params, group, acceptedRepresentative, outcome);
4959
+ return;
4960
+ }
4961
+ withheldBucketFor(group.representative, outcome).push(...group.duplicates);
4962
+ }
4963
+ function fanOutAccepted(params, group, acceptedRepresentative, outcome) {
4964
+ const flag = outcome.reviewFlags.get(group.representative);
4965
+ for (const key of group.duplicates) {
4966
+ const source = params.source.entries.get(key);
4967
+ if (source === void 0) {
4968
+ continue;
4969
+ }
4970
+ if (!gateCandidateValue(source, acceptedRepresentative.value, params.adapter).accepted) {
4971
+ outcome.integrityMismatches.push(key);
4972
+ continue;
4973
+ }
4974
+ outcome.accepted.set(key, { value: acceptedRepresentative.value, source });
4975
+ if (flag !== void 0) {
4976
+ outcome.reviewFlags.set(key, flag);
4977
+ }
4978
+ }
4979
+ }
4980
+ function withheldBucketFor(representative, outcome) {
4981
+ if (outcome.integrityMismatches.includes(representative)) {
4982
+ return outcome.integrityMismatches;
4983
+ }
4984
+ if (outcome.budgetWithheld.includes(representative)) {
4985
+ return outcome.budgetWithheld;
4986
+ }
4987
+ return outcome.providerFailures;
4988
+ }
4397
4989
  function buildRequest3(params, entries) {
4398
4990
  return {
4399
4991
  sourceLocale: params.sourceLocale,
@@ -4436,6 +5028,7 @@ async function runLocale(params) {
4436
5028
  orphaned,
4437
5029
  invalidIcuSource,
4438
5030
  translated: toTranslate,
5031
+ cacheHits: [],
4439
5032
  generated: [],
4440
5033
  integrityMismatches: [],
4441
5034
  providerFailures: [],
@@ -4443,12 +5036,16 @@ async function runLocale(params) {
4443
5036
  pruned,
4444
5037
  notices: sdkNotices
4445
5038
  }),
4446
- lockEntries: {}
5039
+ lockEntries: {},
5040
+ cacheAdditions: []
4447
5041
  };
4448
5042
  }
4449
- const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
5043
+ const partition2 = partitionCacheHits(params, toTranslate);
5044
+ const cacheHitKeys = new Set(partition2.hits.keys());
5045
+ const missGroups = groupMissesByContent(params, partition2.misses);
5046
+ const entries = missGroups.map((group) => params.source.entries.get(group.representative)).filter((entry) => entry !== void 0);
4450
5047
  const startedStopped = params.budget.stopped;
4451
- const accepted = /* @__PURE__ */ new Map();
5048
+ const accepted = new Map(partition2.hits);
4452
5049
  const integrityMismatches = [];
4453
5050
  const providerFailures = [];
4454
5051
  const budgetWithheld = [];
@@ -4463,6 +5060,13 @@ async function runLocale(params) {
4463
5060
  budgetWithheld,
4464
5061
  reviewFlags
4465
5062
  );
5063
+ fanOutContentDuplicates(params, missGroups, {
5064
+ accepted,
5065
+ integrityMismatches,
5066
+ providerFailures,
5067
+ budgetWithheld,
5068
+ reviewFlags
5069
+ });
4466
5070
  const merged = new Map(target.entries);
4467
5071
  for (const key of pruned) {
4468
5072
  merged.delete(key);
@@ -4509,7 +5113,8 @@ async function runLocale(params) {
4509
5113
  unchanged: diff2.unchanged,
4510
5114
  orphaned,
4511
5115
  invalidIcuSource,
4512
- translated: [...accepted.keys()],
5116
+ translated: [...accepted.keys()].filter((key) => !cacheHitKeys.has(key)),
5117
+ cacheHits: [...cacheHitKeys].sort(),
4513
5118
  generated: generation.accepted.map((form) => form.targetKey).sort(),
4514
5119
  integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
4515
5120
  providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
@@ -4519,7 +5124,8 @@ async function runLocale(params) {
4519
5124
  needsReview: needsReviewFor(accepted.keys(), reviewFlags),
4520
5125
  ...localeUsage !== void 0 ? { usage: localeUsage } : {}
4521
5126
  }),
4522
- lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
5127
+ lockEntries: computeLockEntries(params, merged, withheld, generation.accepted),
5128
+ cacheAdditions: collectCacheAdditions(params, accepted, cacheHitKeys)
4523
5129
  };
4524
5130
  }
4525
5131
  var NO_GENERATION_RESULT = {
@@ -4570,12 +5176,16 @@ function baseSummary(parts) {
4570
5176
  orphaned: parts.orphaned,
4571
5177
  pruned: parts.pruned,
4572
5178
  invalidIcuSource: parts.invalidIcuSource,
5179
+ cacheHits: parts.cacheHits,
4573
5180
  integrityMismatches: parts.integrityMismatches,
4574
5181
  providerFailures: parts.providerFailures,
4575
5182
  budgetWithheld: parts.budgetWithheld,
4576
5183
  generated: parts.generated,
4577
5184
  notices: parts.notices,
4578
5185
  needsReview: parts.needsReview ?? [],
5186
+ unfilled: [],
5187
+ malformedRows: [],
5188
+ duplicateKeys: [],
4579
5189
  ...parts.usage !== void 0 ? { usage: parts.usage } : {}
4580
5190
  };
4581
5191
  }
@@ -4593,7 +5203,16 @@ async function translateAndCheck(provider, params, entries, accepted, integrityM
4593
5203
  const notices = [];
4594
5204
  const usage = createUsageAccumulator();
4595
5205
  let tripped = false;
4596
- for (const batch of chunk(entries, params.maxBatchSize)) {
5206
+ const batches = chunk(entries, params.maxBatchSize);
5207
+ let batchIndex = 0;
5208
+ for (const batch of batches) {
5209
+ batchIndex += 1;
5210
+ params.onProgress?.({
5211
+ type: "sub-batch",
5212
+ locale: params.targetLocale,
5213
+ batchIndex,
5214
+ totalBatches: batches.length
5215
+ });
4597
5216
  if (params.budget.stopped) {
4598
5217
  for (const entry of batch) {
4599
5218
  budgetWithheld.push(entry.key);
@@ -4739,6 +5358,33 @@ async function recordRunStatus(cwd, dryRun, summary, fs) {
4739
5358
  } catch {
4740
5359
  }
4741
5360
  }
5361
+ async function createRunCacheState(input, config, cwd, dryRun, fs) {
5362
+ if (dryRun || input.cache === false) {
5363
+ return void 0;
5364
+ }
5365
+ const { memory, writable } = await readTranslationMemory(cacheFilePath(cwd), fs);
5366
+ return { memory, writable, fingerprint: computeFingerprint(config), additions: /* @__PURE__ */ new Map() };
5367
+ }
5368
+ function withCacheNotices(summaries, cache) {
5369
+ if (cache === void 0 || cache.writable) {
5370
+ return [...summaries];
5371
+ }
5372
+ const notice = {
5373
+ code: "CACHE_VERSION_UNRECOGNIZED",
5374
+ message: `${CACHE_FILE_NAME} carries a version this build does not recognize, so it was written by a newer verbatra. It was left untouched and this run used no cache. Upgrade verbatra, or delete the file to rebuild it in this build's format.`
5375
+ };
5376
+ return summaries.map((summary) => ({ ...summary, notices: [...summary.notices, notice] }));
5377
+ }
5378
+ async function recordCacheAdditions(cwd, cache, fs) {
5379
+ if (cache === void 0 || cache.additions.size === 0 || !cache.writable) {
5380
+ return;
5381
+ }
5382
+ try {
5383
+ const merged = applyAdditions(cache.memory, cache.fingerprint, cache.additions);
5384
+ await writeTranslationMemory(cacheFilePath(cwd), merged, fs);
5385
+ } catch {
5386
+ }
5387
+ }
4742
5388
  function buildLocaleRunParams(context, targetLocale, baseline) {
4743
5389
  return {
4744
5390
  source: context.source.resource,
@@ -4757,7 +5403,9 @@ function buildLocaleRunParams(context, targetLocale, baseline) {
4757
5403
  generatePlurals: context.generatePlurals,
4758
5404
  maxBatchSize: context.maxBatchSize,
4759
5405
  fs: context.fs,
4760
- budget: context.budget
5406
+ budget: context.budget,
5407
+ ...context.cache !== void 0 ? { cache: { snapshot: context.cache.memory, fingerprint: context.cache.fingerprint } } : {},
5408
+ ...context.onProgress !== void 0 ? { onProgress: context.onProgress } : {}
4761
5409
  };
4762
5410
  }
4763
5411
  async function runDryLocale(context, targetLocale, lock) {
@@ -4781,6 +5429,9 @@ async function runLiveLocale(context, targetLocale) {
4781
5429
  mode: "replace",
4782
5430
  entries: result.lockEntries
4783
5431
  });
5432
+ if (context.cache !== void 0 && result.cacheAdditions.length > 0) {
5433
+ context.cache.additions.set(targetLocale, additionsToRecord(result.cacheAdditions));
5434
+ }
4784
5435
  return result.summary;
4785
5436
  },
4786
5437
  lockOptions
@@ -4796,27 +5447,98 @@ async function runOneLocale(targetLocale, run) {
4796
5447
  return failureSummary(targetLocale, error);
4797
5448
  }
4798
5449
  }
4799
- async function runAllLocalesDry(context, targetLocales) {
5450
+ async function runLocaleAt(context, targetLocales, localeIndex, runOne, results) {
5451
+ const targetLocale = targetLocales[localeIndex];
5452
+ if (targetLocale === void 0) {
5453
+ return;
5454
+ }
5455
+ context.onProgress?.({
5456
+ type: "locale-started",
5457
+ locale: targetLocale,
5458
+ localeIndex,
5459
+ totalLocales: targetLocales.length
5460
+ });
5461
+ const summary = await runOneLocale(targetLocale, () => runOne(targetLocale));
5462
+ results[localeIndex] = summary;
5463
+ context.onProgress?.({
5464
+ type: "locale-finished",
5465
+ locale: targetLocale,
5466
+ translated: summary.translated.length,
5467
+ localeIndex,
5468
+ totalLocales: targetLocales.length
5469
+ });
5470
+ }
5471
+ async function runLocalesWithProgress(context, targetLocales, runOne, concurrency) {
5472
+ const totalLocales = targetLocales.length;
5473
+ const results = new Array(totalLocales);
5474
+ let nextIndex = 0;
5475
+ let abort;
5476
+ async function worker() {
5477
+ while (abort === void 0 && nextIndex < totalLocales) {
5478
+ const localeIndex = nextIndex;
5479
+ nextIndex += 1;
5480
+ try {
5481
+ await runLocaleAt(context, targetLocales, localeIndex, runOne, results);
5482
+ } catch (error) {
5483
+ abort ??= { reason: error };
5484
+ }
5485
+ }
5486
+ }
5487
+ const workerCount = Math.min(concurrency, totalLocales);
5488
+ const workers = [];
5489
+ for (let index = 0; index < workerCount; index += 1) {
5490
+ workers.push(worker());
5491
+ }
5492
+ await Promise.all(workers);
5493
+ if (abort !== void 0) {
5494
+ throw abort.reason;
5495
+ }
5496
+ return results.filter((summary) => summary !== void 0);
5497
+ }
5498
+ async function runAllLocalesDry(context, targetLocales, concurrency) {
4800
5499
  const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
4801
- const summaries = [];
4802
- for (const targetLocale of targetLocales) {
4803
- summaries.push(
4804
- await runOneLocale(targetLocale, () => runDryLocale(context, targetLocale, lock))
5500
+ return runLocalesWithProgress(
5501
+ context,
5502
+ targetLocales,
5503
+ (targetLocale) => runDryLocale(context, targetLocale, lock),
5504
+ concurrency
5505
+ );
5506
+ }
5507
+ async function runAllLocalesLive(context, targetLocales, concurrency) {
5508
+ return runLocalesWithProgress(
5509
+ context,
5510
+ targetLocales,
5511
+ (targetLocale) => runLiveLocale(context, targetLocale),
5512
+ concurrency
5513
+ );
5514
+ }
5515
+ function resolveConcurrency(value) {
5516
+ if (value === void 0) {
5517
+ return 1;
5518
+ }
5519
+ if (!Number.isInteger(value) || value < 1) {
5520
+ throw new SdkError(
5521
+ "CONCURRENCY_INVALID",
5522
+ `The concurrency option must be an integer of at least 1, got ${value}.`
4805
5523
  );
4806
5524
  }
4807
- return summaries;
5525
+ return value;
4808
5526
  }
4809
- async function runAllLocalesLive(context, targetLocales) {
4810
- const summaries = [];
4811
- for (const targetLocale of targetLocales) {
4812
- summaries.push(await runOneLocale(targetLocale, () => runLiveLocale(context, targetLocale)));
5527
+ function resolveRunConcurrency(value, dryRun, config) {
5528
+ const concurrency = resolveConcurrency(value);
5529
+ if (!dryRun && concurrency > 1 && config.maxTokens !== void 0) {
5530
+ throw new SdkError(
5531
+ "CONCURRENCY_BUDGET_CONFLICT",
5532
+ "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."
5533
+ );
4813
5534
  }
4814
- return summaries;
5535
+ return concurrency;
4815
5536
  }
4816
5537
  async function translate2(input, deps = {}) {
4817
5538
  const config = input.config;
4818
5539
  const cwd = input.cwd ?? process.cwd();
4819
5540
  const dryRun = input.dryRun ?? false;
5541
+ const concurrency = resolveRunConcurrency(input.concurrency, dryRun, config);
4820
5542
  const prune = input.prune ?? config.prune ?? false;
4821
5543
  const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
4822
5544
  const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
@@ -4828,6 +5550,7 @@ async function translate2(input, deps = {}) {
4828
5550
  const adapter = selectAdapter(config.format, deps.adapterRegistry);
4829
5551
  const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
4830
5552
  const source = await readSource(config, cwd, fs, adapter);
5553
+ const cache = await createRunCacheState(input, config, cwd, dryRun, fs);
4831
5554
  const context = {
4832
5555
  source,
4833
5556
  adapter,
@@ -4839,11 +5562,15 @@ async function translate2(input, deps = {}) {
4839
5562
  maxBatchSize,
4840
5563
  fs,
4841
5564
  budget,
5565
+ cache,
4842
5566
  ...input.onLockWait !== void 0 ? { onLockWait: input.onLockWait } : {},
5567
+ ...input.onProgress !== void 0 ? { onProgress: input.onProgress } : {},
4843
5568
  ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {}
4844
5569
  };
4845
- const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales) : await runAllLocalesLive(context, config.targetLocales);
4846
- const { succeeded, partial, failed } = partition(summaries);
5570
+ const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales, concurrency) : await runAllLocalesLive(context, config.targetLocales, concurrency);
5571
+ input.onProgress?.({ type: "run-finished", localesCompleted: summaries.length });
5572
+ const locales = withCacheNotices(summaries, cache);
5573
+ const { succeeded, partial, failed } = partition(locales);
4847
5574
  const usage = summaries.reduce(
4848
5575
  (total, summary2) => combineUsage(total, summary2.usage),
4849
5576
  void 0
@@ -4851,13 +5578,14 @@ async function translate2(input, deps = {}) {
4851
5578
  const budgetSummary = toBudgetSummary(budget);
4852
5579
  const summary = {
4853
5580
  dryRun,
4854
- locales: summaries,
5581
+ locales,
4855
5582
  succeeded,
4856
5583
  partial,
4857
5584
  failed,
4858
5585
  ...usage !== void 0 ? { usage } : {},
4859
5586
  ...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
4860
5587
  };
5588
+ await recordCacheAdditions(cwd, cache, fs);
4861
5589
  await recordRunStatus(cwd, dryRun, summary, fs);
4862
5590
  return summary;
4863
5591
  }
@@ -4873,8 +5601,12 @@ var INSTRUCTIONS_LINES = [
4873
5601
  "How to use this workbook",
4874
5602
  "",
4875
5603
  "1. There is one sheet per language. Open the sheet named for the language you are translating.",
5604
+ " Do not rename, delete, or reorder the language tabs. verbatra matches each tab to a language by",
5605
+ " its exact name; a renamed or missing tab is reported and its translations are not imported.",
4876
5606
  "2. Fill ONLY the 'Translation' column. Leave every other column unchanged.",
4877
- "3. An empty 'Translation' cell means 'not translated yet'. It is skipped, never written as an empty string.",
5607
+ "3. An empty 'Translation' cell means 'not translated yet'. It is skipped, never written as an empty",
5608
+ " string. A cell that contains only spaces is treated the same as an empty cell.",
5609
+ " To deliberately clear an existing value (set it to empty), type exactly [[CLEAR]] in the cell.",
4878
5610
  "4. Keep placeholders exactly as they appear. A token such as {name} or {count} must stay verbatim,",
4879
5611
  " in your translation, with the same spelling. Do not translate or remove it.",
4880
5612
  "5. For ICU messages (for example {count, plural, one {# item} other {# items}}), keep the ICU",
@@ -5039,6 +5771,26 @@ function buildInstructionsSheet(workbook) {
5039
5771
  }
5040
5772
  sheet.getRow(1).font = { bold: true };
5041
5773
  }
5774
+ var WORKBOOK_PROTECTION_XML = '<workbookProtection lockStructure="1" lockWindows="0"/>';
5775
+ function spliceWorkbookProtection(xml) {
5776
+ const anchor = xml.includes("<bookViews") ? "<bookViews" : "<sheets";
5777
+ return xml.replace(anchor, `${WORKBOOK_PROTECTION_XML}${anchor}`);
5778
+ }
5779
+ async function protectWorkbookStructure(bytes) {
5780
+ let zip;
5781
+ try {
5782
+ zip = await JSZip.loadAsync(bytes);
5783
+ } catch {
5784
+ throw new ExchangeError("WORKBOOK_INVALID", "The workbook could not be serialized.");
5785
+ }
5786
+ const workbookPart = zip.file("xl/workbook.xml");
5787
+ if (workbookPart === null) {
5788
+ return bytes;
5789
+ }
5790
+ const xml = await workbookPart.async("string");
5791
+ zip.file("xl/workbook.xml", spliceWorkbookProtection(xml));
5792
+ return zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
5793
+ }
5042
5794
  async function buildWorkbook(model) {
5043
5795
  assertNoWorksheetNameCollisions(model.sheets);
5044
5796
  const workbook = new ExcelJS.Workbook();
@@ -5046,13 +5798,15 @@ async function buildWorkbook(model) {
5046
5798
  for (const sheet of model.sheets) {
5047
5799
  await buildDataSheet(workbook, sheet);
5048
5800
  }
5801
+ let serialized;
5049
5802
  try {
5050
5803
  const buffer = await workbook.xlsx.writeBuffer();
5051
- const view = buffer;
5052
- return Uint8Array.prototype.slice.call(view);
5804
+ serialized = buffer;
5053
5805
  } catch {
5054
5806
  throw new ExchangeError("WORKBOOK_INVALID", "The workbook could not be serialized.");
5055
5807
  }
5808
+ const protectedBytes = await protectWorkbookStructure(serialized);
5809
+ return Uint8Array.prototype.slice.call(protectedBytes);
5056
5810
  }
5057
5811
  var DEFAULT_WORKBOOK_LIMITS = {
5058
5812
  maxDecompressedBytes: 64 * 1024 * 1024,
@@ -5172,26 +5926,24 @@ function assertHeader(sheet) {
5172
5926
  );
5173
5927
  }
5174
5928
  }
5175
- function parseRow(sheet, row) {
5929
+ var MALFORMED_ROW_COLUMN = "Status";
5930
+ function parseRow(row) {
5176
5931
  const candidate = {
5177
5932
  key: cellString(row.getCell(COLUMN.key)),
5178
5933
  source: cellString(row.getCell(COLUMN.source)),
5179
5934
  currentTarget: cellString(row.getCell(COLUMN.current)),
5180
5935
  status: cellString(row.getCell(COLUMN.status)),
5181
5936
  sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
5182
- translation: cellString(row.getCell(COLUMN.translation)),
5937
+ translation: cellString(row.getCell(COLUMN.translation)).trim(),
5183
5938
  context: cellString(row.getCell(COLUMN.context)),
5184
5939
  reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
5185
5940
  reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
5186
5941
  };
5187
5942
  const result = rowSchema.safeParse(candidate);
5188
5943
  if (!result.success) {
5189
- throw new ExchangeError(
5190
- "WORKBOOK_INVALID",
5191
- `The sheet "${sheet.name}" has a row with a missing key or an unrecognized status.`
5192
- );
5944
+ return { ok: false, column: MALFORMED_ROW_COLUMN };
5193
5945
  }
5194
- return result.data;
5946
+ return { ok: true, row: result.data };
5195
5947
  }
5196
5948
  function readDataSheet(sheet, limits) {
5197
5949
  assertHeader(sheet);
@@ -5202,6 +5954,9 @@ function readDataSheet(sheet, limits) {
5202
5954
  );
5203
5955
  }
5204
5956
  const rows = [];
5957
+ const malformed = [];
5958
+ const duplicates = [];
5959
+ const seenKeys = /* @__PURE__ */ new Set();
5205
5960
  for (let rowNumber = HEADER_ROW + 1; rowNumber <= sheet.rowCount; rowNumber += 1) {
5206
5961
  const row = sheet.getRow(rowNumber);
5207
5962
  if (row.cellCount > limits.maxCellsPerRow) {
@@ -5213,9 +5968,19 @@ function readDataSheet(sheet, limits) {
5213
5968
  if (cellString(row.getCell(COLUMN.key)) === "") {
5214
5969
  continue;
5215
5970
  }
5216
- rows.push(parseRow(sheet, row));
5971
+ const outcome = parseRow(row);
5972
+ if (!outcome.ok) {
5973
+ malformed.push({ locale: sheet.name, row: rowNumber, column: outcome.column });
5974
+ continue;
5975
+ }
5976
+ if (seenKeys.has(outcome.row.key)) {
5977
+ duplicates.push({ locale: sheet.name, key: outcome.row.key, row: rowNumber });
5978
+ continue;
5979
+ }
5980
+ seenKeys.add(outcome.row.key);
5981
+ rows.push(outcome.row);
5217
5982
  }
5218
- return { locale: sheet.name, rows };
5983
+ return { sheet: { locale: sheet.name, rows }, malformed, duplicates };
5219
5984
  }
5220
5985
  async function loadWorkbook(bytes) {
5221
5986
  const workbook = new ExcelJS.Workbook();
@@ -5238,13 +6003,18 @@ async function readWorkbook(bytes, options = {}) {
5238
6003
  );
5239
6004
  }
5240
6005
  const sheets = [];
6006
+ const malformedRows = [];
6007
+ const duplicateKeys = [];
5241
6008
  for (const sheet of workbook.worksheets) {
5242
6009
  if (sheet.name === INSTRUCTIONS_SHEET_NAME) {
5243
6010
  continue;
5244
6011
  }
5245
- sheets.push(readDataSheet(sheet, limits));
6012
+ const read = readDataSheet(sheet, limits);
6013
+ sheets.push(read.sheet);
6014
+ malformedRows.push(...read.malformed);
6015
+ duplicateKeys.push(...read.duplicates);
5246
6016
  }
5247
- return { sheets };
6017
+ return { sheets, malformedRows, duplicateKeys };
5248
6018
  }
5249
6019
 
5250
6020
  // src/flow/workbook/export-workbook.ts
@@ -5345,6 +6115,7 @@ async function exportWorkbook(input, deps = {}) {
5345
6115
  }
5346
6116
 
5347
6117
  // src/flow/workbook/import-locale.ts
6118
+ var CLEAR_SENTINEL = "[[CLEAR]]";
5348
6119
  var UnknownKeyError = class extends Error {
5349
6120
  key;
5350
6121
  constructor(key) {
@@ -5373,9 +6144,20 @@ function trackBlankDrift(row, params, buckets) {
5373
6144
  buckets.blankDrifted.add(row.key);
5374
6145
  }
5375
6146
  }
5376
- function classifyRows(params, buckets) {
6147
+ function classifyClear(row, sourceEntry, buckets) {
6148
+ if (contentHash(sourceEntry) !== row.sourceHash) {
6149
+ buckets.mismatches.push(row.key);
6150
+ buckets.withheld.add(row.key);
6151
+ return;
6152
+ }
6153
+ buckets.accepted.set(row.key, { value: "", source: sourceEntry, cleared: true });
6154
+ }
6155
+ function classifyRows(params, buckets, liveCandidates) {
5377
6156
  for (const row of params.sheet.rows) {
5378
6157
  if (row.translation === "") {
6158
+ if (liveCandidates.has(row.key)) {
6159
+ buckets.unfilled.push(row.key);
6160
+ }
5379
6161
  trackBlankDrift(row, params, buckets);
5380
6162
  continue;
5381
6163
  }
@@ -5386,9 +6168,17 @@ function classifyRows(params, buckets) {
5386
6168
  if (sourceEntry === void 0) {
5387
6169
  continue;
5388
6170
  }
6171
+ if (row.translation === CLEAR_SENTINEL) {
6172
+ classifyClear(row, sourceEntry, buckets);
6173
+ continue;
6174
+ }
5389
6175
  const reason = judge(row, sourceEntry, params.adapter);
5390
6176
  if (reason === void 0) {
5391
- buckets.accepted.set(row.key, { value: row.translation, source: sourceEntry });
6177
+ buckets.accepted.set(row.key, {
6178
+ value: row.translation,
6179
+ source: sourceEntry,
6180
+ cleared: false
6181
+ });
5392
6182
  } else {
5393
6183
  buckets.mismatches.push(row.key);
5394
6184
  buckets.withheld.add(row.key);
@@ -5407,9 +6197,10 @@ function importLocale(params) {
5407
6197
  accepted: /* @__PURE__ */ new Map(),
5408
6198
  mismatches: [],
5409
6199
  withheld: /* @__PURE__ */ new Set(),
5410
- blankDrifted: /* @__PURE__ */ new Set()
6200
+ blankDrifted: /* @__PURE__ */ new Set(),
6201
+ unfilled: []
5411
6202
  };
5412
- classifyRows(params, buckets);
6203
+ classifyRows(params, buckets, /* @__PURE__ */ new Set([...diff2.missing, ...diff2.changed]));
5413
6204
  const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
5414
6205
  const invalidIcuSource = [...new Set(params.sourceInvalidIcuKeys)].filter((key) => rowKeys.has(key)).sort();
5415
6206
  const translated = [...buckets.accepted.keys()].sort();
@@ -5418,6 +6209,7 @@ function importLocale(params) {
5418
6209
  locale: params.sheet.locale,
5419
6210
  status: deriveLocaleStatus({
5420
6211
  translated,
6212
+ cacheHits: [],
5421
6213
  generated: [],
5422
6214
  integrityMismatches,
5423
6215
  providerFailures: [],
@@ -5428,12 +6220,16 @@ function importLocale(params) {
5428
6220
  orphaned: diff2.orphaned,
5429
6221
  pruned: [],
5430
6222
  invalidIcuSource,
6223
+ cacheHits: [],
5431
6224
  integrityMismatches,
5432
6225
  providerFailures: [],
5433
6226
  budgetWithheld: [],
5434
6227
  generated: [],
5435
6228
  notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
5436
- needsReview: []
6229
+ needsReview: [],
6230
+ unfilled: [...new Set(buckets.unfilled)].sort(),
6231
+ malformedRows: params.malformedRows,
6232
+ duplicateKeys: params.duplicateKeys
5437
6233
  };
5438
6234
  return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
5439
6235
  }
@@ -5460,6 +6256,21 @@ function mergeAccepted(target, accepted) {
5460
6256
  }
5461
6257
  return merged;
5462
6258
  }
6259
+ function sheetCacheAdditions(accepted) {
6260
+ const record = {};
6261
+ for (const [, { value, source, cleared }] of accepted) {
6262
+ if (!cleared) {
6263
+ record[contentHash(source)] = value;
6264
+ }
6265
+ }
6266
+ return record;
6267
+ }
6268
+ function collectSheetAdditions(byLocale, locale, additions) {
6269
+ if (Object.keys(additions).length === 0) {
6270
+ return;
6271
+ }
6272
+ byLocale.set(locale, { ...byLocale.get(locale), ...additions });
6273
+ }
5463
6274
  function computeLockEntries2(source, merged, baseline, accepted) {
5464
6275
  const entries = {};
5465
6276
  for (const key of merged.keys()) {
@@ -5476,11 +6287,20 @@ function computeLockEntries2(source, merged, baseline, accepted) {
5476
6287
  }
5477
6288
  return entries;
5478
6289
  }
6290
+ var MissingSheetError = class extends Error {
6291
+ code = "WORKBOOK_SHEET_MISSING";
6292
+ constructor(locale) {
6293
+ super(
6294
+ `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.`
6295
+ );
6296
+ this.name = "MissingSheetError";
6297
+ }
6298
+ };
5479
6299
  async function runSheet(ctx, sheet, lock) {
5480
6300
  if (!ctx.config.targetLocales.includes(sheet.locale)) {
5481
6301
  throw new SdkError(
5482
6302
  "CONFIG_INVALID",
5483
- `The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
6303
+ `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.`
5484
6304
  );
5485
6305
  }
5486
6306
  const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
@@ -5491,10 +6311,12 @@ async function runSheet(ctx, sheet, lock) {
5491
6311
  target,
5492
6312
  baseline,
5493
6313
  adapter: ctx.adapter,
5494
- sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys
6314
+ sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys,
6315
+ malformedRows: ctx.malformedRows.filter((problem) => problem.locale === sheet.locale).map((problem) => ({ row: problem.row, column: problem.column })),
6316
+ duplicateKeys: ctx.duplicateKeys.filter((duplicate) => duplicate.locale === sheet.locale).map((duplicate) => ({ key: duplicate.key, row: duplicate.row }))
5495
6317
  });
5496
6318
  if (ctx.dryRun) {
5497
- return { summary, lockEntries: {} };
6319
+ return { summary, lockEntries: {}, cacheAdditions: {} };
5498
6320
  }
5499
6321
  const merged = mergeAccepted(target, accepted);
5500
6322
  if (accepted.size > 0) {
@@ -5509,7 +6331,11 @@ async function runSheet(ctx, sheet, lock) {
5509
6331
  path
5510
6332
  );
5511
6333
  }
5512
- return { summary, lockEntries: computeLockEntries2(ctx.source, merged, baseline, accepted) };
6334
+ return {
6335
+ summary,
6336
+ lockEntries: computeLockEntries2(ctx.source, merged, baseline, accepted),
6337
+ cacheAdditions: sheetCacheAdditions(accepted)
6338
+ };
5513
6339
  }
5514
6340
  async function importWorkbook(input, deps = {}) {
5515
6341
  const config = input.config;
@@ -5534,9 +6360,12 @@ async function importWorkbook(input, deps = {}) {
5534
6360
  fs,
5535
6361
  source: source.resource,
5536
6362
  sourceInvalidIcuKeys: source.invalidIcuKeys,
5537
- dryRun
6363
+ dryRun,
6364
+ malformedRows: data.malformedRows,
6365
+ duplicateKeys: data.duplicateKeys
5538
6366
  };
5539
6367
  const summaries = [];
6368
+ const cacheAdditions = /* @__PURE__ */ new Map();
5540
6369
  for (const sheet of data.sheets) {
5541
6370
  try {
5542
6371
  let summary;
@@ -5549,6 +6378,7 @@ async function importWorkbook(input, deps = {}) {
5549
6378
  mode: "replace",
5550
6379
  entries: result.lockEntries
5551
6380
  });
6381
+ collectSheetAdditions(cacheAdditions, sheet.locale, result.cacheAdditions);
5552
6382
  return result.summary;
5553
6383
  });
5554
6384
  }
@@ -5557,6 +6387,15 @@ async function importWorkbook(input, deps = {}) {
5557
6387
  summaries.push(failureSummary(sheet.locale, error));
5558
6388
  }
5559
6389
  }
6390
+ const presentLocales = new Set(data.sheets.map((sheet) => sheet.locale));
6391
+ for (const locale of config.targetLocales) {
6392
+ if (!presentLocales.has(locale)) {
6393
+ summaries.push(failureSummary(locale, new MissingSheetError(locale)));
6394
+ }
6395
+ }
6396
+ if (!dryRun) {
6397
+ await feedTranslationMemory(cwd, fs, computeFingerprint(config), cacheAdditions);
6398
+ }
5560
6399
  const { succeeded, partial, failed } = partition(summaries);
5561
6400
  return { dryRun, locales: summaries, succeeded, partial, failed };
5562
6401
  }
@@ -5608,6 +6447,7 @@ async function watch(input, deps = {}) {
5608
6447
  const cwd = input.cwd ?? process.cwd();
5609
6448
  const debounceMs = input.debounceMs ?? DEFAULT_DEBOUNCE_MS;
5610
6449
  const fs = deps.fs ?? defaultFs;
6450
+ resolveRunConcurrency(input.concurrency, false, input.config);
5611
6451
  const sourcePath = localeFilePath(cwd, input.config.files.pattern, input.config.sourceLocale);
5612
6452
  if (!await fs.fileExists(sourcePath)) {
5613
6453
  throw new SdkError(
@@ -5620,7 +6460,10 @@ async function watch(input, deps = {}) {
5620
6460
  config: input.config,
5621
6461
  cwd,
5622
6462
  ...input.onLockWait !== void 0 ? { onLockWait: input.onLockWait } : {},
5623
- ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {}
6463
+ ...input.onProgress !== void 0 ? { onProgress: input.onProgress } : {},
6464
+ ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {},
6465
+ ...input.concurrency !== void 0 ? { concurrency: input.concurrency } : {},
6466
+ ...input.cache !== void 0 ? { cache: input.cache } : {}
5624
6467
  };
5625
6468
  let state = "idle";
5626
6469
  let pending = false;
@@ -5688,6 +6531,6 @@ async function watch(input, deps = {}) {
5688
6531
  return { stop };
5689
6532
  }
5690
6533
 
5691
- export { DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
6534
+ export { CACHE_FILE_NAME, DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
5692
6535
  //# sourceMappingURL=index.js.map
5693
6536
  //# sourceMappingURL=index.js.map