@verbatra/sdk 0.5.0-next.2 → 0.5.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -777,6 +777,50 @@ function createDefaultClient2() {
777
777
  };
778
778
  return { client, freeAccount };
779
779
  }
780
+ var DEEPL_MAX_TEXTS_PER_REQUEST = 50;
781
+ var DEEPL_MAX_PAYLOAD_BYTES = 128 * 1024;
782
+ var DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES = 4 * 1024;
783
+ var DEEPL_MAX_TEXT_PAYLOAD_BYTES = DEEPL_MAX_PAYLOAD_BYTES - DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES;
784
+ function estimateWireBytes(text) {
785
+ return encodeURIComponent(text).length;
786
+ }
787
+ function chunkTextsForDeepL(texts) {
788
+ const chunks = [];
789
+ let current = [];
790
+ let currentBytes = 0;
791
+ for (const text of texts) {
792
+ const textBytes = estimateWireBytes(text);
793
+ const startsNewChunk = current.length > 0 && (current.length >= DEEPL_MAX_TEXTS_PER_REQUEST || currentBytes + textBytes > DEEPL_MAX_TEXT_PAYLOAD_BYTES);
794
+ if (startsNewChunk) {
795
+ chunks.push(current);
796
+ current = [];
797
+ currentBytes = 0;
798
+ }
799
+ current.push(text);
800
+ currentBytes += textBytes;
801
+ }
802
+ if (current.length > 0) {
803
+ chunks.push(current);
804
+ }
805
+ return chunks;
806
+ }
807
+ var DEPRECATED_BARE_TARGET_CODES = /* @__PURE__ */ new Set(["en", "pt"]);
808
+ function assertValidDeepLSourceLocale(locale) {
809
+ if (locale.includes("-")) {
810
+ throw new ProviderError(
811
+ "INVALID_REQUEST",
812
+ `DeepL does not accept a regional or script source locale code: "${locale}". Only the base language code is valid as a DeepL source (for example, use "en" instead of "en-US").`
813
+ );
814
+ }
815
+ }
816
+ function assertValidDeepLTargetLocale(locale) {
817
+ if (DEPRECATED_BARE_TARGET_CODES.has(locale.toLowerCase())) {
818
+ throw new ProviderError(
819
+ "INVALID_REQUEST",
820
+ `DeepL requires a disambiguated target locale code instead of "${locale}" (for example, "en-GB" or "en-US" for English, "pt-PT" or "pt-BR" for Portuguese).`
821
+ );
822
+ }
823
+ }
780
824
  var PLACEHOLDER_UNSUPPORTED_MESSAGE = "Some entries contain placeholders or ICU syntax that DeepL cannot preserve; they were left untranslated. Use an LLM provider to translate placeholder-bearing strings.";
781
825
  function partitionByPlaceholders(entries) {
782
826
  const protectable = [];
@@ -856,6 +900,8 @@ function resolveClient(deps) {
856
900
  }
857
901
  async function translate(bundle, config, request) {
858
902
  const data = validateRequest(request);
903
+ assertValidDeepLSourceLocale(data.sourceLocale);
904
+ assertValidDeepLTargetLocale(data.targetLocale);
859
905
  const { protectable, unprotectable } = partitionByPlaceholders(data.entries);
860
906
  const genericGlossarySupplied = request.glossary !== void 0 && Object.keys(request.glossary).length > 0;
861
907
  const { options, notices } = buildTranslateOptions({
@@ -883,7 +929,7 @@ async function translateProtectable(client, data, protectable, options, extract,
883
929
  return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
884
930
  }
885
931
  const texts = protectable.map((entry) => entry.value);
886
- const results = await callClient2(
932
+ const results = await callClientChunked(
887
933
  client,
888
934
  texts,
889
935
  data.sourceLocale,
@@ -901,6 +947,14 @@ function callClient2(client, texts, sourceLang, targetLang, options, signal) {
901
947
  signal
902
948
  );
903
949
  }
950
+ async function callClientChunked(client, texts, sourceLang, targetLang, options, signal) {
951
+ const results = [];
952
+ for (const chunk2 of chunkTextsForDeepL(texts)) {
953
+ const chunkResults = await callClient2(client, chunk2, sourceLang, targetLang, options, signal);
954
+ results.push(...chunkResults);
955
+ }
956
+ return results;
957
+ }
904
958
  var geminiConfigSchema = z.object({
905
959
  model: z.string().min(1),
906
960
  maxOutputTokens: z.number().int().positive()
@@ -1372,6 +1426,17 @@ function buildProvider(config) {
1372
1426
 
1373
1427
  // src/config/schema.ts
1374
1428
  var DEFAULT_MAX_BATCH_SIZE = 50;
1429
+ function findCaseInsensitiveDuplicate(locales) {
1430
+ const seen = /* @__PURE__ */ new Set();
1431
+ for (const locale of locales) {
1432
+ const key = locale.toLowerCase();
1433
+ if (seen.has(key)) {
1434
+ return locale;
1435
+ }
1436
+ seen.add(key);
1437
+ }
1438
+ return void 0;
1439
+ }
1375
1440
  var verbatraConfigSchema = z.strictObject({
1376
1441
  sourceLocale: z.string().min(1),
1377
1442
  targetLocales: z.array(z.string().min(1)).min(1),
@@ -1413,6 +1478,14 @@ var verbatraConfigSchema = z.strictObject({
1413
1478
  }).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
1414
1479
  message: "targetLocales must not include the source locale",
1415
1480
  path: ["targetLocales"]
1481
+ }).refine((config) => findCaseInsensitiveDuplicate(config.targetLocales) === void 0, {
1482
+ error: (issue) => {
1483
+ const duplicate = findCaseInsensitiveDuplicate(
1484
+ issue.input.targetLocales
1485
+ );
1486
+ return `targetLocales must not contain case-insensitively duplicate locales: "${duplicate}"`;
1487
+ },
1488
+ path: ["targetLocales"]
1416
1489
  }).refine((config) => config.files.pattern.includes(LOCALE_TOKEN), {
1417
1490
  message: `files.pattern must contain the ${LOCALE_TOKEN} token`,
1418
1491
  path: ["files", "pattern"]
@@ -2128,11 +2201,24 @@ function unflattenEntries(entries) {
2128
2201
  }
2129
2202
  return root;
2130
2203
  }
2131
- function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree) {
2204
+ function mergeDescriptions(entries, content, deriveDescriptions) {
2205
+ if (!deriveDescriptions) {
2206
+ return;
2207
+ }
2208
+ for (const [key, description] of deriveDescriptions(content)) {
2209
+ const entry = entries.get(key);
2210
+ if (entry !== void 0) {
2211
+ entries.set(key, { ...entry, description });
2212
+ }
2213
+ }
2214
+ }
2215
+ function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree, deriveDescriptions) {
2132
2216
  try {
2133
2217
  const tree = parse3(content);
2134
2218
  validateTree?.(tree);
2135
- return flattenTree(tree, namespace, deriveEntry, keyMode);
2219
+ const entries = flattenTree(tree, namespace, deriveEntry, keyMode);
2220
+ mergeDescriptions(entries, content, deriveDescriptions);
2221
+ return entries;
2136
2222
  } catch (error) {
2137
2223
  rethrowStructured(error, "The file could not be parsed.");
2138
2224
  }
@@ -2151,6 +2237,7 @@ function createTreeFileAdapter(options) {
2151
2237
  validateTree,
2152
2238
  buildWriteTree,
2153
2239
  comparePlaceholders,
2240
+ deriveDescriptions,
2154
2241
  keyMode = "literal-leaf"
2155
2242
  } = options;
2156
2243
  return {
@@ -2162,7 +2249,15 @@ function createTreeFileAdapter(options) {
2162
2249
  async read(filePath, locale) {
2163
2250
  const content = await readFileContent(filePath);
2164
2251
  const namespace = namespaceOf(filePath);
2165
- const entries = toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree);
2252
+ const entries = toEntries(
2253
+ content,
2254
+ namespace,
2255
+ parse3,
2256
+ deriveEntry,
2257
+ keyMode,
2258
+ validateTree,
2259
+ deriveDescriptions
2260
+ );
2166
2261
  const resource = { locale, namespace, format, entries };
2167
2262
  const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
2168
2263
  return { resource, invalidIcuKeys };
@@ -2203,6 +2298,31 @@ function stripArbMetadata(tree) {
2203
2298
  function originalKey(encoded) {
2204
2299
  return decodeKeyToSegments(encoded).join(".");
2205
2300
  }
2301
+ function messageKeyForMetadata(key) {
2302
+ return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
2303
+ }
2304
+ function descriptionOf(value) {
2305
+ if (typeof value !== "object" || value === null) {
2306
+ return void 0;
2307
+ }
2308
+ const description = value.description;
2309
+ return typeof description === "string" ? description : void 0;
2310
+ }
2311
+ function extractArbDescriptions(content) {
2312
+ const tree = parseArbObject(content);
2313
+ const out = /* @__PURE__ */ new Map();
2314
+ for (const [key, value] of Object.entries(tree)) {
2315
+ const messageKey = messageKeyForMetadata(key);
2316
+ if (messageKey === null) {
2317
+ continue;
2318
+ }
2319
+ const description = descriptionOf(value);
2320
+ if (description !== void 0) {
2321
+ out.set(encodeSegment(messageKey), description);
2322
+ }
2323
+ }
2324
+ return out;
2325
+ }
2206
2326
  function messagesFromEntries(entries) {
2207
2327
  const out = /* @__PURE__ */ new Map();
2208
2328
  for (const [key, entry] of entries) {
@@ -2259,7 +2379,8 @@ function createArbAdapter() {
2259
2379
  computeInvalidIcuKeys: icuInvalidKeys,
2260
2380
  validateMessage: icuIsValid,
2261
2381
  comparePlaceholders: compareIcuPlaceholders,
2262
- buildWriteTree: buildArbWriteTree
2382
+ buildWriteTree: buildArbWriteTree,
2383
+ deriveDescriptions: extractArbDescriptions
2263
2384
  });
2264
2385
  }
2265
2386
  function createJsonFileAdapter(options) {
@@ -2565,16 +2686,33 @@ function parseXml(content) {
2565
2686
  }
2566
2687
  return { doc, root };
2567
2688
  }
2689
+ function firstNoteText(parent) {
2690
+ const note = childByName(parent, "note");
2691
+ if (note === null) {
2692
+ return void 0;
2693
+ }
2694
+ const text = (note.textContent ?? "").trim();
2695
+ return text === "" ? void 0 : text;
2696
+ }
2697
+ function transUnitDescription(tu) {
2698
+ return firstNoteText(tu);
2699
+ }
2700
+ function unitDescription(unit) {
2701
+ const notes = childByName(unit, "notes");
2702
+ return notes === null ? void 0 : firstNoteText(notes);
2703
+ }
2568
2704
  function walkXliff12(root) {
2569
2705
  const units = [];
2570
2706
  collectByTag(root, "trans-unit").forEach((tu, index) => {
2571
2707
  const source = childByName(tu, "source");
2572
2708
  if (source !== null) {
2709
+ const description = transUnitDescription(tu);
2573
2710
  units.push({
2574
2711
  key: unitKey(tu, index),
2575
2712
  source,
2576
2713
  target: childByName(tu, "target"),
2577
- container: tu
2714
+ container: tu,
2715
+ ...description !== void 0 ? { description } : {}
2578
2716
  });
2579
2717
  }
2580
2718
  });
@@ -2584,12 +2722,19 @@ function walkXliff20(root) {
2584
2722
  const units = [];
2585
2723
  collectByTag(root, "unit").forEach((unit, index) => {
2586
2724
  const baseKey = unitKey(unit, index);
2725
+ const description = unitDescription(unit);
2587
2726
  const segments = elementChildren(unit).filter((el) => el.localName === "segment");
2588
2727
  segments.forEach((segment, segIndex) => {
2589
2728
  const source = childByName(segment, "source");
2590
2729
  if (source !== null) {
2591
2730
  const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
2592
- units.push({ key, source, target: childByName(segment, "target"), container: segment });
2731
+ units.push({
2732
+ key,
2733
+ source,
2734
+ target: childByName(segment, "target"),
2735
+ container: segment,
2736
+ ...description !== void 0 ? { description } : {}
2737
+ });
2593
2738
  }
2594
2739
  });
2595
2740
  });
@@ -2628,7 +2773,8 @@ function parseXliffEntries(content, namespace) {
2628
2773
  namespace,
2629
2774
  value,
2630
2775
  placeholders: extractXliffPlaceholders(value),
2631
- isPlural: false
2776
+ isPlural: false,
2777
+ ...unit.description !== void 0 ? { description: unit.description } : {}
2632
2778
  });
2633
2779
  }
2634
2780
  return out;
@@ -3519,10 +3665,13 @@ var INSTRUCTIONS_LINES = [
3519
3665
  " structure and the argument names exactly. Translate only the human-readable text.",
3520
3666
  "6. Do not edit the 'Key' column or the hidden 'Source hash' column. They map your translation",
3521
3667
  " back to the right string. You may sort or filter rows freely; mapping does not depend on row order.",
3668
+ "7. The 'Context' column, when present, is reference text from the source file explaining what the",
3669
+ " string means or where it is used. It is read-only; leave it as-is.",
3522
3670
  "",
3523
3671
  "Status values:",
3524
3672
  " new - this string has no translation yet.",
3525
3673
  " changed - the source string changed and the translation needs updating.",
3674
+ " unchanged - already up to date, included for reference. No action needed.",
3526
3675
  "",
3527
3676
  "When you are done, save the file and send it back. verbatra checks every value on import:",
3528
3677
  "a value with a broken placeholder, invalid ICU, or a source that changed since export is",
@@ -3534,7 +3683,8 @@ var COLUMN = {
3534
3683
  current: 3,
3535
3684
  status: 4,
3536
3685
  translation: 5,
3537
- sourceHash: 6
3686
+ sourceHash: 6,
3687
+ context: 7
3538
3688
  };
3539
3689
  var HEADERS = [
3540
3690
  "Key",
@@ -3542,7 +3692,8 @@ var HEADERS = [
3542
3692
  "Current translation",
3543
3693
  "Status",
3544
3694
  "Translation",
3545
- "Source hash"
3695
+ "Source hash",
3696
+ "Context"
3546
3697
  ];
3547
3698
  var HEADER_ROW = 1;
3548
3699
  var INSTRUCTIONS_SHEET_NAME = "Instructions";
@@ -3561,7 +3712,8 @@ var COLUMN_WIDTHS = {
3561
3712
  [COLUMN.source]: 50,
3562
3713
  [COLUMN.current]: 50,
3563
3714
  [COLUMN.status]: 12,
3564
- [COLUMN.translation]: 50
3715
+ [COLUMN.translation]: 50,
3716
+ [COLUMN.context]: 50
3565
3717
  };
3566
3718
  var TEXT_NUMBER_FORMAT = "@";
3567
3719
  function styleHeader(sheet) {
@@ -3592,7 +3744,8 @@ function writeRow(sheet, sheetRow) {
3592
3744
  translationCell.numFmt = TEXT_NUMBER_FORMAT;
3593
3745
  translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
3594
3746
  row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
3595
- for (let column = COLUMN.key; column <= COLUMN.sourceHash; column += 1) {
3747
+ row.getCell(COLUMN.context).value = sheetRow.context;
3748
+ for (let column = COLUMN.key; column <= COLUMN.context; column += 1) {
3596
3749
  const cell = row.getCell(column);
3597
3750
  cell.protection = { locked: column !== COLUMN.translation };
3598
3751
  if (column !== COLUMN.translation) {
@@ -3617,6 +3770,26 @@ function assertValidWorksheetName(locale) {
3617
3770
  );
3618
3771
  }
3619
3772
  }
3773
+ function assertNoWorksheetNameCollisions(sheets) {
3774
+ const reservedKey = INSTRUCTIONS_SHEET_NAME.toLowerCase();
3775
+ const seen = /* @__PURE__ */ new Set();
3776
+ for (const sheet of sheets) {
3777
+ const key = sheet.locale.toLowerCase();
3778
+ if (key === reservedKey) {
3779
+ throw new ExchangeError(
3780
+ "WORKBOOK_INVALID",
3781
+ `The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with the reserved "${INSTRUCTIONS_SHEET_NAME}" sheet.`
3782
+ );
3783
+ }
3784
+ if (seen.has(key)) {
3785
+ throw new ExchangeError(
3786
+ "WORKBOOK_INVALID",
3787
+ `The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with another target locale's sheet name.`
3788
+ );
3789
+ }
3790
+ seen.add(key);
3791
+ }
3792
+ }
3620
3793
  async function buildDataSheet(workbook, sheet) {
3621
3794
  assertValidWorksheetName(sheet.locale);
3622
3795
  const worksheet = workbook.addWorksheet(sheet.locale);
@@ -3643,6 +3816,7 @@ function buildInstructionsSheet(workbook) {
3643
3816
  sheet.getRow(1).font = { bold: true };
3644
3817
  }
3645
3818
  async function buildWorkbook(model) {
3819
+ assertNoWorksheetNameCollisions(model.sheets);
3646
3820
  const workbook = new ExcelJS.Workbook();
3647
3821
  buildInstructionsSheet(workbook);
3648
3822
  for (const sheet of model.sheets) {
@@ -3743,9 +3917,10 @@ var rowSchema = z.object({
3743
3917
  key: z.string().min(1),
3744
3918
  source: z.string(),
3745
3919
  currentTarget: z.string(),
3746
- status: z.enum(["new", "changed"]),
3920
+ status: z.enum(["new", "changed", "unchanged"]),
3747
3921
  sourceHash: z.string(),
3748
- translation: z.string()
3922
+ translation: z.string(),
3923
+ context: z.string()
3749
3924
  });
3750
3925
  function cellString(cell) {
3751
3926
  const value = cell.value;
@@ -3778,7 +3953,10 @@ function parseRow(sheet, row) {
3778
3953
  currentTarget: cellString(row.getCell(COLUMN.current)),
3779
3954
  status: cellString(row.getCell(COLUMN.status)),
3780
3955
  sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
3781
- translation: cellString(row.getCell(COLUMN.translation))
3956
+ translation: cellString(row.getCell(COLUMN.translation)),
3957
+ // A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
3958
+ // empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
3959
+ context: cellString(row.getCell(COLUMN.context))
3782
3960
  };
3783
3961
  const result = rowSchema.safeParse(candidate);
3784
3962
  if (!result.success) {
@@ -3867,14 +4045,15 @@ function buildRows(source, target, baseline, includeUnchanged) {
3867
4045
  currentTarget: target.entries.get(key)?.value ?? "",
3868
4046
  status,
3869
4047
  sourceHash: contentHash(sourceEntry),
3870
- translation: ""
4048
+ translation: "",
4049
+ context: sourceEntry.description ?? ""
3871
4050
  });
3872
4051
  }
3873
4052
  };
3874
4053
  add(diff2.missing, "new");
3875
4054
  add(diff2.changed, "changed");
3876
4055
  if (includeUnchanged) {
3877
- add(diff2.unchanged, "changed");
4056
+ add(diff2.unchanged, "unchanged");
3878
4057
  }
3879
4058
  return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
3880
4059
  }