@verbatra/sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -7,16 +7,20 @@ var cosmiconfigTypescriptLoader = require('cosmiconfig-typescript-loader');
7
7
  var zod = require('zod');
8
8
  var Anthropic = require('@anthropic-ai/sdk');
9
9
  var deepl = require('deepl-node');
10
+ var module$1 = require('module');
10
11
  var log = require('loglevel');
11
12
  var genai = require('@google/genai');
12
13
  var OpenAI = require('openai');
13
14
  var crypto = require('crypto');
14
15
  var promises = require('fs/promises');
15
16
  var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
17
+ var xmldom = require('@xmldom/xmldom');
18
+ var yaml = require('yaml');
16
19
  var ExcelJS = require('exceljs');
17
20
  var JSZip = require('jszip');
18
21
  var chokidar = require('chokidar');
19
22
 
23
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
20
24
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
25
 
22
26
  function _interopNamespace(e) {
@@ -129,7 +133,10 @@ var SUPPORTED_FORMATS = [
129
133
  "i18next-json",
130
134
  "vue-i18n-json",
131
135
  "next-intl-json",
132
- "ngx-translate-json"
136
+ "ngx-translate-json",
137
+ "xliff",
138
+ "yaml",
139
+ "arb"
133
140
  ];
134
141
  var supportedFormatSchema = zod.z.enum(SUPPORTED_FORMATS);
135
142
  var translationEntrySchema = zod.z.object({
@@ -184,6 +191,26 @@ var LOCALE_TOKEN = "{locale}";
184
191
  function localeFilePath(cwd, pattern, locale) {
185
192
  return path.resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
186
193
  }
194
+ var REDACTED = "[REDACTED]";
195
+ var KEY_PATTERNS = [
196
+ // The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
197
+ /\bsk-[A-Za-z0-9_-]{8,}/g,
198
+ /AIza[0-9A-Za-z_-]{35}/g,
199
+ /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(?::fx)?/g
200
+ ];
201
+ function escapeForRegExp(value) {
202
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
203
+ }
204
+ function redact(text, secret = process.env.ANTHROPIC_API_KEY) {
205
+ let out = text;
206
+ for (const pattern of KEY_PATTERNS) {
207
+ out = out.replace(pattern, REDACTED);
208
+ }
209
+ if (secret !== void 0 && secret.length > 0) {
210
+ out = out.replace(new RegExp(escapeForRegExp(secret), "g"), REDACTED);
211
+ }
212
+ return out;
213
+ }
187
214
  var ProviderError = class extends Error {
188
215
  /** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
189
216
  code;
@@ -192,7 +219,7 @@ var ProviderError = class extends Error {
192
219
  * @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
193
220
  */
194
221
  constructor(code, message) {
195
- super(message);
222
+ super(redact(message, ""));
196
223
  this.name = "ProviderError";
197
224
  this.code = code;
198
225
  }
@@ -319,6 +346,12 @@ function assertNotTruncated(truncated) {
319
346
  throw new ProviderError("OUTPUT_TRUNCATED", OUTPUT_TRUNCATED_MESSAGE);
320
347
  }
321
348
  }
349
+ var PROVIDER_ENV = {
350
+ anthropic: "ANTHROPIC_API_KEY",
351
+ openai: "OPENAI_API_KEY",
352
+ gemini: "GEMINI_API_KEY",
353
+ deepl: "DEEPL_API_KEY"
354
+ };
322
355
  function readRequiredEnv(name) {
323
356
  const value = process.env[name];
324
357
  if (value === void 0 || value.length === 0) {
@@ -327,16 +360,16 @@ function readRequiredEnv(name) {
327
360
  return value;
328
361
  }
329
362
  function requireAnthropicKey() {
330
- return readRequiredEnv("ANTHROPIC_API_KEY");
363
+ return readRequiredEnv(PROVIDER_ENV.anthropic);
331
364
  }
332
365
  function requireOpenAiKey() {
333
- return readRequiredEnv("OPENAI_API_KEY");
366
+ return readRequiredEnv(PROVIDER_ENV.openai);
334
367
  }
335
368
  function requireGeminiKey() {
336
- return readRequiredEnv("GEMINI_API_KEY");
369
+ return readRequiredEnv(PROVIDER_ENV.gemini);
337
370
  }
338
371
  function requireDeepLKey() {
339
- return readRequiredEnv("DEEPL_API_KEY");
372
+ return readRequiredEnv(PROVIDER_ENV.deepl);
340
373
  }
341
374
  function createDefaultClient() {
342
375
  const sdk = new Anthropic__default.default({ apiKey: requireAnthropicKey(), logLevel: "off" });
@@ -437,8 +470,22 @@ function toUsage(usage) {
437
470
  var deepLConfigSchema = zod.z.object({
438
471
  glossaryId: zod.z.string().min(1).optional()
439
472
  });
473
+ var DEEPL_LOGGER = "deepl";
474
+ function resolveDeeplLoglevel(requireFn = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
475
+ try {
476
+ const entry = requireFn.resolve("deepl-node");
477
+ return module$1.createRequire(entry)("loglevel");
478
+ } catch {
479
+ return void 0;
480
+ }
481
+ }
482
+ function silenceDeeplLogger(instances) {
483
+ for (const instance of instances) {
484
+ instance?.getLogger(DEEPL_LOGGER).setLevel("silent");
485
+ }
486
+ }
440
487
  function silenceSdkLogging() {
441
- log__default.default.getLogger("deepl").setLevel("silent");
488
+ silenceDeeplLogger([log__default.default, resolveDeeplLoglevel()]);
442
489
  }
443
490
  function createDefaultClient2() {
444
491
  silenceSdkLogging();
@@ -799,6 +846,11 @@ function createMechanism3(client, config) {
799
846
  function callClient4(client, body) {
800
847
  return guardProviderCall(() => client.chat.completions.create(body));
801
848
  }
849
+ var SCAFFOLD_MODELS = {
850
+ anthropic: "claude-sonnet-4-6",
851
+ openai: "gpt-5.4-mini",
852
+ gemini: "gemini-2.5-flash"
853
+ };
802
854
  var providerConfigSchema = zod.z.discriminatedUnion("id", [
803
855
  zod.z.object({ id: zod.z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
804
856
  zod.z.object({ id: zod.z.literal("openai"), options: openAiConfigSchema.strict() }),
@@ -829,27 +881,25 @@ var verbatraConfigSchema = zod.z.strictObject({
829
881
  glossary: zod.z.record(zod.z.string(), zod.z.string()).optional(),
830
882
  tone: zod.z.enum(["formal", "informal", "neutral"]).optional(),
831
883
  /**
832
- * Opt-in orphan pruning, off by default (absent is treated as false). When true, keys present in a
833
- * target file but absent from the source (the diff's orphaned keys) are removed from the written file
834
- * and the lock. A per-run `prune` option on `translate` (the CLI `--prune` flag) overrides this. This
835
- * is non-secret, consistent with the config's no-secret invariant.
884
+ * Opt-in orphan pruning, off by default. When true, keys present in a target file but absent from
885
+ * the source are removed from the written file and the lock. The per-run `prune` option on
886
+ * `translate` (the CLI `--prune` flag) overrides this.
836
887
  */
837
888
  prune: zod.z.boolean().optional(),
838
889
  /**
839
- * Opt-in plural-category generation, off by default (absent is treated as false). When true, and only
840
- * for an i18next-JSON project translated by an LLM provider, verbatra synthesizes the CLDR plural forms
841
- * a target language requires but the source does not supply (for example Polish few/many). A per-run
890
+ * Opt-in plural-category generation, off by default. When true, and only for an i18next-JSON project
891
+ * translated by an LLM provider, verbatra synthesizes the CLDR plural forms a target language
892
+ * requires but the source does not supply (for example Polish few/many). The per-run
842
893
  * `generatePlurals` option on `translate` overrides this. Unsupported cases (DeepL, non-i18next, an
843
894
  * unknown language) fall back to the per-locale plural warning.
844
895
  */
845
896
  generatePlurals: zod.z.boolean().optional(),
846
897
  /**
847
- * Optional maximum number of entries sent in a single provider request. A locale's missing-plus-changed
848
- * entries are split into sequential sub-batches no larger than this so one oversized request cannot sink
849
- * the whole locale; a failed sub-batch is withheld and retried while the others still make progress.
850
- * Must be a positive integer (non-integer, zero, or negative is rejected at this boundary, never
851
- * coerced). When absent, {@link DEFAULT_MAX_BATCH_SIZE} applies: 50, a conservative count that stays
852
- * well inside provider context windows for typical short i18n strings while keeping request counts low.
898
+ * Optional maximum number of entries sent in a single provider request. A locale's missing and
899
+ * changed entries are split into sequential sub-batches no larger than this, so one oversized request
900
+ * cannot sink the whole locale; a failed sub-batch is withheld while the others make progress. Must
901
+ * be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
902
+ * {@link DEFAULT_MAX_BATCH_SIZE} applies.
853
903
  */
854
904
  maxBatchSize: zod.z.number().int().positive().optional()
855
905
  }).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
@@ -1081,6 +1131,85 @@ async function writeLockFile(path, lock, fs) {
1081
1131
  await fs.writeFile(path, `${JSON.stringify(ordered, null, 2)}
1082
1132
  `);
1083
1133
  }
1134
+ var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
1135
+ var INVALID = { placeholders: [], isPlural: false, valid: false };
1136
+ function tokenOf(element) {
1137
+ switch (element.type) {
1138
+ case icuMessageformatParser.TYPE.argument:
1139
+ case icuMessageformatParser.TYPE.number:
1140
+ case icuMessageformatParser.TYPE.date:
1141
+ case icuMessageformatParser.TYPE.time:
1142
+ case icuMessageformatParser.TYPE.select:
1143
+ case icuMessageformatParser.TYPE.plural:
1144
+ return `{${element.value}}`;
1145
+ case icuMessageformatParser.TYPE.tag:
1146
+ return `<${element.value}>`;
1147
+ default:
1148
+ return void 0;
1149
+ }
1150
+ }
1151
+ function childMessages(element) {
1152
+ if (element.type === icuMessageformatParser.TYPE.plural || element.type === icuMessageformatParser.TYPE.select) {
1153
+ return Object.values(element.options).map((option) => option.value);
1154
+ }
1155
+ if (element.type === icuMessageformatParser.TYPE.tag) {
1156
+ return [element.children];
1157
+ }
1158
+ return [];
1159
+ }
1160
+ function collect(elements, add, state) {
1161
+ for (const element of elements) {
1162
+ const token = tokenOf(element);
1163
+ if (token !== void 0) {
1164
+ add(token);
1165
+ }
1166
+ if (element.type === icuMessageformatParser.TYPE.plural) {
1167
+ state.isPlural = true;
1168
+ }
1169
+ for (const child of childMessages(element)) {
1170
+ collect(child, add, state);
1171
+ }
1172
+ }
1173
+ }
1174
+ function analyzeIcuValue(value) {
1175
+ if (!value.includes("{") && !value.includes("<")) {
1176
+ return VALID_EMPTY;
1177
+ }
1178
+ try {
1179
+ const ast = icuMessageformatParser.parse(value);
1180
+ const placeholders = [];
1181
+ const state = { isPlural: false };
1182
+ collect(
1183
+ ast,
1184
+ (token) => {
1185
+ placeholders.push(token);
1186
+ },
1187
+ state
1188
+ );
1189
+ return { placeholders, isPlural: state.isPlural, valid: true };
1190
+ } catch {
1191
+ return INVALID;
1192
+ }
1193
+ }
1194
+ function icuPlaceholders(value) {
1195
+ return analyzeIcuValue(value).placeholders;
1196
+ }
1197
+ function icuIsValid(value) {
1198
+ return analyzeIcuValue(value).valid;
1199
+ }
1200
+ function icuDeriveEntry(_key, value) {
1201
+ const analysis = analyzeIcuValue(value);
1202
+ return { placeholders: analysis.placeholders, isPlural: analysis.isPlural };
1203
+ }
1204
+ function icuInvalidKeys(entries) {
1205
+ const invalid = [];
1206
+ for (const [key, entry] of entries) {
1207
+ if (!icuIsValid(entry.value)) {
1208
+ invalid.push(key);
1209
+ }
1210
+ }
1211
+ return invalid;
1212
+ }
1084
1213
  var AdapterError = class extends Error {
1085
1214
  code;
1086
1215
  constructor(code, message) {
@@ -1089,6 +1218,82 @@ var AdapterError = class extends Error {
1089
1218
  this.code = code;
1090
1219
  }
1091
1220
  };
1221
+ var MAX_DEPTH = 100;
1222
+ var MAX_INPUT_BYTES = 16 * 1024 * 1024;
1223
+ var jsonTreeSchema = zod.z.lazy(
1224
+ () => zod.z.union([zod.z.string(), zod.z.record(zod.z.string(), jsonTreeSchema)])
1225
+ );
1226
+ var rootSchema = zod.z.record(zod.z.string(), jsonTreeSchema);
1227
+ function assertWithinDepth(value, max) {
1228
+ const stack = [{ node: value, depth: 1 }];
1229
+ while (stack.length > 0) {
1230
+ const top = stack.pop();
1231
+ if (top === void 0) {
1232
+ break;
1233
+ }
1234
+ const { node, depth } = top;
1235
+ if (typeof node !== "object" || node === null) {
1236
+ continue;
1237
+ }
1238
+ if (depth > max) {
1239
+ throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
1240
+ }
1241
+ for (const child of Object.values(node)) {
1242
+ stack.push({ node: child, depth: depth + 1 });
1243
+ }
1244
+ }
1245
+ }
1246
+ function assertJsonRecord(value) {
1247
+ assertWithinDepth(value, MAX_DEPTH);
1248
+ const result = rootSchema.safeParse(value);
1249
+ if (!result.success) {
1250
+ throw new AdapterError(
1251
+ "INVALID_STRUCTURE",
1252
+ "The file is not a valid object (expected nested objects of string values)."
1253
+ );
1254
+ }
1255
+ return result.data;
1256
+ }
1257
+ function parseJsonObject(content) {
1258
+ let parsed;
1259
+ try {
1260
+ parsed = JSON.parse(content);
1261
+ } catch {
1262
+ throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
1263
+ }
1264
+ return assertJsonRecord(parsed);
1265
+ }
1266
+ function serializeJsonTree(tree) {
1267
+ return `${JSON.stringify(tree, null, 2)}
1268
+ `;
1269
+ }
1270
+ function namespaceOf(filePath) {
1271
+ return path.basename(filePath, path.extname(filePath));
1272
+ }
1273
+ function rethrowStructured(error, message) {
1274
+ if (error instanceof AdapterError) {
1275
+ throw error;
1276
+ }
1277
+ throw new AdapterError("INVALID_STRUCTURE", message);
1278
+ }
1279
+ function computeIcu(entries, compute) {
1280
+ if (!compute) {
1281
+ return [];
1282
+ }
1283
+ try {
1284
+ return compute(entries);
1285
+ } catch (error) {
1286
+ rethrowStructured(error, "The file could not be analyzed for message validity.");
1287
+ }
1288
+ }
1289
+ function buildCanHandle(extensions, sniff) {
1290
+ return (filePath, sample) => {
1291
+ if (!extensions.includes(path.extname(filePath).toLowerCase())) {
1292
+ return false;
1293
+ }
1294
+ return sample === void 0 || sniff === void 0 || sniff(sample);
1295
+ };
1296
+ }
1092
1297
  var nodeOps = {
1093
1298
  writeFile: (path, data) => promises.writeFile(path, data, "utf8"),
1094
1299
  rename: (from, to) => promises.rename(from, to),
@@ -1113,8 +1318,6 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
1113
1318
  throw error;
1114
1319
  }
1115
1320
  }
1116
- var MAX_DEPTH = 100;
1117
- var MAX_INPUT_BYTES = 16 * 1024 * 1024;
1118
1321
  async function readBoundedUtf82(handle, size) {
1119
1322
  const buffer = Buffer.allocUnsafe(size);
1120
1323
  let offset = 0;
@@ -1142,6 +1345,16 @@ async function readBounded2(filePath) {
1142
1345
  await handle.close();
1143
1346
  }
1144
1347
  }
1348
+ async function readFileContent(filePath) {
1349
+ const outcome = await readBounded2(filePath);
1350
+ if (outcome.kind === "not-a-file") {
1351
+ throw new AdapterError("INVALID_STRUCTURE", "The path is not a regular file.");
1352
+ }
1353
+ if (outcome.kind === "too-large") {
1354
+ throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
1355
+ }
1356
+ return outcome.content;
1357
+ }
1145
1358
  var BACKSLASH = "\\";
1146
1359
  var DOT = ".";
1147
1360
  var ESCAPED_BACKSLASH = "\\\\";
@@ -1255,46 +1468,6 @@ function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
1255
1468
  addEntries({ namespace, derive, out, claimed: /* @__PURE__ */ new Map() }, [], tree);
1256
1469
  return out;
1257
1470
  }
1258
- var jsonTreeSchema = zod.z.lazy(
1259
- () => zod.z.union([zod.z.string(), zod.z.record(zod.z.string(), jsonTreeSchema)])
1260
- );
1261
- var rootSchema = zod.z.record(zod.z.string(), jsonTreeSchema);
1262
- function assertWithinDepth(value, max) {
1263
- const stack = [{ node: value, depth: 1 }];
1264
- while (stack.length > 0) {
1265
- const top = stack.pop();
1266
- if (top === void 0) {
1267
- break;
1268
- }
1269
- const { node, depth } = top;
1270
- if (typeof node !== "object" || node === null) {
1271
- continue;
1272
- }
1273
- if (depth > max) {
1274
- throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
1275
- }
1276
- for (const child of Object.values(node)) {
1277
- stack.push({ node: child, depth: depth + 1 });
1278
- }
1279
- }
1280
- }
1281
- function parseJsonObject(content) {
1282
- let parsed;
1283
- try {
1284
- parsed = JSON.parse(content);
1285
- } catch {
1286
- throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
1287
- }
1288
- assertWithinDepth(parsed, MAX_DEPTH);
1289
- const result = rootSchema.safeParse(parsed);
1290
- if (!result.success) {
1291
- throw new AdapterError(
1292
- "INVALID_STRUCTURE",
1293
- "The file is not a valid JSON object (expected nested objects of string values)."
1294
- );
1295
- }
1296
- return result.data;
1297
- }
1298
1471
  function emptyNode() {
1299
1472
  return /* @__PURE__ */ Object.create(null);
1300
1473
  }
@@ -1331,77 +1504,146 @@ function unflattenEntries(entries) {
1331
1504
  }
1332
1505
  return root;
1333
1506
  }
1334
- function namespaceOf(filePath) {
1335
- return path.basename(filePath, path.extname(filePath));
1336
- }
1337
- function canHandle(filePath, sample) {
1338
- if (path.extname(filePath).toLowerCase() !== ".json") {
1339
- return false;
1340
- }
1341
- return sample === void 0 || sample.trimStart().startsWith("{");
1342
- }
1343
- function rethrowStructured(error, message) {
1344
- if (error instanceof AdapterError) {
1345
- throw error;
1346
- }
1347
- throw new AdapterError("INVALID_STRUCTURE", message);
1348
- }
1349
- function toEntries(content, namespace, deriveEntry, keyMode, validateTree) {
1507
+ function toEntries(content, namespace, parse2, deriveEntry, keyMode, validateTree) {
1350
1508
  try {
1351
- const tree = parseJsonObject(content);
1509
+ const tree = parse2(content);
1352
1510
  validateTree?.(tree);
1353
1511
  return flattenTree(tree, namespace, deriveEntry, keyMode);
1354
1512
  } catch (error) {
1355
- rethrowStructured(error, "The file could not be read as JSON.");
1513
+ rethrowStructured(error, "The file could not be parsed.");
1356
1514
  }
1357
1515
  }
1358
- function computeIcu(entries, compute) {
1359
- if (!compute) {
1360
- return [];
1361
- }
1362
- try {
1363
- return compute(entries);
1364
- } catch (error) {
1365
- rethrowStructured(error, "The file could not be analyzed for message validity.");
1366
- }
1367
- }
1368
- function createJsonFileAdapter(options) {
1516
+ function createTreeFileAdapter(options) {
1369
1517
  const {
1370
1518
  format,
1519
+ extensions,
1520
+ sniff,
1521
+ parse: parse2,
1522
+ serialize,
1371
1523
  deriveEntry,
1372
- extractPlaceholders: extractPlaceholders2,
1373
- computeInvalidIcuKeys: computeInvalidIcuKeys2,
1374
- validateMessage: validateMessage2,
1524
+ extractPlaceholders,
1525
+ computeInvalidIcuKeys,
1526
+ validateMessage,
1375
1527
  validateTree,
1376
1528
  buildWriteTree,
1377
1529
  keyMode = "literal-leaf"
1378
1530
  } = options;
1379
1531
  return {
1380
1532
  format,
1381
- canHandle,
1382
- extractPlaceholders: extractPlaceholders2,
1383
- // Non-ICU formats supply no validator: every value is valid for their syntax.
1384
- validateMessage: validateMessage2 ?? (() => true),
1533
+ canHandle: buildCanHandle(extensions, sniff),
1534
+ extractPlaceholders,
1535
+ validateMessage: validateMessage ?? (() => true),
1385
1536
  async read(filePath, locale) {
1386
- const outcome = await readBounded2(filePath);
1387
- if (outcome.kind === "not-a-file") {
1388
- throw new AdapterError("INVALID_STRUCTURE", "The path is not a regular file.");
1389
- }
1390
- if (outcome.kind === "too-large") {
1391
- throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
1392
- }
1537
+ const content = await readFileContent(filePath);
1393
1538
  const namespace = namespaceOf(filePath);
1394
- const entries = toEntries(outcome.content, namespace, deriveEntry, keyMode, validateTree);
1539
+ const entries = toEntries(content, namespace, parse2, deriveEntry, keyMode, validateTree);
1395
1540
  const resource = { locale, namespace, format, entries };
1396
- const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys2);
1541
+ const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
1397
1542
  return { resource, invalidIcuKeys };
1398
1543
  },
1399
1544
  async write(resource, filePath) {
1400
1545
  const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
1401
- await atomicWriteFile(filePath, `${JSON.stringify(tree, null, 2)}
1402
- `);
1546
+ await atomicWriteFile(filePath, serialize(tree));
1547
+ }
1548
+ };
1549
+ }
1550
+ function isMetadataKey(key) {
1551
+ return key.startsWith("@");
1552
+ }
1553
+ function parseArbObject(content) {
1554
+ let parsed;
1555
+ try {
1556
+ parsed = JSON.parse(content);
1557
+ } catch {
1558
+ throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
1559
+ }
1560
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1561
+ throw new AdapterError(
1562
+ "INVALID_STRUCTURE",
1563
+ "The file is not a valid object (expected nested objects of string values)."
1564
+ );
1565
+ }
1566
+ return parsed;
1567
+ }
1568
+ function stripArbMetadata(tree) {
1569
+ const out = /* @__PURE__ */ Object.create(null);
1570
+ for (const [key, value] of Object.entries(tree)) {
1571
+ if (!isMetadataKey(key)) {
1572
+ out[key] = value;
1573
+ }
1574
+ }
1575
+ return out;
1576
+ }
1577
+ function originalKey(encoded) {
1578
+ return decodeKeyToSegments(encoded).join(".");
1579
+ }
1580
+ function messagesFromEntries(entries) {
1581
+ const out = /* @__PURE__ */ new Map();
1582
+ for (const [key, entry] of entries) {
1583
+ out.set(originalKey(key), entry.value);
1584
+ }
1585
+ return out;
1586
+ }
1587
+ async function readDestinationPairs(filePath) {
1588
+ let parsed;
1589
+ try {
1590
+ const outcome = await readBounded2(filePath);
1591
+ if (outcome.kind !== "ok") {
1592
+ return null;
1593
+ }
1594
+ parsed = JSON.parse(outcome.content);
1595
+ } catch {
1596
+ return null;
1597
+ }
1598
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1599
+ return null;
1600
+ }
1601
+ return Object.entries(parsed);
1602
+ }
1603
+ async function buildArbWriteTree(entries, filePath) {
1604
+ const messages = messagesFromEntries(entries);
1605
+ const pairs = await readDestinationPairs(filePath);
1606
+ const out = /* @__PURE__ */ Object.create(null);
1607
+ const consumed = /* @__PURE__ */ new Set();
1608
+ for (const [key, value] of pairs ?? []) {
1609
+ const translated = isMetadataKey(key) ? void 0 : messages.get(key);
1610
+ if (translated !== void 0) {
1611
+ consumed.add(key);
1403
1612
  }
1404
- };
1613
+ out[key] = translated ?? value;
1614
+ }
1615
+ for (const [key, value] of messages) {
1616
+ if (!consumed.has(key)) {
1617
+ out[key] = value;
1618
+ }
1619
+ }
1620
+ return out;
1621
+ }
1622
+ function parseArb(content) {
1623
+ return assertJsonRecord(stripArbMetadata(parseArbObject(content)));
1624
+ }
1625
+ function createArbAdapter() {
1626
+ return createTreeFileAdapter({
1627
+ format: "arb",
1628
+ extensions: [".arb"],
1629
+ sniff: (sample) => sample.trimStart().startsWith("{"),
1630
+ parse: parseArb,
1631
+ serialize: serializeJsonTree,
1632
+ extractPlaceholders: icuPlaceholders,
1633
+ deriveEntry: icuDeriveEntry,
1634
+ computeInvalidIcuKeys: icuInvalidKeys,
1635
+ validateMessage: icuIsValid,
1636
+ buildWriteTree: buildArbWriteTree
1637
+ });
1638
+ }
1639
+ function createJsonFileAdapter(options) {
1640
+ return createTreeFileAdapter({
1641
+ ...options,
1642
+ extensions: [".json"],
1643
+ sniff: (sample) => sample.trimStart().startsWith("{"),
1644
+ parse: parseJsonObject,
1645
+ serialize: serializeJsonTree
1646
+ });
1405
1647
  }
1406
1648
  var DOUBLE_BRACE_PATTERN = /\{\{[^{}]*\}\}/g;
1407
1649
  var I18NEXT_PATTERN = /\{\{[^{}]*\}\}|\$t\([^()]*\)/g;
@@ -1448,91 +1690,13 @@ function createI18nextJsonAdapter() {
1448
1690
  })
1449
1691
  });
1450
1692
  }
1451
- var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
1452
- var INVALID = { placeholders: [], isPlural: false, valid: false };
1453
- function tokenOf(element) {
1454
- switch (element.type) {
1455
- case icuMessageformatParser.TYPE.argument:
1456
- case icuMessageformatParser.TYPE.number:
1457
- case icuMessageformatParser.TYPE.date:
1458
- case icuMessageformatParser.TYPE.time:
1459
- case icuMessageformatParser.TYPE.select:
1460
- case icuMessageformatParser.TYPE.plural:
1461
- return `{${element.value}}`;
1462
- case icuMessageformatParser.TYPE.tag:
1463
- return `<${element.value}>`;
1464
- default:
1465
- return void 0;
1466
- }
1467
- }
1468
- function childMessages(element) {
1469
- if (element.type === icuMessageformatParser.TYPE.plural || element.type === icuMessageformatParser.TYPE.select) {
1470
- return Object.values(element.options).map((option) => option.value);
1471
- }
1472
- if (element.type === icuMessageformatParser.TYPE.tag) {
1473
- return [element.children];
1474
- }
1475
- return [];
1476
- }
1477
- function collect(elements, add, state) {
1478
- for (const element of elements) {
1479
- const token = tokenOf(element);
1480
- if (token !== void 0) {
1481
- add(token);
1482
- }
1483
- if (element.type === icuMessageformatParser.TYPE.plural) {
1484
- state.isPlural = true;
1485
- }
1486
- for (const child of childMessages(element)) {
1487
- collect(child, add, state);
1488
- }
1489
- }
1490
- }
1491
- function analyzeIcuValue(value) {
1492
- if (!value.includes("{") && !value.includes("<")) {
1493
- return VALID_EMPTY;
1494
- }
1495
- try {
1496
- const ast = icuMessageformatParser.parse(value);
1497
- const placeholders = [];
1498
- const state = { isPlural: false };
1499
- collect(
1500
- ast,
1501
- (token) => {
1502
- placeholders.push(token);
1503
- },
1504
- state
1505
- );
1506
- return { placeholders, isPlural: state.isPlural, valid: true };
1507
- } catch {
1508
- return INVALID;
1509
- }
1510
- }
1511
- function extractPlaceholders(value) {
1512
- return analyzeIcuValue(value).placeholders;
1513
- }
1514
- function validateMessage(value) {
1515
- return analyzeIcuValue(value).valid;
1516
- }
1517
- function computeInvalidIcuKeys(entries) {
1518
- const invalid = [];
1519
- for (const [key, entry] of entries) {
1520
- if (!validateMessage(entry.value)) {
1521
- invalid.push(key);
1522
- }
1523
- }
1524
- return invalid;
1525
- }
1526
1693
  function createNextIntlJsonAdapter() {
1527
1694
  return createJsonFileAdapter({
1528
1695
  format: "next-intl-json",
1529
- extractPlaceholders,
1530
- deriveEntry: (_key, value) => {
1531
- const analysis = analyzeIcuValue(value);
1532
- return { placeholders: analysis.placeholders, isPlural: analysis.isPlural };
1533
- },
1534
- computeInvalidIcuKeys,
1535
- validateMessage
1696
+ extractPlaceholders: icuPlaceholders,
1697
+ deriveEntry: icuDeriveEntry,
1698
+ computeInvalidIcuKeys: icuInvalidKeys,
1699
+ validateMessage: icuIsValid
1536
1700
  });
1537
1701
  }
1538
1702
  function assertNotMixed(tree) {
@@ -1594,8 +1758,7 @@ function createNgxTranslateJsonAdapter() {
1594
1758
  }),
1595
1759
  validateTree: assertNotMixed,
1596
1760
  buildWriteTree: buildNgxWriteTree,
1597
- // ngx-translate flat style uses dotted keys as path notation, not literal leaves;
1598
- // keep the legacy non-encoding flatten so its flat/nested round-trip is unchanged.
1761
+ // ngx-translate flat style uses dotted keys as path notation, not literal leaves.
1599
1762
  keyMode: "path-notation"
1600
1763
  });
1601
1764
  }
@@ -1671,8 +1834,245 @@ function createVueI18nJsonAdapter() {
1671
1834
  })
1672
1835
  });
1673
1836
  }
1837
+ function toEntries2(content, namespace, parseEntries) {
1838
+ try {
1839
+ return parseEntries(content, namespace);
1840
+ } catch (error) {
1841
+ rethrowStructured(error, "The file could not be parsed.");
1842
+ }
1843
+ }
1844
+ function createFlatFileAdapter(options) {
1845
+ const {
1846
+ format,
1847
+ extensions,
1848
+ sniff,
1849
+ parseEntries,
1850
+ serializeEntries,
1851
+ extractPlaceholders,
1852
+ validateMessage,
1853
+ computeInvalidIcuKeys
1854
+ } = options;
1855
+ return {
1856
+ format,
1857
+ canHandle: buildCanHandle(extensions, sniff),
1858
+ extractPlaceholders,
1859
+ validateMessage: validateMessage ?? (() => true),
1860
+ async read(filePath, locale) {
1861
+ const content = await readFileContent(filePath);
1862
+ const namespace = namespaceOf(filePath);
1863
+ const entries = toEntries2(content, namespace, parseEntries);
1864
+ const resource = { locale, namespace, format, entries };
1865
+ const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
1866
+ return { resource, invalidIcuKeys };
1867
+ },
1868
+ async write(resource, filePath) {
1869
+ const data = await serializeEntries(resource.entries, filePath);
1870
+ await atomicWriteFile(filePath, data);
1871
+ }
1872
+ };
1873
+ }
1874
+ var XLIFF_PATTERN = /<(?:x|g|bx|ex|ph|it|mrk)\b[^>]*>|\{[^{}]+\}/g;
1875
+ function extractXliffPlaceholders(value) {
1876
+ const result = [];
1877
+ for (const match of value.matchAll(XLIFF_PATTERN)) {
1878
+ const token = match[0];
1879
+ if (token !== void 0) {
1880
+ result.push(token);
1881
+ }
1882
+ }
1883
+ return result;
1884
+ }
1885
+ var ELEMENT_NODE = 1;
1886
+ function isElement(node) {
1887
+ return node.nodeType === ELEMENT_NODE;
1888
+ }
1889
+ function elementChildren(parent) {
1890
+ return Array.from(parent.childNodes).filter(isElement);
1891
+ }
1892
+ function childByName(parent, name) {
1893
+ return elementChildren(parent).find((el) => el.localName === name) ?? null;
1894
+ }
1895
+ function collectByTag(root, name) {
1896
+ return Array.from(root.getElementsByTagName(name));
1897
+ }
1898
+ function unitKey(element, index) {
1899
+ return element.getAttribute("id") ?? element.getAttribute("resname") ?? `unit-${index}`;
1900
+ }
1901
+ function onFatal(level) {
1902
+ if (level === "fatalError") {
1903
+ throw new Error("malformed XML");
1904
+ }
1905
+ }
1906
+ function assertNoDoctype(content) {
1907
+ if (/<!DOCTYPE/i.test(content) || /<!ENTITY/i.test(content)) {
1908
+ throw new AdapterError("INVALID_XML", "XLIFF with a DTD or entity declaration is rejected.");
1909
+ }
1910
+ }
1911
+ function parseXml(content) {
1912
+ assertNoDoctype(content);
1913
+ let doc;
1914
+ try {
1915
+ doc = new xmldom.DOMParser({ onError: onFatal }).parseFromString(content, "text/xml");
1916
+ } catch {
1917
+ throw new AdapterError("INVALID_XML", "The file is not valid XML.");
1918
+ }
1919
+ const root = doc.documentElement;
1920
+ if (root === null || root.localName !== "xliff") {
1921
+ throw new AdapterError("INVALID_STRUCTURE", "The file is not an XLIFF document.");
1922
+ }
1923
+ return { doc, root };
1924
+ }
1925
+ function walkXliff12(root) {
1926
+ const units = [];
1927
+ collectByTag(root, "trans-unit").forEach((tu, index) => {
1928
+ const source = childByName(tu, "source");
1929
+ if (source !== null) {
1930
+ units.push({
1931
+ key: unitKey(tu, index),
1932
+ source,
1933
+ target: childByName(tu, "target"),
1934
+ container: tu
1935
+ });
1936
+ }
1937
+ });
1938
+ return units;
1939
+ }
1940
+ function walkXliff20(root) {
1941
+ const units = [];
1942
+ collectByTag(root, "unit").forEach((unit, index) => {
1943
+ const baseKey = unitKey(unit, index);
1944
+ const segments = elementChildren(unit).filter((el) => el.localName === "segment");
1945
+ segments.forEach((segment, segIndex) => {
1946
+ const source = childByName(segment, "source");
1947
+ if (source !== null) {
1948
+ const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
1949
+ units.push({ key, source, target: childByName(segment, "target"), container: segment });
1950
+ }
1951
+ });
1952
+ });
1953
+ return units;
1954
+ }
1955
+ function walkUnits(root) {
1956
+ const version = root.getAttribute("version") ?? "1.2";
1957
+ return version.startsWith("2") ? walkXliff20(root) : walkXliff12(root);
1958
+ }
1959
+ function innerXml(serializer, element) {
1960
+ return Array.from(element.childNodes).map((node) => serializer.serializeToString(node)).join("");
1961
+ }
1962
+ function unitValue(serializer, unit) {
1963
+ if (unit.target !== null) {
1964
+ const targetXml = innerXml(serializer, unit.target);
1965
+ if (targetXml.trim() !== "") {
1966
+ return targetXml;
1967
+ }
1968
+ }
1969
+ return innerXml(serializer, unit.source);
1970
+ }
1971
+ function parseXliffEntries(content, namespace) {
1972
+ const { root } = parseXml(content);
1973
+ const serializer = new xmldom.XMLSerializer();
1974
+ const out = /* @__PURE__ */ new Map();
1975
+ for (const unit of walkUnits(root)) {
1976
+ const value = unitValue(serializer, unit);
1977
+ out.set(unit.key, {
1978
+ key: unit.key,
1979
+ namespace,
1980
+ value,
1981
+ placeholders: extractXliffPlaceholders(value),
1982
+ isPlural: false
1983
+ });
1984
+ }
1985
+ return out;
1986
+ }
1987
+ async function readDestination(filePath) {
1988
+ let outcome;
1989
+ try {
1990
+ outcome = await readBounded2(filePath);
1991
+ } catch {
1992
+ throw new AdapterError("INVALID_STRUCTURE", "The destination XLIFF file does not exist.");
1993
+ }
1994
+ if (outcome.kind === "not-a-file") {
1995
+ throw new AdapterError("INVALID_STRUCTURE", "The destination path is not a regular file.");
1996
+ }
1997
+ if (outcome.kind === "too-large") {
1998
+ throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
1999
+ }
2000
+ return outcome.content;
2001
+ }
2002
+ function fragmentNodes(parser, value) {
2003
+ try {
2004
+ const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
2005
+ return root === null ? null : Array.from(root.childNodes);
2006
+ } catch {
2007
+ return null;
2008
+ }
2009
+ }
2010
+ function setTargetValue(doc, parser, element, value) {
2011
+ while (element.firstChild !== null) {
2012
+ element.removeChild(element.firstChild);
2013
+ }
2014
+ const nodes = fragmentNodes(parser, value);
2015
+ if (nodes === null) {
2016
+ element.textContent = value;
2017
+ return;
2018
+ }
2019
+ for (const node of nodes) {
2020
+ element.appendChild(doc.importNode(node, true));
2021
+ }
2022
+ }
2023
+ async function serializeXliffEntries(entries, filePath) {
2024
+ const { doc, root } = parseXml(await readDestination(filePath));
2025
+ const parser = new xmldom.DOMParser({ onError: onFatal });
2026
+ for (const unit of walkUnits(root)) {
2027
+ const entry = entries.get(unit.key);
2028
+ if (entry !== void 0) {
2029
+ const target = unit.target ?? doc.createElement("target");
2030
+ if (unit.target === null) {
2031
+ unit.container.appendChild(target);
2032
+ }
2033
+ setTargetValue(doc, parser, target, entry.value);
2034
+ }
2035
+ }
2036
+ return new xmldom.XMLSerializer().serializeToString(doc);
2037
+ }
2038
+ function sniffXliff(sample) {
2039
+ const head = sample.trimStart();
2040
+ return head.startsWith("<xliff") || head.startsWith("<?xml");
2041
+ }
2042
+ function createXliffAdapter() {
2043
+ return createFlatFileAdapter({
2044
+ format: "xliff",
2045
+ extensions: [".xlf", ".xliff"],
2046
+ sniff: sniffXliff,
2047
+ parseEntries: parseXliffEntries,
2048
+ serializeEntries: serializeXliffEntries,
2049
+ extractPlaceholders: extractXliffPlaceholders
2050
+ });
2051
+ }
2052
+ function parseYamlObject(content) {
2053
+ let parsed;
2054
+ try {
2055
+ parsed = yaml.parse(content, { maxAliasCount: 100 });
2056
+ } catch {
2057
+ throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
2058
+ }
2059
+ return assertJsonRecord(parsed);
2060
+ }
2061
+ function createYamlAdapter() {
2062
+ return createTreeFileAdapter({
2063
+ format: "yaml",
2064
+ extensions: [".yml", ".yaml"],
2065
+ parse: parseYamlObject,
2066
+ serialize: (tree) => yaml.stringify(tree),
2067
+ extractPlaceholders: extractDoubleBracePlaceholders,
2068
+ deriveEntry: (_key, value) => ({
2069
+ placeholders: extractDoubleBracePlaceholders(value),
2070
+ isPlural: false
2071
+ })
2072
+ });
2073
+ }
1674
2074
  function createDefaultRegistry() {
1675
- return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter());
2075
+ return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter());
1676
2076
  }
1677
2077
 
1678
2078
  // src/selection/select-adapter.ts
@@ -1687,6 +2087,89 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
1687
2087
  );
1688
2088
  }
1689
2089
 
2090
+ // src/flow/source.ts
2091
+ async function readSource(config, cwd, fs, adapter) {
2092
+ const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
2093
+ if (!await fs.fileExists(sourcePath)) {
2094
+ throw new SdkError(
2095
+ "SOURCE_UNREADABLE",
2096
+ `The source locale file was not found at ${sourcePath}.`
2097
+ );
2098
+ }
2099
+ try {
2100
+ return await adapter.read(sourcePath, config.sourceLocale);
2101
+ } catch (error) {
2102
+ const detail = error instanceof Error ? error.message : String(error);
2103
+ throw new SdkError(
2104
+ "SOURCE_INVALID",
2105
+ `The source locale file at ${sourcePath} could not be read: ${detail}`
2106
+ );
2107
+ }
2108
+ }
2109
+
2110
+ // src/flow/diff-locales.ts
2111
+ async function readTarget(cwd, config, adapter, fs, locale) {
2112
+ const path = localeFilePath(cwd, config.files.pattern, locale);
2113
+ if (!await fs.fileExists(path)) {
2114
+ return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
2115
+ }
2116
+ return (await adapter.read(path, locale)).resource;
2117
+ }
2118
+ function selectedLocales(config, requested) {
2119
+ if (requested === void 0) {
2120
+ return config.targetLocales;
2121
+ }
2122
+ const wanted = new Set(requested);
2123
+ return config.targetLocales.filter((locale) => wanted.has(locale));
2124
+ }
2125
+ async function diffLocales(input, deps = {}) {
2126
+ const config = input.config;
2127
+ const cwd = input.cwd ?? process.cwd();
2128
+ const fs = deps.fs ?? defaultFs;
2129
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
2130
+ const source = await readSource(config, cwd, fs, adapter);
2131
+ const lock = await readLockFile(lockFilePath(cwd), fs);
2132
+ return Promise.all(
2133
+ selectedLocales(config, input.locales).map(async (locale) => {
2134
+ const target = await readTarget(cwd, config, adapter, fs, locale);
2135
+ const diff2 = diffResources(source.resource, target, { baseline: baselineFor(lock, locale) });
2136
+ return { locale, diff: diff2 };
2137
+ })
2138
+ );
2139
+ }
2140
+
2141
+ // src/flow/check.ts
2142
+ function toCheckSummary(locale, diff2) {
2143
+ return {
2144
+ locale,
2145
+ missing: diff2.missing.length,
2146
+ stale: diff2.changed.length,
2147
+ upToDate: diff2.unchanged.length,
2148
+ inSync: diff2.missing.length === 0 && diff2.changed.length === 0
2149
+ };
2150
+ }
2151
+ async function check(input, deps = {}) {
2152
+ const results = await diffLocales(input, deps);
2153
+ const locales = results.map(({ locale, diff: diff2 }) => toCheckSummary(locale, diff2));
2154
+ return { inSync: locales.every((entry) => entry.inSync), locales };
2155
+ }
2156
+
2157
+ // src/flow/diff.ts
2158
+ function toLocaleDiff(locale, diff2) {
2159
+ return {
2160
+ locale,
2161
+ missing: diff2.missing,
2162
+ changed: diff2.changed,
2163
+ orphaned: diff2.orphaned,
2164
+ hasPendingChanges: diff2.missing.length > 0 || diff2.changed.length > 0
2165
+ };
2166
+ }
2167
+ async function diff(input, deps = {}) {
2168
+ const results = await diffLocales(input, deps);
2169
+ const locales = results.map(({ locale, diff: result }) => toLocaleDiff(locale, result));
2170
+ return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
2171
+ }
2172
+
1690
2173
  // src/selection/select-provider.ts
1691
2174
  function selectProvider(config, createProvider = buildProvider) {
1692
2175
  try {
@@ -1937,7 +2420,7 @@ async function generatePluralForms(context) {
1937
2420
  function emptyResource(locale, format) {
1938
2421
  return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
1939
2422
  }
1940
- async function readTarget(params) {
2423
+ async function readTarget2(params) {
1941
2424
  const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
1942
2425
  if (!await params.fs.fileExists(path)) {
1943
2426
  return emptyResource(params.targetLocale, params.format);
@@ -1955,12 +2438,12 @@ function buildRequest3(params, entries) {
1955
2438
  };
1956
2439
  }
1957
2440
  async function runLocale(params) {
1958
- const target = await readTarget(params);
1959
- const diff = diffResources(params.source, target, { baseline: params.baseline });
1960
- const orphaned = params.generatePlurals ? diff.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff.orphaned;
2441
+ const target = await readTarget2(params);
2442
+ const diff2 = diffResources(params.source, target, { baseline: params.baseline });
2443
+ const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
1961
2444
  const pruned = params.prune ? orphaned : [];
1962
2445
  const invalidIcu = new Set(params.sourceInvalidIcuKeys);
1963
- const candidates = [...diff.missing, ...diff.changed];
2446
+ const candidates = [...diff2.missing, ...diff2.changed];
1964
2447
  const toTranslate = candidates.filter((key) => !invalidIcu.has(key));
1965
2448
  const invalidIcuSource = candidates.filter((key) => invalidIcu.has(key));
1966
2449
  const pluralNotice = detectMissingPluralCategories(
@@ -1974,7 +2457,7 @@ async function runLocale(params) {
1974
2457
  return {
1975
2458
  summary: baseSummary({
1976
2459
  locale: params.targetLocale,
1977
- unchanged: diff.unchanged,
2460
+ unchanged: diff2.unchanged,
1978
2461
  orphaned,
1979
2462
  invalidIcuSource,
1980
2463
  translated: toTranslate,
@@ -2023,12 +2506,12 @@ async function runLocale(params) {
2023
2506
  return {
2024
2507
  summary: baseSummary({
2025
2508
  locale: params.targetLocale,
2026
- unchanged: diff.unchanged,
2509
+ unchanged: diff2.unchanged,
2027
2510
  orphaned,
2028
2511
  invalidIcuSource,
2029
2512
  translated: [...accepted.keys()],
2030
2513
  generated: generation.accepted.map((form) => form.targetKey).sort(),
2031
- // Withheld generated forms surface alongside withheld translations (spec D4): both failed integrity.
2514
+ // Withheld generated forms surface alongside withheld translations: both failed integrity.
2032
2515
  integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
2033
2516
  pruned,
2034
2517
  notices
@@ -2152,26 +2635,6 @@ function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
2152
2635
  }
2153
2636
  }
2154
2637
 
2155
- // src/flow/source.ts
2156
- async function readSource(config, cwd, fs, adapter) {
2157
- const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
2158
- if (!await fs.fileExists(sourcePath)) {
2159
- throw new SdkError(
2160
- "SOURCE_UNREADABLE",
2161
- `The source locale file was not found at ${sourcePath}.`
2162
- );
2163
- }
2164
- try {
2165
- return await adapter.read(sourcePath, config.sourceLocale);
2166
- } catch (error) {
2167
- const detail = error instanceof Error ? error.message : String(error);
2168
- throw new SdkError(
2169
- "SOURCE_INVALID",
2170
- `The source locale file at ${sourcePath} could not be read: ${detail}`
2171
- );
2172
- }
2173
- }
2174
-
2175
2638
  // src/flow/translate-project.ts
2176
2639
  async function translate2(input, deps = {}) {
2177
2640
  const config = input.config;
@@ -2380,7 +2843,7 @@ var DEFAULT_WORKBOOK_LIMITS = {
2380
2843
  maxRowsPerSheet: 1e5,
2381
2844
  maxCellsPerRow: 64
2382
2845
  };
2383
- function assertNoDoctype(name, xml) {
2846
+ function assertNoDoctype2(name, xml) {
2384
2847
  if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
2385
2848
  throw new ExchangeError(
2386
2849
  "WORKBOOK_INVALID",
@@ -2435,7 +2898,7 @@ async function guardWorkbookBytes(bytes, limits) {
2435
2898
  `The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
2436
2899
  );
2437
2900
  }
2438
- assertNoDoctype(file.name, content);
2901
+ assertNoDoctype2(file.name, content);
2439
2902
  }
2440
2903
  }
2441
2904
  var rowSchema = zod.z.object({
@@ -2544,7 +3007,7 @@ async function readWorkbook(bytes, options = {}) {
2544
3007
 
2545
3008
  // src/flow/workbook/export-workbook.ts
2546
3009
  var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
2547
- async function readTarget2(cwd, config, adapter, fs, locale) {
3010
+ async function readTarget3(cwd, config, adapter, fs, locale) {
2548
3011
  const path = localeFilePath(cwd, config.files.pattern, locale);
2549
3012
  if (!await fs.fileExists(path)) {
2550
3013
  return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
@@ -2552,7 +3015,7 @@ async function readTarget2(cwd, config, adapter, fs, locale) {
2552
3015
  return (await adapter.read(path, locale)).resource;
2553
3016
  }
2554
3017
  function buildRows(source, target, baseline, includeUnchanged) {
2555
- const diff = diffResources(source, target, { baseline });
3018
+ const diff2 = diffResources(source, target, { baseline });
2556
3019
  const rows = [];
2557
3020
  const add = (keys, status) => {
2558
3021
  for (const key of keys) {
@@ -2570,14 +3033,14 @@ function buildRows(source, target, baseline, includeUnchanged) {
2570
3033
  });
2571
3034
  }
2572
3035
  };
2573
- add(diff.missing, "new");
2574
- add(diff.changed, "changed");
3036
+ add(diff2.missing, "new");
3037
+ add(diff2.changed, "changed");
2575
3038
  if (includeUnchanged) {
2576
- add(diff.unchanged, "changed");
3039
+ add(diff2.unchanged, "changed");
2577
3040
  }
2578
3041
  return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
2579
3042
  }
2580
- function selectedLocales(config, requested) {
3043
+ function selectedLocales2(config, requested) {
2581
3044
  if (requested === void 0) {
2582
3045
  return config.targetLocales;
2583
3046
  }
@@ -2591,10 +3054,10 @@ async function exportWorkbook(input, deps = {}) {
2591
3054
  const adapter = selectAdapter(config.format, deps.adapterRegistry);
2592
3055
  const source = await readSource(config, cwd, fs, adapter);
2593
3056
  const lock = await readLockFile(lockFilePath(cwd), fs);
2594
- const locales = selectedLocales(config, input.locales);
3057
+ const locales = selectedLocales2(config, input.locales);
2595
3058
  const sheets = await Promise.all(
2596
3059
  locales.map(async (locale) => {
2597
- const target = await readTarget2(cwd, config, adapter, fs, locale);
3060
+ const target = await readTarget3(cwd, config, adapter, fs, locale);
2598
3061
  const rows = buildRows(
2599
3062
  source.resource,
2600
3063
  target,
@@ -2664,7 +3127,7 @@ function classifyRows(params, buckets) {
2664
3127
  }
2665
3128
  }
2666
3129
  function importLocale(params) {
2667
- const diff = diffResources(params.source, params.target, { baseline: params.baseline });
3130
+ const diff2 = diffResources(params.source, params.target, { baseline: params.baseline });
2668
3131
  const buckets = { accepted: /* @__PURE__ */ new Map(), mismatches: [], withheld: /* @__PURE__ */ new Set() };
2669
3132
  classifyRows(params, buckets);
2670
3133
  const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
@@ -2673,8 +3136,8 @@ function importLocale(params) {
2673
3136
  locale: params.sheet.locale,
2674
3137
  status: "succeeded",
2675
3138
  translated: [...buckets.accepted.keys()].sort(),
2676
- unchanged: diff.unchanged,
2677
- orphaned: diff.orphaned,
3139
+ unchanged: diff2.unchanged,
3140
+ orphaned: diff2.orphaned,
2678
3141
  // Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
2679
3142
  pruned: [],
2680
3143
  invalidIcuSource,
@@ -2701,7 +3164,7 @@ async function readWorkbookBytes(path, fs) {
2701
3164
  }
2702
3165
  return read.bytes;
2703
3166
  }
2704
- async function readTarget3(cwd, config, adapter, fs, locale) {
3167
+ async function readTarget4(cwd, config, adapter, fs, locale) {
2705
3168
  const path = localeFilePath(cwd, config.files.pattern, locale);
2706
3169
  if (!await fs.fileExists(path)) {
2707
3170
  return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
@@ -2740,7 +3203,7 @@ async function runSheet(ctx, sheet, lock) {
2740
3203
  `The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
2741
3204
  );
2742
3205
  }
2743
- const target = await readTarget3(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
3206
+ const target = await readTarget4(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
2744
3207
  const baseline = baselineFor(lock, sheet.locale);
2745
3208
  const { summary, accepted, withheld } = importLocale({
2746
3209
  sheet,
@@ -2810,6 +3273,16 @@ async function importWorkbook(input, deps = {}) {
2810
3273
  const { succeeded, failed } = partition(summaries);
2811
3274
  return { dryRun, locales: summaries, succeeded, failed };
2812
3275
  }
3276
+
3277
+ // src/scaffolding.ts
3278
+ var scaffoldingMetadata = {
3279
+ /** Provider id -> the environment variable its API key is read from. Owned by ai-providers. */
3280
+ providerEnv: PROVIDER_ENV,
3281
+ /** LLM provider id -> a cosmetic default scaffold model. Owned by ai-providers. DeepL has none. */
3282
+ scaffoldModels: SCAFFOLD_MODELS,
3283
+ /** The closed set of source format ids. Owned by core. */
3284
+ supportedFormats: SUPPORTED_FORMATS
3285
+ };
2813
3286
  var defaultCreateWatcher = (paths) => {
2814
3287
  const fsWatcher = chokidar.watch([...paths], { persistent: true, ignoreInitial: true });
2815
3288
  return {
@@ -2918,10 +3391,13 @@ async function watch(input, deps = {}) {
2918
3391
 
2919
3392
  exports.DEFAULT_WORKBOOK_PATH = DEFAULT_WORKBOOK_PATH;
2920
3393
  exports.SdkError = SdkError;
3394
+ exports.check = check;
2921
3395
  exports.defineConfig = defineConfig;
3396
+ exports.diff = diff;
2922
3397
  exports.exportWorkbook = exportWorkbook;
2923
3398
  exports.importWorkbook = importWorkbook;
2924
3399
  exports.loadConfig = loadConfig;
3400
+ exports.scaffoldingMetadata = scaffoldingMetadata;
2925
3401
  exports.translate = translate2;
2926
3402
  exports.verbatraConfigSchema = verbatraConfigSchema;
2927
3403
  exports.watch = watch;