@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.js CHANGED
@@ -5,12 +5,15 @@ import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
5
5
  import { z } from 'zod';
6
6
  import Anthropic from '@anthropic-ai/sdk';
7
7
  import * as deepl from 'deepl-node';
8
+ import { createRequire } from 'module';
8
9
  import log from 'loglevel';
9
10
  import { GoogleGenAI } from '@google/genai';
10
11
  import OpenAI from 'openai';
11
12
  import { randomUUID } from 'crypto';
12
13
  import { access, writeFile, rename, rm, open } from 'fs/promises';
13
- import { parse, TYPE } from '@formatjs/icu-messageformat-parser';
14
+ import { parse as parse$1, TYPE } from '@formatjs/icu-messageformat-parser';
15
+ import { XMLSerializer, DOMParser } from '@xmldom/xmldom';
16
+ import { stringify, parse } from 'yaml';
14
17
  import ExcelJS from 'exceljs';
15
18
  import JSZip from 'jszip';
16
19
  import { watch as watch$1 } from 'chokidar';
@@ -100,7 +103,10 @@ var SUPPORTED_FORMATS = [
100
103
  "i18next-json",
101
104
  "vue-i18n-json",
102
105
  "next-intl-json",
103
- "ngx-translate-json"
106
+ "ngx-translate-json",
107
+ "xliff",
108
+ "yaml",
109
+ "arb"
104
110
  ];
105
111
  var supportedFormatSchema = z.enum(SUPPORTED_FORMATS);
106
112
  var translationEntrySchema = z.object({
@@ -155,6 +161,26 @@ var LOCALE_TOKEN = "{locale}";
155
161
  function localeFilePath(cwd, pattern, locale) {
156
162
  return resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
157
163
  }
164
+ var REDACTED = "[REDACTED]";
165
+ var KEY_PATTERNS = [
166
+ // The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
167
+ /\bsk-[A-Za-z0-9_-]{8,}/g,
168
+ /AIza[0-9A-Za-z_-]{35}/g,
169
+ /[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
170
+ ];
171
+ function escapeForRegExp(value) {
172
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
173
+ }
174
+ function redact(text, secret = process.env.ANTHROPIC_API_KEY) {
175
+ let out = text;
176
+ for (const pattern of KEY_PATTERNS) {
177
+ out = out.replace(pattern, REDACTED);
178
+ }
179
+ if (secret !== void 0 && secret.length > 0) {
180
+ out = out.replace(new RegExp(escapeForRegExp(secret), "g"), REDACTED);
181
+ }
182
+ return out;
183
+ }
158
184
  var ProviderError = class extends Error {
159
185
  /** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
160
186
  code;
@@ -163,7 +189,7 @@ var ProviderError = class extends Error {
163
189
  * @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
164
190
  */
165
191
  constructor(code, message) {
166
- super(message);
192
+ super(redact(message, ""));
167
193
  this.name = "ProviderError";
168
194
  this.code = code;
169
195
  }
@@ -290,6 +316,12 @@ function assertNotTruncated(truncated) {
290
316
  throw new ProviderError("OUTPUT_TRUNCATED", OUTPUT_TRUNCATED_MESSAGE);
291
317
  }
292
318
  }
319
+ var PROVIDER_ENV = {
320
+ anthropic: "ANTHROPIC_API_KEY",
321
+ openai: "OPENAI_API_KEY",
322
+ gemini: "GEMINI_API_KEY",
323
+ deepl: "DEEPL_API_KEY"
324
+ };
293
325
  function readRequiredEnv(name) {
294
326
  const value = process.env[name];
295
327
  if (value === void 0 || value.length === 0) {
@@ -298,16 +330,16 @@ function readRequiredEnv(name) {
298
330
  return value;
299
331
  }
300
332
  function requireAnthropicKey() {
301
- return readRequiredEnv("ANTHROPIC_API_KEY");
333
+ return readRequiredEnv(PROVIDER_ENV.anthropic);
302
334
  }
303
335
  function requireOpenAiKey() {
304
- return readRequiredEnv("OPENAI_API_KEY");
336
+ return readRequiredEnv(PROVIDER_ENV.openai);
305
337
  }
306
338
  function requireGeminiKey() {
307
- return readRequiredEnv("GEMINI_API_KEY");
339
+ return readRequiredEnv(PROVIDER_ENV.gemini);
308
340
  }
309
341
  function requireDeepLKey() {
310
- return readRequiredEnv("DEEPL_API_KEY");
342
+ return readRequiredEnv(PROVIDER_ENV.deepl);
311
343
  }
312
344
  function createDefaultClient() {
313
345
  const sdk = new Anthropic({ apiKey: requireAnthropicKey(), logLevel: "off" });
@@ -408,8 +440,22 @@ function toUsage(usage) {
408
440
  var deepLConfigSchema = z.object({
409
441
  glossaryId: z.string().min(1).optional()
410
442
  });
443
+ var DEEPL_LOGGER = "deepl";
444
+ function resolveDeeplLoglevel(requireFn = createRequire(import.meta.url)) {
445
+ try {
446
+ const entry = requireFn.resolve("deepl-node");
447
+ return createRequire(entry)("loglevel");
448
+ } catch {
449
+ return void 0;
450
+ }
451
+ }
452
+ function silenceDeeplLogger(instances) {
453
+ for (const instance of instances) {
454
+ instance?.getLogger(DEEPL_LOGGER).setLevel("silent");
455
+ }
456
+ }
411
457
  function silenceSdkLogging() {
412
- log.getLogger("deepl").setLevel("silent");
458
+ silenceDeeplLogger([log, resolveDeeplLoglevel()]);
413
459
  }
414
460
  function createDefaultClient2() {
415
461
  silenceSdkLogging();
@@ -770,6 +816,11 @@ function createMechanism3(client, config) {
770
816
  function callClient4(client, body) {
771
817
  return guardProviderCall(() => client.chat.completions.create(body));
772
818
  }
819
+ var SCAFFOLD_MODELS = {
820
+ anthropic: "claude-sonnet-4-6",
821
+ openai: "gpt-5.4-mini",
822
+ gemini: "gemini-2.5-flash"
823
+ };
773
824
  var providerConfigSchema = z.discriminatedUnion("id", [
774
825
  z.object({ id: z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
775
826
  z.object({ id: z.literal("openai"), options: openAiConfigSchema.strict() }),
@@ -800,27 +851,25 @@ var verbatraConfigSchema = z.strictObject({
800
851
  glossary: z.record(z.string(), z.string()).optional(),
801
852
  tone: z.enum(["formal", "informal", "neutral"]).optional(),
802
853
  /**
803
- * Opt-in orphan pruning, off by default (absent is treated as false). When true, keys present in a
804
- * target file but absent from the source (the diff's orphaned keys) are removed from the written file
805
- * and the lock. A per-run `prune` option on `translate` (the CLI `--prune` flag) overrides this. This
806
- * is non-secret, consistent with the config's no-secret invariant.
854
+ * Opt-in orphan pruning, off by default. When true, keys present in a target file but absent from
855
+ * the source are removed from the written file and the lock. The per-run `prune` option on
856
+ * `translate` (the CLI `--prune` flag) overrides this.
807
857
  */
808
858
  prune: z.boolean().optional(),
809
859
  /**
810
- * Opt-in plural-category generation, off by default (absent is treated as false). When true, and only
811
- * for an i18next-JSON project translated by an LLM provider, verbatra synthesizes the CLDR plural forms
812
- * a target language requires but the source does not supply (for example Polish few/many). A per-run
860
+ * Opt-in plural-category generation, off by default. When true, and only for an i18next-JSON project
861
+ * translated by an LLM provider, verbatra synthesizes the CLDR plural forms a target language
862
+ * requires but the source does not supply (for example Polish few/many). The per-run
813
863
  * `generatePlurals` option on `translate` overrides this. Unsupported cases (DeepL, non-i18next, an
814
864
  * unknown language) fall back to the per-locale plural warning.
815
865
  */
816
866
  generatePlurals: z.boolean().optional(),
817
867
  /**
818
- * Optional maximum number of entries sent in a single provider request. A locale's missing-plus-changed
819
- * entries are split into sequential sub-batches no larger than this so one oversized request cannot sink
820
- * the whole locale; a failed sub-batch is withheld and retried while the others still make progress.
821
- * Must be a positive integer (non-integer, zero, or negative is rejected at this boundary, never
822
- * coerced). When absent, {@link DEFAULT_MAX_BATCH_SIZE} applies: 50, a conservative count that stays
823
- * well inside provider context windows for typical short i18n strings while keeping request counts low.
868
+ * Optional maximum number of entries sent in a single provider request. A locale's missing and
869
+ * changed entries are split into sequential sub-batches no larger than this, so one oversized request
870
+ * cannot sink the whole locale; a failed sub-batch is withheld while the others make progress. Must
871
+ * be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
872
+ * {@link DEFAULT_MAX_BATCH_SIZE} applies.
824
873
  */
825
874
  maxBatchSize: z.number().int().positive().optional()
826
875
  }).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
@@ -1052,6 +1101,85 @@ async function writeLockFile(path, lock, fs) {
1052
1101
  await fs.writeFile(path, `${JSON.stringify(ordered, null, 2)}
1053
1102
  `);
1054
1103
  }
1104
+ var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
1105
+ var INVALID = { placeholders: [], isPlural: false, valid: false };
1106
+ function tokenOf(element) {
1107
+ switch (element.type) {
1108
+ case TYPE.argument:
1109
+ case TYPE.number:
1110
+ case TYPE.date:
1111
+ case TYPE.time:
1112
+ case TYPE.select:
1113
+ case TYPE.plural:
1114
+ return `{${element.value}}`;
1115
+ case TYPE.tag:
1116
+ return `<${element.value}>`;
1117
+ default:
1118
+ return void 0;
1119
+ }
1120
+ }
1121
+ function childMessages(element) {
1122
+ if (element.type === TYPE.plural || element.type === TYPE.select) {
1123
+ return Object.values(element.options).map((option) => option.value);
1124
+ }
1125
+ if (element.type === TYPE.tag) {
1126
+ return [element.children];
1127
+ }
1128
+ return [];
1129
+ }
1130
+ function collect(elements, add, state) {
1131
+ for (const element of elements) {
1132
+ const token = tokenOf(element);
1133
+ if (token !== void 0) {
1134
+ add(token);
1135
+ }
1136
+ if (element.type === TYPE.plural) {
1137
+ state.isPlural = true;
1138
+ }
1139
+ for (const child of childMessages(element)) {
1140
+ collect(child, add, state);
1141
+ }
1142
+ }
1143
+ }
1144
+ function analyzeIcuValue(value) {
1145
+ if (!value.includes("{") && !value.includes("<")) {
1146
+ return VALID_EMPTY;
1147
+ }
1148
+ try {
1149
+ const ast = parse$1(value);
1150
+ const placeholders = [];
1151
+ const state = { isPlural: false };
1152
+ collect(
1153
+ ast,
1154
+ (token) => {
1155
+ placeholders.push(token);
1156
+ },
1157
+ state
1158
+ );
1159
+ return { placeholders, isPlural: state.isPlural, valid: true };
1160
+ } catch {
1161
+ return INVALID;
1162
+ }
1163
+ }
1164
+ function icuPlaceholders(value) {
1165
+ return analyzeIcuValue(value).placeholders;
1166
+ }
1167
+ function icuIsValid(value) {
1168
+ return analyzeIcuValue(value).valid;
1169
+ }
1170
+ function icuDeriveEntry(_key, value) {
1171
+ const analysis = analyzeIcuValue(value);
1172
+ return { placeholders: analysis.placeholders, isPlural: analysis.isPlural };
1173
+ }
1174
+ function icuInvalidKeys(entries) {
1175
+ const invalid = [];
1176
+ for (const [key, entry] of entries) {
1177
+ if (!icuIsValid(entry.value)) {
1178
+ invalid.push(key);
1179
+ }
1180
+ }
1181
+ return invalid;
1182
+ }
1055
1183
  var AdapterError = class extends Error {
1056
1184
  code;
1057
1185
  constructor(code, message) {
@@ -1060,6 +1188,82 @@ var AdapterError = class extends Error {
1060
1188
  this.code = code;
1061
1189
  }
1062
1190
  };
1191
+ var MAX_DEPTH = 100;
1192
+ var MAX_INPUT_BYTES = 16 * 1024 * 1024;
1193
+ var jsonTreeSchema = z.lazy(
1194
+ () => z.union([z.string(), z.record(z.string(), jsonTreeSchema)])
1195
+ );
1196
+ var rootSchema = z.record(z.string(), jsonTreeSchema);
1197
+ function assertWithinDepth(value, max) {
1198
+ const stack = [{ node: value, depth: 1 }];
1199
+ while (stack.length > 0) {
1200
+ const top = stack.pop();
1201
+ if (top === void 0) {
1202
+ break;
1203
+ }
1204
+ const { node, depth } = top;
1205
+ if (typeof node !== "object" || node === null) {
1206
+ continue;
1207
+ }
1208
+ if (depth > max) {
1209
+ throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
1210
+ }
1211
+ for (const child of Object.values(node)) {
1212
+ stack.push({ node: child, depth: depth + 1 });
1213
+ }
1214
+ }
1215
+ }
1216
+ function assertJsonRecord(value) {
1217
+ assertWithinDepth(value, MAX_DEPTH);
1218
+ const result = rootSchema.safeParse(value);
1219
+ if (!result.success) {
1220
+ throw new AdapterError(
1221
+ "INVALID_STRUCTURE",
1222
+ "The file is not a valid object (expected nested objects of string values)."
1223
+ );
1224
+ }
1225
+ return result.data;
1226
+ }
1227
+ function parseJsonObject(content) {
1228
+ let parsed;
1229
+ try {
1230
+ parsed = JSON.parse(content);
1231
+ } catch {
1232
+ throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
1233
+ }
1234
+ return assertJsonRecord(parsed);
1235
+ }
1236
+ function serializeJsonTree(tree) {
1237
+ return `${JSON.stringify(tree, null, 2)}
1238
+ `;
1239
+ }
1240
+ function namespaceOf(filePath) {
1241
+ return basename(filePath, extname(filePath));
1242
+ }
1243
+ function rethrowStructured(error, message) {
1244
+ if (error instanceof AdapterError) {
1245
+ throw error;
1246
+ }
1247
+ throw new AdapterError("INVALID_STRUCTURE", message);
1248
+ }
1249
+ function computeIcu(entries, compute) {
1250
+ if (!compute) {
1251
+ return [];
1252
+ }
1253
+ try {
1254
+ return compute(entries);
1255
+ } catch (error) {
1256
+ rethrowStructured(error, "The file could not be analyzed for message validity.");
1257
+ }
1258
+ }
1259
+ function buildCanHandle(extensions, sniff) {
1260
+ return (filePath, sample) => {
1261
+ if (!extensions.includes(extname(filePath).toLowerCase())) {
1262
+ return false;
1263
+ }
1264
+ return sample === void 0 || sniff === void 0 || sniff(sample);
1265
+ };
1266
+ }
1063
1267
  var nodeOps = {
1064
1268
  writeFile: (path, data) => writeFile(path, data, "utf8"),
1065
1269
  rename: (from, to) => rename(from, to),
@@ -1084,8 +1288,6 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
1084
1288
  throw error;
1085
1289
  }
1086
1290
  }
1087
- var MAX_DEPTH = 100;
1088
- var MAX_INPUT_BYTES = 16 * 1024 * 1024;
1089
1291
  async function readBoundedUtf82(handle, size) {
1090
1292
  const buffer = Buffer.allocUnsafe(size);
1091
1293
  let offset = 0;
@@ -1113,6 +1315,16 @@ async function readBounded2(filePath) {
1113
1315
  await handle.close();
1114
1316
  }
1115
1317
  }
1318
+ async function readFileContent(filePath) {
1319
+ const outcome = await readBounded2(filePath);
1320
+ if (outcome.kind === "not-a-file") {
1321
+ throw new AdapterError("INVALID_STRUCTURE", "The path is not a regular file.");
1322
+ }
1323
+ if (outcome.kind === "too-large") {
1324
+ throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
1325
+ }
1326
+ return outcome.content;
1327
+ }
1116
1328
  var BACKSLASH = "\\";
1117
1329
  var DOT = ".";
1118
1330
  var ESCAPED_BACKSLASH = "\\\\";
@@ -1226,46 +1438,6 @@ function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
1226
1438
  addEntries({ namespace, derive, out, claimed: /* @__PURE__ */ new Map() }, [], tree);
1227
1439
  return out;
1228
1440
  }
1229
- var jsonTreeSchema = z.lazy(
1230
- () => z.union([z.string(), z.record(z.string(), jsonTreeSchema)])
1231
- );
1232
- var rootSchema = z.record(z.string(), jsonTreeSchema);
1233
- function assertWithinDepth(value, max) {
1234
- const stack = [{ node: value, depth: 1 }];
1235
- while (stack.length > 0) {
1236
- const top = stack.pop();
1237
- if (top === void 0) {
1238
- break;
1239
- }
1240
- const { node, depth } = top;
1241
- if (typeof node !== "object" || node === null) {
1242
- continue;
1243
- }
1244
- if (depth > max) {
1245
- throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
1246
- }
1247
- for (const child of Object.values(node)) {
1248
- stack.push({ node: child, depth: depth + 1 });
1249
- }
1250
- }
1251
- }
1252
- function parseJsonObject(content) {
1253
- let parsed;
1254
- try {
1255
- parsed = JSON.parse(content);
1256
- } catch {
1257
- throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
1258
- }
1259
- assertWithinDepth(parsed, MAX_DEPTH);
1260
- const result = rootSchema.safeParse(parsed);
1261
- if (!result.success) {
1262
- throw new AdapterError(
1263
- "INVALID_STRUCTURE",
1264
- "The file is not a valid JSON object (expected nested objects of string values)."
1265
- );
1266
- }
1267
- return result.data;
1268
- }
1269
1441
  function emptyNode() {
1270
1442
  return /* @__PURE__ */ Object.create(null);
1271
1443
  }
@@ -1302,77 +1474,146 @@ function unflattenEntries(entries) {
1302
1474
  }
1303
1475
  return root;
1304
1476
  }
1305
- function namespaceOf(filePath) {
1306
- return basename(filePath, extname(filePath));
1307
- }
1308
- function canHandle(filePath, sample) {
1309
- if (extname(filePath).toLowerCase() !== ".json") {
1310
- return false;
1311
- }
1312
- return sample === void 0 || sample.trimStart().startsWith("{");
1313
- }
1314
- function rethrowStructured(error, message) {
1315
- if (error instanceof AdapterError) {
1316
- throw error;
1317
- }
1318
- throw new AdapterError("INVALID_STRUCTURE", message);
1319
- }
1320
- function toEntries(content, namespace, deriveEntry, keyMode, validateTree) {
1477
+ function toEntries(content, namespace, parse2, deriveEntry, keyMode, validateTree) {
1321
1478
  try {
1322
- const tree = parseJsonObject(content);
1479
+ const tree = parse2(content);
1323
1480
  validateTree?.(tree);
1324
1481
  return flattenTree(tree, namespace, deriveEntry, keyMode);
1325
1482
  } catch (error) {
1326
- rethrowStructured(error, "The file could not be read as JSON.");
1483
+ rethrowStructured(error, "The file could not be parsed.");
1327
1484
  }
1328
1485
  }
1329
- function computeIcu(entries, compute) {
1330
- if (!compute) {
1331
- return [];
1332
- }
1333
- try {
1334
- return compute(entries);
1335
- } catch (error) {
1336
- rethrowStructured(error, "The file could not be analyzed for message validity.");
1337
- }
1338
- }
1339
- function createJsonFileAdapter(options) {
1486
+ function createTreeFileAdapter(options) {
1340
1487
  const {
1341
1488
  format,
1489
+ extensions,
1490
+ sniff,
1491
+ parse: parse2,
1492
+ serialize,
1342
1493
  deriveEntry,
1343
- extractPlaceholders: extractPlaceholders2,
1344
- computeInvalidIcuKeys: computeInvalidIcuKeys2,
1345
- validateMessage: validateMessage2,
1494
+ extractPlaceholders,
1495
+ computeInvalidIcuKeys,
1496
+ validateMessage,
1346
1497
  validateTree,
1347
1498
  buildWriteTree,
1348
1499
  keyMode = "literal-leaf"
1349
1500
  } = options;
1350
1501
  return {
1351
1502
  format,
1352
- canHandle,
1353
- extractPlaceholders: extractPlaceholders2,
1354
- // Non-ICU formats supply no validator: every value is valid for their syntax.
1355
- validateMessage: validateMessage2 ?? (() => true),
1503
+ canHandle: buildCanHandle(extensions, sniff),
1504
+ extractPlaceholders,
1505
+ validateMessage: validateMessage ?? (() => true),
1356
1506
  async read(filePath, locale) {
1357
- const outcome = await readBounded2(filePath);
1358
- if (outcome.kind === "not-a-file") {
1359
- throw new AdapterError("INVALID_STRUCTURE", "The path is not a regular file.");
1360
- }
1361
- if (outcome.kind === "too-large") {
1362
- throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
1363
- }
1507
+ const content = await readFileContent(filePath);
1364
1508
  const namespace = namespaceOf(filePath);
1365
- const entries = toEntries(outcome.content, namespace, deriveEntry, keyMode, validateTree);
1509
+ const entries = toEntries(content, namespace, parse2, deriveEntry, keyMode, validateTree);
1366
1510
  const resource = { locale, namespace, format, entries };
1367
- const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys2);
1511
+ const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
1368
1512
  return { resource, invalidIcuKeys };
1369
1513
  },
1370
1514
  async write(resource, filePath) {
1371
1515
  const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
1372
- await atomicWriteFile(filePath, `${JSON.stringify(tree, null, 2)}
1373
- `);
1516
+ await atomicWriteFile(filePath, serialize(tree));
1517
+ }
1518
+ };
1519
+ }
1520
+ function isMetadataKey(key) {
1521
+ return key.startsWith("@");
1522
+ }
1523
+ function parseArbObject(content) {
1524
+ let parsed;
1525
+ try {
1526
+ parsed = JSON.parse(content);
1527
+ } catch {
1528
+ throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
1529
+ }
1530
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1531
+ throw new AdapterError(
1532
+ "INVALID_STRUCTURE",
1533
+ "The file is not a valid object (expected nested objects of string values)."
1534
+ );
1535
+ }
1536
+ return parsed;
1537
+ }
1538
+ function stripArbMetadata(tree) {
1539
+ const out = /* @__PURE__ */ Object.create(null);
1540
+ for (const [key, value] of Object.entries(tree)) {
1541
+ if (!isMetadataKey(key)) {
1542
+ out[key] = value;
1543
+ }
1544
+ }
1545
+ return out;
1546
+ }
1547
+ function originalKey(encoded) {
1548
+ return decodeKeyToSegments(encoded).join(".");
1549
+ }
1550
+ function messagesFromEntries(entries) {
1551
+ const out = /* @__PURE__ */ new Map();
1552
+ for (const [key, entry] of entries) {
1553
+ out.set(originalKey(key), entry.value);
1554
+ }
1555
+ return out;
1556
+ }
1557
+ async function readDestinationPairs(filePath) {
1558
+ let parsed;
1559
+ try {
1560
+ const outcome = await readBounded2(filePath);
1561
+ if (outcome.kind !== "ok") {
1562
+ return null;
1563
+ }
1564
+ parsed = JSON.parse(outcome.content);
1565
+ } catch {
1566
+ return null;
1567
+ }
1568
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1569
+ return null;
1570
+ }
1571
+ return Object.entries(parsed);
1572
+ }
1573
+ async function buildArbWriteTree(entries, filePath) {
1574
+ const messages = messagesFromEntries(entries);
1575
+ const pairs = await readDestinationPairs(filePath);
1576
+ const out = /* @__PURE__ */ Object.create(null);
1577
+ const consumed = /* @__PURE__ */ new Set();
1578
+ for (const [key, value] of pairs ?? []) {
1579
+ const translated = isMetadataKey(key) ? void 0 : messages.get(key);
1580
+ if (translated !== void 0) {
1581
+ consumed.add(key);
1374
1582
  }
1375
- };
1583
+ out[key] = translated ?? value;
1584
+ }
1585
+ for (const [key, value] of messages) {
1586
+ if (!consumed.has(key)) {
1587
+ out[key] = value;
1588
+ }
1589
+ }
1590
+ return out;
1591
+ }
1592
+ function parseArb(content) {
1593
+ return assertJsonRecord(stripArbMetadata(parseArbObject(content)));
1594
+ }
1595
+ function createArbAdapter() {
1596
+ return createTreeFileAdapter({
1597
+ format: "arb",
1598
+ extensions: [".arb"],
1599
+ sniff: (sample) => sample.trimStart().startsWith("{"),
1600
+ parse: parseArb,
1601
+ serialize: serializeJsonTree,
1602
+ extractPlaceholders: icuPlaceholders,
1603
+ deriveEntry: icuDeriveEntry,
1604
+ computeInvalidIcuKeys: icuInvalidKeys,
1605
+ validateMessage: icuIsValid,
1606
+ buildWriteTree: buildArbWriteTree
1607
+ });
1608
+ }
1609
+ function createJsonFileAdapter(options) {
1610
+ return createTreeFileAdapter({
1611
+ ...options,
1612
+ extensions: [".json"],
1613
+ sniff: (sample) => sample.trimStart().startsWith("{"),
1614
+ parse: parseJsonObject,
1615
+ serialize: serializeJsonTree
1616
+ });
1376
1617
  }
1377
1618
  var DOUBLE_BRACE_PATTERN = /\{\{[^{}]*\}\}/g;
1378
1619
  var I18NEXT_PATTERN = /\{\{[^{}]*\}\}|\$t\([^()]*\)/g;
@@ -1419,91 +1660,13 @@ function createI18nextJsonAdapter() {
1419
1660
  })
1420
1661
  });
1421
1662
  }
1422
- var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
1423
- var INVALID = { placeholders: [], isPlural: false, valid: false };
1424
- function tokenOf(element) {
1425
- switch (element.type) {
1426
- case TYPE.argument:
1427
- case TYPE.number:
1428
- case TYPE.date:
1429
- case TYPE.time:
1430
- case TYPE.select:
1431
- case TYPE.plural:
1432
- return `{${element.value}}`;
1433
- case TYPE.tag:
1434
- return `<${element.value}>`;
1435
- default:
1436
- return void 0;
1437
- }
1438
- }
1439
- function childMessages(element) {
1440
- if (element.type === TYPE.plural || element.type === TYPE.select) {
1441
- return Object.values(element.options).map((option) => option.value);
1442
- }
1443
- if (element.type === TYPE.tag) {
1444
- return [element.children];
1445
- }
1446
- return [];
1447
- }
1448
- function collect(elements, add, state) {
1449
- for (const element of elements) {
1450
- const token = tokenOf(element);
1451
- if (token !== void 0) {
1452
- add(token);
1453
- }
1454
- if (element.type === TYPE.plural) {
1455
- state.isPlural = true;
1456
- }
1457
- for (const child of childMessages(element)) {
1458
- collect(child, add, state);
1459
- }
1460
- }
1461
- }
1462
- function analyzeIcuValue(value) {
1463
- if (!value.includes("{") && !value.includes("<")) {
1464
- return VALID_EMPTY;
1465
- }
1466
- try {
1467
- const ast = parse(value);
1468
- const placeholders = [];
1469
- const state = { isPlural: false };
1470
- collect(
1471
- ast,
1472
- (token) => {
1473
- placeholders.push(token);
1474
- },
1475
- state
1476
- );
1477
- return { placeholders, isPlural: state.isPlural, valid: true };
1478
- } catch {
1479
- return INVALID;
1480
- }
1481
- }
1482
- function extractPlaceholders(value) {
1483
- return analyzeIcuValue(value).placeholders;
1484
- }
1485
- function validateMessage(value) {
1486
- return analyzeIcuValue(value).valid;
1487
- }
1488
- function computeInvalidIcuKeys(entries) {
1489
- const invalid = [];
1490
- for (const [key, entry] of entries) {
1491
- if (!validateMessage(entry.value)) {
1492
- invalid.push(key);
1493
- }
1494
- }
1495
- return invalid;
1496
- }
1497
1663
  function createNextIntlJsonAdapter() {
1498
1664
  return createJsonFileAdapter({
1499
1665
  format: "next-intl-json",
1500
- extractPlaceholders,
1501
- deriveEntry: (_key, value) => {
1502
- const analysis = analyzeIcuValue(value);
1503
- return { placeholders: analysis.placeholders, isPlural: analysis.isPlural };
1504
- },
1505
- computeInvalidIcuKeys,
1506
- validateMessage
1666
+ extractPlaceholders: icuPlaceholders,
1667
+ deriveEntry: icuDeriveEntry,
1668
+ computeInvalidIcuKeys: icuInvalidKeys,
1669
+ validateMessage: icuIsValid
1507
1670
  });
1508
1671
  }
1509
1672
  function assertNotMixed(tree) {
@@ -1565,8 +1728,7 @@ function createNgxTranslateJsonAdapter() {
1565
1728
  }),
1566
1729
  validateTree: assertNotMixed,
1567
1730
  buildWriteTree: buildNgxWriteTree,
1568
- // ngx-translate flat style uses dotted keys as path notation, not literal leaves;
1569
- // keep the legacy non-encoding flatten so its flat/nested round-trip is unchanged.
1731
+ // ngx-translate flat style uses dotted keys as path notation, not literal leaves.
1570
1732
  keyMode: "path-notation"
1571
1733
  });
1572
1734
  }
@@ -1642,8 +1804,245 @@ function createVueI18nJsonAdapter() {
1642
1804
  })
1643
1805
  });
1644
1806
  }
1807
+ function toEntries2(content, namespace, parseEntries) {
1808
+ try {
1809
+ return parseEntries(content, namespace);
1810
+ } catch (error) {
1811
+ rethrowStructured(error, "The file could not be parsed.");
1812
+ }
1813
+ }
1814
+ function createFlatFileAdapter(options) {
1815
+ const {
1816
+ format,
1817
+ extensions,
1818
+ sniff,
1819
+ parseEntries,
1820
+ serializeEntries,
1821
+ extractPlaceholders,
1822
+ validateMessage,
1823
+ computeInvalidIcuKeys
1824
+ } = options;
1825
+ return {
1826
+ format,
1827
+ canHandle: buildCanHandle(extensions, sniff),
1828
+ extractPlaceholders,
1829
+ validateMessage: validateMessage ?? (() => true),
1830
+ async read(filePath, locale) {
1831
+ const content = await readFileContent(filePath);
1832
+ const namespace = namespaceOf(filePath);
1833
+ const entries = toEntries2(content, namespace, parseEntries);
1834
+ const resource = { locale, namespace, format, entries };
1835
+ const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
1836
+ return { resource, invalidIcuKeys };
1837
+ },
1838
+ async write(resource, filePath) {
1839
+ const data = await serializeEntries(resource.entries, filePath);
1840
+ await atomicWriteFile(filePath, data);
1841
+ }
1842
+ };
1843
+ }
1844
+ var XLIFF_PATTERN = /<(?:x|g|bx|ex|ph|it|mrk)\b[^>]*>|\{[^{}]+\}/g;
1845
+ function extractXliffPlaceholders(value) {
1846
+ const result = [];
1847
+ for (const match of value.matchAll(XLIFF_PATTERN)) {
1848
+ const token = match[0];
1849
+ if (token !== void 0) {
1850
+ result.push(token);
1851
+ }
1852
+ }
1853
+ return result;
1854
+ }
1855
+ var ELEMENT_NODE = 1;
1856
+ function isElement(node) {
1857
+ return node.nodeType === ELEMENT_NODE;
1858
+ }
1859
+ function elementChildren(parent) {
1860
+ return Array.from(parent.childNodes).filter(isElement);
1861
+ }
1862
+ function childByName(parent, name) {
1863
+ return elementChildren(parent).find((el) => el.localName === name) ?? null;
1864
+ }
1865
+ function collectByTag(root, name) {
1866
+ return Array.from(root.getElementsByTagName(name));
1867
+ }
1868
+ function unitKey(element, index) {
1869
+ return element.getAttribute("id") ?? element.getAttribute("resname") ?? `unit-${index}`;
1870
+ }
1871
+ function onFatal(level) {
1872
+ if (level === "fatalError") {
1873
+ throw new Error("malformed XML");
1874
+ }
1875
+ }
1876
+ function assertNoDoctype(content) {
1877
+ if (/<!DOCTYPE/i.test(content) || /<!ENTITY/i.test(content)) {
1878
+ throw new AdapterError("INVALID_XML", "XLIFF with a DTD or entity declaration is rejected.");
1879
+ }
1880
+ }
1881
+ function parseXml(content) {
1882
+ assertNoDoctype(content);
1883
+ let doc;
1884
+ try {
1885
+ doc = new DOMParser({ onError: onFatal }).parseFromString(content, "text/xml");
1886
+ } catch {
1887
+ throw new AdapterError("INVALID_XML", "The file is not valid XML.");
1888
+ }
1889
+ const root = doc.documentElement;
1890
+ if (root === null || root.localName !== "xliff") {
1891
+ throw new AdapterError("INVALID_STRUCTURE", "The file is not an XLIFF document.");
1892
+ }
1893
+ return { doc, root };
1894
+ }
1895
+ function walkXliff12(root) {
1896
+ const units = [];
1897
+ collectByTag(root, "trans-unit").forEach((tu, index) => {
1898
+ const source = childByName(tu, "source");
1899
+ if (source !== null) {
1900
+ units.push({
1901
+ key: unitKey(tu, index),
1902
+ source,
1903
+ target: childByName(tu, "target"),
1904
+ container: tu
1905
+ });
1906
+ }
1907
+ });
1908
+ return units;
1909
+ }
1910
+ function walkXliff20(root) {
1911
+ const units = [];
1912
+ collectByTag(root, "unit").forEach((unit, index) => {
1913
+ const baseKey = unitKey(unit, index);
1914
+ const segments = elementChildren(unit).filter((el) => el.localName === "segment");
1915
+ segments.forEach((segment, segIndex) => {
1916
+ const source = childByName(segment, "source");
1917
+ if (source !== null) {
1918
+ const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
1919
+ units.push({ key, source, target: childByName(segment, "target"), container: segment });
1920
+ }
1921
+ });
1922
+ });
1923
+ return units;
1924
+ }
1925
+ function walkUnits(root) {
1926
+ const version = root.getAttribute("version") ?? "1.2";
1927
+ return version.startsWith("2") ? walkXliff20(root) : walkXliff12(root);
1928
+ }
1929
+ function innerXml(serializer, element) {
1930
+ return Array.from(element.childNodes).map((node) => serializer.serializeToString(node)).join("");
1931
+ }
1932
+ function unitValue(serializer, unit) {
1933
+ if (unit.target !== null) {
1934
+ const targetXml = innerXml(serializer, unit.target);
1935
+ if (targetXml.trim() !== "") {
1936
+ return targetXml;
1937
+ }
1938
+ }
1939
+ return innerXml(serializer, unit.source);
1940
+ }
1941
+ function parseXliffEntries(content, namespace) {
1942
+ const { root } = parseXml(content);
1943
+ const serializer = new XMLSerializer();
1944
+ const out = /* @__PURE__ */ new Map();
1945
+ for (const unit of walkUnits(root)) {
1946
+ const value = unitValue(serializer, unit);
1947
+ out.set(unit.key, {
1948
+ key: unit.key,
1949
+ namespace,
1950
+ value,
1951
+ placeholders: extractXliffPlaceholders(value),
1952
+ isPlural: false
1953
+ });
1954
+ }
1955
+ return out;
1956
+ }
1957
+ async function readDestination(filePath) {
1958
+ let outcome;
1959
+ try {
1960
+ outcome = await readBounded2(filePath);
1961
+ } catch {
1962
+ throw new AdapterError("INVALID_STRUCTURE", "The destination XLIFF file does not exist.");
1963
+ }
1964
+ if (outcome.kind === "not-a-file") {
1965
+ throw new AdapterError("INVALID_STRUCTURE", "The destination path is not a regular file.");
1966
+ }
1967
+ if (outcome.kind === "too-large") {
1968
+ throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
1969
+ }
1970
+ return outcome.content;
1971
+ }
1972
+ function fragmentNodes(parser, value) {
1973
+ try {
1974
+ const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
1975
+ return root === null ? null : Array.from(root.childNodes);
1976
+ } catch {
1977
+ return null;
1978
+ }
1979
+ }
1980
+ function setTargetValue(doc, parser, element, value) {
1981
+ while (element.firstChild !== null) {
1982
+ element.removeChild(element.firstChild);
1983
+ }
1984
+ const nodes = fragmentNodes(parser, value);
1985
+ if (nodes === null) {
1986
+ element.textContent = value;
1987
+ return;
1988
+ }
1989
+ for (const node of nodes) {
1990
+ element.appendChild(doc.importNode(node, true));
1991
+ }
1992
+ }
1993
+ async function serializeXliffEntries(entries, filePath) {
1994
+ const { doc, root } = parseXml(await readDestination(filePath));
1995
+ const parser = new DOMParser({ onError: onFatal });
1996
+ for (const unit of walkUnits(root)) {
1997
+ const entry = entries.get(unit.key);
1998
+ if (entry !== void 0) {
1999
+ const target = unit.target ?? doc.createElement("target");
2000
+ if (unit.target === null) {
2001
+ unit.container.appendChild(target);
2002
+ }
2003
+ setTargetValue(doc, parser, target, entry.value);
2004
+ }
2005
+ }
2006
+ return new XMLSerializer().serializeToString(doc);
2007
+ }
2008
+ function sniffXliff(sample) {
2009
+ const head = sample.trimStart();
2010
+ return head.startsWith("<xliff") || head.startsWith("<?xml");
2011
+ }
2012
+ function createXliffAdapter() {
2013
+ return createFlatFileAdapter({
2014
+ format: "xliff",
2015
+ extensions: [".xlf", ".xliff"],
2016
+ sniff: sniffXliff,
2017
+ parseEntries: parseXliffEntries,
2018
+ serializeEntries: serializeXliffEntries,
2019
+ extractPlaceholders: extractXliffPlaceholders
2020
+ });
2021
+ }
2022
+ function parseYamlObject(content) {
2023
+ let parsed;
2024
+ try {
2025
+ parsed = parse(content, { maxAliasCount: 100 });
2026
+ } catch {
2027
+ throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
2028
+ }
2029
+ return assertJsonRecord(parsed);
2030
+ }
2031
+ function createYamlAdapter() {
2032
+ return createTreeFileAdapter({
2033
+ format: "yaml",
2034
+ extensions: [".yml", ".yaml"],
2035
+ parse: parseYamlObject,
2036
+ serialize: (tree) => stringify(tree),
2037
+ extractPlaceholders: extractDoubleBracePlaceholders,
2038
+ deriveEntry: (_key, value) => ({
2039
+ placeholders: extractDoubleBracePlaceholders(value),
2040
+ isPlural: false
2041
+ })
2042
+ });
2043
+ }
1645
2044
  function createDefaultRegistry() {
1646
- return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter());
2045
+ return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter());
1647
2046
  }
1648
2047
 
1649
2048
  // src/selection/select-adapter.ts
@@ -1658,6 +2057,89 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
1658
2057
  );
1659
2058
  }
1660
2059
 
2060
+ // src/flow/source.ts
2061
+ async function readSource(config, cwd, fs, adapter) {
2062
+ const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
2063
+ if (!await fs.fileExists(sourcePath)) {
2064
+ throw new SdkError(
2065
+ "SOURCE_UNREADABLE",
2066
+ `The source locale file was not found at ${sourcePath}.`
2067
+ );
2068
+ }
2069
+ try {
2070
+ return await adapter.read(sourcePath, config.sourceLocale);
2071
+ } catch (error) {
2072
+ const detail = error instanceof Error ? error.message : String(error);
2073
+ throw new SdkError(
2074
+ "SOURCE_INVALID",
2075
+ `The source locale file at ${sourcePath} could not be read: ${detail}`
2076
+ );
2077
+ }
2078
+ }
2079
+
2080
+ // src/flow/diff-locales.ts
2081
+ async function readTarget(cwd, config, adapter, fs, locale) {
2082
+ const path = localeFilePath(cwd, config.files.pattern, locale);
2083
+ if (!await fs.fileExists(path)) {
2084
+ return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
2085
+ }
2086
+ return (await adapter.read(path, locale)).resource;
2087
+ }
2088
+ function selectedLocales(config, requested) {
2089
+ if (requested === void 0) {
2090
+ return config.targetLocales;
2091
+ }
2092
+ const wanted = new Set(requested);
2093
+ return config.targetLocales.filter((locale) => wanted.has(locale));
2094
+ }
2095
+ async function diffLocales(input, deps = {}) {
2096
+ const config = input.config;
2097
+ const cwd = input.cwd ?? process.cwd();
2098
+ const fs = deps.fs ?? defaultFs;
2099
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
2100
+ const source = await readSource(config, cwd, fs, adapter);
2101
+ const lock = await readLockFile(lockFilePath(cwd), fs);
2102
+ return Promise.all(
2103
+ selectedLocales(config, input.locales).map(async (locale) => {
2104
+ const target = await readTarget(cwd, config, adapter, fs, locale);
2105
+ const diff2 = diffResources(source.resource, target, { baseline: baselineFor(lock, locale) });
2106
+ return { locale, diff: diff2 };
2107
+ })
2108
+ );
2109
+ }
2110
+
2111
+ // src/flow/check.ts
2112
+ function toCheckSummary(locale, diff2) {
2113
+ return {
2114
+ locale,
2115
+ missing: diff2.missing.length,
2116
+ stale: diff2.changed.length,
2117
+ upToDate: diff2.unchanged.length,
2118
+ inSync: diff2.missing.length === 0 && diff2.changed.length === 0
2119
+ };
2120
+ }
2121
+ async function check(input, deps = {}) {
2122
+ const results = await diffLocales(input, deps);
2123
+ const locales = results.map(({ locale, diff: diff2 }) => toCheckSummary(locale, diff2));
2124
+ return { inSync: locales.every((entry) => entry.inSync), locales };
2125
+ }
2126
+
2127
+ // src/flow/diff.ts
2128
+ function toLocaleDiff(locale, diff2) {
2129
+ return {
2130
+ locale,
2131
+ missing: diff2.missing,
2132
+ changed: diff2.changed,
2133
+ orphaned: diff2.orphaned,
2134
+ hasPendingChanges: diff2.missing.length > 0 || diff2.changed.length > 0
2135
+ };
2136
+ }
2137
+ async function diff(input, deps = {}) {
2138
+ const results = await diffLocales(input, deps);
2139
+ const locales = results.map(({ locale, diff: result }) => toLocaleDiff(locale, result));
2140
+ return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
2141
+ }
2142
+
1661
2143
  // src/selection/select-provider.ts
1662
2144
  function selectProvider(config, createProvider = buildProvider) {
1663
2145
  try {
@@ -1908,7 +2390,7 @@ async function generatePluralForms(context) {
1908
2390
  function emptyResource(locale, format) {
1909
2391
  return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
1910
2392
  }
1911
- async function readTarget(params) {
2393
+ async function readTarget2(params) {
1912
2394
  const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
1913
2395
  if (!await params.fs.fileExists(path)) {
1914
2396
  return emptyResource(params.targetLocale, params.format);
@@ -1926,12 +2408,12 @@ function buildRequest3(params, entries) {
1926
2408
  };
1927
2409
  }
1928
2410
  async function runLocale(params) {
1929
- const target = await readTarget(params);
1930
- const diff = diffResources(params.source, target, { baseline: params.baseline });
1931
- const orphaned = params.generatePlurals ? diff.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff.orphaned;
2411
+ const target = await readTarget2(params);
2412
+ const diff2 = diffResources(params.source, target, { baseline: params.baseline });
2413
+ const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
1932
2414
  const pruned = params.prune ? orphaned : [];
1933
2415
  const invalidIcu = new Set(params.sourceInvalidIcuKeys);
1934
- const candidates = [...diff.missing, ...diff.changed];
2416
+ const candidates = [...diff2.missing, ...diff2.changed];
1935
2417
  const toTranslate = candidates.filter((key) => !invalidIcu.has(key));
1936
2418
  const invalidIcuSource = candidates.filter((key) => invalidIcu.has(key));
1937
2419
  const pluralNotice = detectMissingPluralCategories(
@@ -1945,7 +2427,7 @@ async function runLocale(params) {
1945
2427
  return {
1946
2428
  summary: baseSummary({
1947
2429
  locale: params.targetLocale,
1948
- unchanged: diff.unchanged,
2430
+ unchanged: diff2.unchanged,
1949
2431
  orphaned,
1950
2432
  invalidIcuSource,
1951
2433
  translated: toTranslate,
@@ -1994,12 +2476,12 @@ async function runLocale(params) {
1994
2476
  return {
1995
2477
  summary: baseSummary({
1996
2478
  locale: params.targetLocale,
1997
- unchanged: diff.unchanged,
2479
+ unchanged: diff2.unchanged,
1998
2480
  orphaned,
1999
2481
  invalidIcuSource,
2000
2482
  translated: [...accepted.keys()],
2001
2483
  generated: generation.accepted.map((form) => form.targetKey).sort(),
2002
- // Withheld generated forms surface alongside withheld translations (spec D4): both failed integrity.
2484
+ // Withheld generated forms surface alongside withheld translations: both failed integrity.
2003
2485
  integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
2004
2486
  pruned,
2005
2487
  notices
@@ -2123,26 +2605,6 @@ function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
2123
2605
  }
2124
2606
  }
2125
2607
 
2126
- // src/flow/source.ts
2127
- async function readSource(config, cwd, fs, adapter) {
2128
- const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
2129
- if (!await fs.fileExists(sourcePath)) {
2130
- throw new SdkError(
2131
- "SOURCE_UNREADABLE",
2132
- `The source locale file was not found at ${sourcePath}.`
2133
- );
2134
- }
2135
- try {
2136
- return await adapter.read(sourcePath, config.sourceLocale);
2137
- } catch (error) {
2138
- const detail = error instanceof Error ? error.message : String(error);
2139
- throw new SdkError(
2140
- "SOURCE_INVALID",
2141
- `The source locale file at ${sourcePath} could not be read: ${detail}`
2142
- );
2143
- }
2144
- }
2145
-
2146
2608
  // src/flow/translate-project.ts
2147
2609
  async function translate2(input, deps = {}) {
2148
2610
  const config = input.config;
@@ -2351,7 +2813,7 @@ var DEFAULT_WORKBOOK_LIMITS = {
2351
2813
  maxRowsPerSheet: 1e5,
2352
2814
  maxCellsPerRow: 64
2353
2815
  };
2354
- function assertNoDoctype(name, xml) {
2816
+ function assertNoDoctype2(name, xml) {
2355
2817
  if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
2356
2818
  throw new ExchangeError(
2357
2819
  "WORKBOOK_INVALID",
@@ -2406,7 +2868,7 @@ async function guardWorkbookBytes(bytes, limits) {
2406
2868
  `The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
2407
2869
  );
2408
2870
  }
2409
- assertNoDoctype(file.name, content);
2871
+ assertNoDoctype2(file.name, content);
2410
2872
  }
2411
2873
  }
2412
2874
  var rowSchema = z.object({
@@ -2515,7 +2977,7 @@ async function readWorkbook(bytes, options = {}) {
2515
2977
 
2516
2978
  // src/flow/workbook/export-workbook.ts
2517
2979
  var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
2518
- async function readTarget2(cwd, config, adapter, fs, locale) {
2980
+ async function readTarget3(cwd, config, adapter, fs, locale) {
2519
2981
  const path = localeFilePath(cwd, config.files.pattern, locale);
2520
2982
  if (!await fs.fileExists(path)) {
2521
2983
  return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
@@ -2523,7 +2985,7 @@ async function readTarget2(cwd, config, adapter, fs, locale) {
2523
2985
  return (await adapter.read(path, locale)).resource;
2524
2986
  }
2525
2987
  function buildRows(source, target, baseline, includeUnchanged) {
2526
- const diff = diffResources(source, target, { baseline });
2988
+ const diff2 = diffResources(source, target, { baseline });
2527
2989
  const rows = [];
2528
2990
  const add = (keys, status) => {
2529
2991
  for (const key of keys) {
@@ -2541,14 +3003,14 @@ function buildRows(source, target, baseline, includeUnchanged) {
2541
3003
  });
2542
3004
  }
2543
3005
  };
2544
- add(diff.missing, "new");
2545
- add(diff.changed, "changed");
3006
+ add(diff2.missing, "new");
3007
+ add(diff2.changed, "changed");
2546
3008
  if (includeUnchanged) {
2547
- add(diff.unchanged, "changed");
3009
+ add(diff2.unchanged, "changed");
2548
3010
  }
2549
3011
  return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
2550
3012
  }
2551
- function selectedLocales(config, requested) {
3013
+ function selectedLocales2(config, requested) {
2552
3014
  if (requested === void 0) {
2553
3015
  return config.targetLocales;
2554
3016
  }
@@ -2562,10 +3024,10 @@ async function exportWorkbook(input, deps = {}) {
2562
3024
  const adapter = selectAdapter(config.format, deps.adapterRegistry);
2563
3025
  const source = await readSource(config, cwd, fs, adapter);
2564
3026
  const lock = await readLockFile(lockFilePath(cwd), fs);
2565
- const locales = selectedLocales(config, input.locales);
3027
+ const locales = selectedLocales2(config, input.locales);
2566
3028
  const sheets = await Promise.all(
2567
3029
  locales.map(async (locale) => {
2568
- const target = await readTarget2(cwd, config, adapter, fs, locale);
3030
+ const target = await readTarget3(cwd, config, adapter, fs, locale);
2569
3031
  const rows = buildRows(
2570
3032
  source.resource,
2571
3033
  target,
@@ -2635,7 +3097,7 @@ function classifyRows(params, buckets) {
2635
3097
  }
2636
3098
  }
2637
3099
  function importLocale(params) {
2638
- const diff = diffResources(params.source, params.target, { baseline: params.baseline });
3100
+ const diff2 = diffResources(params.source, params.target, { baseline: params.baseline });
2639
3101
  const buckets = { accepted: /* @__PURE__ */ new Map(), mismatches: [], withheld: /* @__PURE__ */ new Set() };
2640
3102
  classifyRows(params, buckets);
2641
3103
  const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
@@ -2644,8 +3106,8 @@ function importLocale(params) {
2644
3106
  locale: params.sheet.locale,
2645
3107
  status: "succeeded",
2646
3108
  translated: [...buckets.accepted.keys()].sort(),
2647
- unchanged: diff.unchanged,
2648
- orphaned: diff.orphaned,
3109
+ unchanged: diff2.unchanged,
3110
+ orphaned: diff2.orphaned,
2649
3111
  // Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
2650
3112
  pruned: [],
2651
3113
  invalidIcuSource,
@@ -2672,7 +3134,7 @@ async function readWorkbookBytes(path, fs) {
2672
3134
  }
2673
3135
  return read.bytes;
2674
3136
  }
2675
- async function readTarget3(cwd, config, adapter, fs, locale) {
3137
+ async function readTarget4(cwd, config, adapter, fs, locale) {
2676
3138
  const path = localeFilePath(cwd, config.files.pattern, locale);
2677
3139
  if (!await fs.fileExists(path)) {
2678
3140
  return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
@@ -2711,7 +3173,7 @@ async function runSheet(ctx, sheet, lock) {
2711
3173
  `The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
2712
3174
  );
2713
3175
  }
2714
- const target = await readTarget3(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
3176
+ const target = await readTarget4(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
2715
3177
  const baseline = baselineFor(lock, sheet.locale);
2716
3178
  const { summary, accepted, withheld } = importLocale({
2717
3179
  sheet,
@@ -2781,6 +3243,16 @@ async function importWorkbook(input, deps = {}) {
2781
3243
  const { succeeded, failed } = partition(summaries);
2782
3244
  return { dryRun, locales: summaries, succeeded, failed };
2783
3245
  }
3246
+
3247
+ // src/scaffolding.ts
3248
+ var scaffoldingMetadata = {
3249
+ /** Provider id -> the environment variable its API key is read from. Owned by ai-providers. */
3250
+ providerEnv: PROVIDER_ENV,
3251
+ /** LLM provider id -> a cosmetic default scaffold model. Owned by ai-providers. DeepL has none. */
3252
+ scaffoldModels: SCAFFOLD_MODELS,
3253
+ /** The closed set of source format ids. Owned by core. */
3254
+ supportedFormats: SUPPORTED_FORMATS
3255
+ };
2784
3256
  var defaultCreateWatcher = (paths) => {
2785
3257
  const fsWatcher = watch$1([...paths], { persistent: true, ignoreInitial: true });
2786
3258
  return {
@@ -2887,6 +3359,6 @@ async function watch(input, deps = {}) {
2887
3359
  return { stop };
2888
3360
  }
2889
3361
 
2890
- export { DEFAULT_WORKBOOK_PATH, SdkError, defineConfig, exportWorkbook, importWorkbook, loadConfig, translate2 as translate, verbatraConfigSchema, watch };
3362
+ export { DEFAULT_WORKBOOK_PATH, SdkError, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
2891
3363
  //# sourceMappingURL=index.js.map
2892
3364
  //# sourceMappingURL=index.js.map