@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.cjs CHANGED
@@ -807,6 +807,50 @@ function createDefaultClient2() {
807
807
  };
808
808
  return { client, freeAccount };
809
809
  }
810
+ var DEEPL_MAX_TEXTS_PER_REQUEST = 50;
811
+ var DEEPL_MAX_PAYLOAD_BYTES = 128 * 1024;
812
+ var DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES = 4 * 1024;
813
+ var DEEPL_MAX_TEXT_PAYLOAD_BYTES = DEEPL_MAX_PAYLOAD_BYTES - DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES;
814
+ function estimateWireBytes(text) {
815
+ return encodeURIComponent(text).length;
816
+ }
817
+ function chunkTextsForDeepL(texts) {
818
+ const chunks = [];
819
+ let current = [];
820
+ let currentBytes = 0;
821
+ for (const text of texts) {
822
+ const textBytes = estimateWireBytes(text);
823
+ const startsNewChunk = current.length > 0 && (current.length >= DEEPL_MAX_TEXTS_PER_REQUEST || currentBytes + textBytes > DEEPL_MAX_TEXT_PAYLOAD_BYTES);
824
+ if (startsNewChunk) {
825
+ chunks.push(current);
826
+ current = [];
827
+ currentBytes = 0;
828
+ }
829
+ current.push(text);
830
+ currentBytes += textBytes;
831
+ }
832
+ if (current.length > 0) {
833
+ chunks.push(current);
834
+ }
835
+ return chunks;
836
+ }
837
+ var DEPRECATED_BARE_TARGET_CODES = /* @__PURE__ */ new Set(["en", "pt"]);
838
+ function assertValidDeepLSourceLocale(locale) {
839
+ if (locale.includes("-")) {
840
+ throw new ProviderError(
841
+ "INVALID_REQUEST",
842
+ `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").`
843
+ );
844
+ }
845
+ }
846
+ function assertValidDeepLTargetLocale(locale) {
847
+ if (DEPRECATED_BARE_TARGET_CODES.has(locale.toLowerCase())) {
848
+ throw new ProviderError(
849
+ "INVALID_REQUEST",
850
+ `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).`
851
+ );
852
+ }
853
+ }
810
854
  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.";
811
855
  function partitionByPlaceholders(entries) {
812
856
  const protectable = [];
@@ -886,6 +930,8 @@ function resolveClient(deps) {
886
930
  }
887
931
  async function translate(bundle, config, request) {
888
932
  const data = validateRequest(request);
933
+ assertValidDeepLSourceLocale(data.sourceLocale);
934
+ assertValidDeepLTargetLocale(data.targetLocale);
889
935
  const { protectable, unprotectable } = partitionByPlaceholders(data.entries);
890
936
  const genericGlossarySupplied = request.glossary !== void 0 && Object.keys(request.glossary).length > 0;
891
937
  const { options, notices } = buildTranslateOptions({
@@ -913,7 +959,7 @@ async function translateProtectable(client, data, protectable, options, extract,
913
959
  return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
914
960
  }
915
961
  const texts = protectable.map((entry) => entry.value);
916
- const results = await callClient2(
962
+ const results = await callClientChunked(
917
963
  client,
918
964
  texts,
919
965
  data.sourceLocale,
@@ -931,6 +977,14 @@ function callClient2(client, texts, sourceLang, targetLang, options, signal) {
931
977
  signal
932
978
  );
933
979
  }
980
+ async function callClientChunked(client, texts, sourceLang, targetLang, options, signal) {
981
+ const results = [];
982
+ for (const chunk2 of chunkTextsForDeepL(texts)) {
983
+ const chunkResults = await callClient2(client, chunk2, sourceLang, targetLang, options, signal);
984
+ results.push(...chunkResults);
985
+ }
986
+ return results;
987
+ }
934
988
  var geminiConfigSchema = zod.z.object({
935
989
  model: zod.z.string().min(1),
936
990
  maxOutputTokens: zod.z.number().int().positive()
@@ -1402,6 +1456,17 @@ function buildProvider(config) {
1402
1456
 
1403
1457
  // src/config/schema.ts
1404
1458
  var DEFAULT_MAX_BATCH_SIZE = 50;
1459
+ function findCaseInsensitiveDuplicate(locales) {
1460
+ const seen = /* @__PURE__ */ new Set();
1461
+ for (const locale of locales) {
1462
+ const key = locale.toLowerCase();
1463
+ if (seen.has(key)) {
1464
+ return locale;
1465
+ }
1466
+ seen.add(key);
1467
+ }
1468
+ return void 0;
1469
+ }
1405
1470
  var verbatraConfigSchema = zod.z.strictObject({
1406
1471
  sourceLocale: zod.z.string().min(1),
1407
1472
  targetLocales: zod.z.array(zod.z.string().min(1)).min(1),
@@ -1443,6 +1508,14 @@ var verbatraConfigSchema = zod.z.strictObject({
1443
1508
  }).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
1444
1509
  message: "targetLocales must not include the source locale",
1445
1510
  path: ["targetLocales"]
1511
+ }).refine((config) => findCaseInsensitiveDuplicate(config.targetLocales) === void 0, {
1512
+ error: (issue) => {
1513
+ const duplicate = findCaseInsensitiveDuplicate(
1514
+ issue.input.targetLocales
1515
+ );
1516
+ return `targetLocales must not contain case-insensitively duplicate locales: "${duplicate}"`;
1517
+ },
1518
+ path: ["targetLocales"]
1446
1519
  }).refine((config) => config.files.pattern.includes(LOCALE_TOKEN), {
1447
1520
  message: `files.pattern must contain the ${LOCALE_TOKEN} token`,
1448
1521
  path: ["files", "pattern"]
@@ -2158,11 +2231,24 @@ function unflattenEntries(entries) {
2158
2231
  }
2159
2232
  return root;
2160
2233
  }
2161
- function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree) {
2234
+ function mergeDescriptions(entries, content, deriveDescriptions) {
2235
+ if (!deriveDescriptions) {
2236
+ return;
2237
+ }
2238
+ for (const [key, description] of deriveDescriptions(content)) {
2239
+ const entry = entries.get(key);
2240
+ if (entry !== void 0) {
2241
+ entries.set(key, { ...entry, description });
2242
+ }
2243
+ }
2244
+ }
2245
+ function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree, deriveDescriptions) {
2162
2246
  try {
2163
2247
  const tree = parse3(content);
2164
2248
  validateTree?.(tree);
2165
- return flattenTree(tree, namespace, deriveEntry, keyMode);
2249
+ const entries = flattenTree(tree, namespace, deriveEntry, keyMode);
2250
+ mergeDescriptions(entries, content, deriveDescriptions);
2251
+ return entries;
2166
2252
  } catch (error) {
2167
2253
  rethrowStructured(error, "The file could not be parsed.");
2168
2254
  }
@@ -2181,6 +2267,7 @@ function createTreeFileAdapter(options) {
2181
2267
  validateTree,
2182
2268
  buildWriteTree,
2183
2269
  comparePlaceholders,
2270
+ deriveDescriptions,
2184
2271
  keyMode = "literal-leaf"
2185
2272
  } = options;
2186
2273
  return {
@@ -2192,7 +2279,15 @@ function createTreeFileAdapter(options) {
2192
2279
  async read(filePath, locale) {
2193
2280
  const content = await readFileContent(filePath);
2194
2281
  const namespace = namespaceOf(filePath);
2195
- const entries = toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree);
2282
+ const entries = toEntries(
2283
+ content,
2284
+ namespace,
2285
+ parse3,
2286
+ deriveEntry,
2287
+ keyMode,
2288
+ validateTree,
2289
+ deriveDescriptions
2290
+ );
2196
2291
  const resource = { locale, namespace, format, entries };
2197
2292
  const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
2198
2293
  return { resource, invalidIcuKeys };
@@ -2233,6 +2328,31 @@ function stripArbMetadata(tree) {
2233
2328
  function originalKey(encoded) {
2234
2329
  return decodeKeyToSegments(encoded).join(".");
2235
2330
  }
2331
+ function messageKeyForMetadata(key) {
2332
+ return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
2333
+ }
2334
+ function descriptionOf(value) {
2335
+ if (typeof value !== "object" || value === null) {
2336
+ return void 0;
2337
+ }
2338
+ const description = value.description;
2339
+ return typeof description === "string" ? description : void 0;
2340
+ }
2341
+ function extractArbDescriptions(content) {
2342
+ const tree = parseArbObject(content);
2343
+ const out = /* @__PURE__ */ new Map();
2344
+ for (const [key, value] of Object.entries(tree)) {
2345
+ const messageKey = messageKeyForMetadata(key);
2346
+ if (messageKey === null) {
2347
+ continue;
2348
+ }
2349
+ const description = descriptionOf(value);
2350
+ if (description !== void 0) {
2351
+ out.set(encodeSegment(messageKey), description);
2352
+ }
2353
+ }
2354
+ return out;
2355
+ }
2236
2356
  function messagesFromEntries(entries) {
2237
2357
  const out = /* @__PURE__ */ new Map();
2238
2358
  for (const [key, entry] of entries) {
@@ -2289,7 +2409,8 @@ function createArbAdapter() {
2289
2409
  computeInvalidIcuKeys: icuInvalidKeys,
2290
2410
  validateMessage: icuIsValid,
2291
2411
  comparePlaceholders: compareIcuPlaceholders,
2292
- buildWriteTree: buildArbWriteTree
2412
+ buildWriteTree: buildArbWriteTree,
2413
+ deriveDescriptions: extractArbDescriptions
2293
2414
  });
2294
2415
  }
2295
2416
  function createJsonFileAdapter(options) {
@@ -2595,16 +2716,33 @@ function parseXml(content) {
2595
2716
  }
2596
2717
  return { doc, root };
2597
2718
  }
2719
+ function firstNoteText(parent) {
2720
+ const note = childByName(parent, "note");
2721
+ if (note === null) {
2722
+ return void 0;
2723
+ }
2724
+ const text = (note.textContent ?? "").trim();
2725
+ return text === "" ? void 0 : text;
2726
+ }
2727
+ function transUnitDescription(tu) {
2728
+ return firstNoteText(tu);
2729
+ }
2730
+ function unitDescription(unit) {
2731
+ const notes = childByName(unit, "notes");
2732
+ return notes === null ? void 0 : firstNoteText(notes);
2733
+ }
2598
2734
  function walkXliff12(root) {
2599
2735
  const units = [];
2600
2736
  collectByTag(root, "trans-unit").forEach((tu, index) => {
2601
2737
  const source = childByName(tu, "source");
2602
2738
  if (source !== null) {
2739
+ const description = transUnitDescription(tu);
2603
2740
  units.push({
2604
2741
  key: unitKey(tu, index),
2605
2742
  source,
2606
2743
  target: childByName(tu, "target"),
2607
- container: tu
2744
+ container: tu,
2745
+ ...description !== void 0 ? { description } : {}
2608
2746
  });
2609
2747
  }
2610
2748
  });
@@ -2614,12 +2752,19 @@ function walkXliff20(root) {
2614
2752
  const units = [];
2615
2753
  collectByTag(root, "unit").forEach((unit, index) => {
2616
2754
  const baseKey = unitKey(unit, index);
2755
+ const description = unitDescription(unit);
2617
2756
  const segments = elementChildren(unit).filter((el) => el.localName === "segment");
2618
2757
  segments.forEach((segment, segIndex) => {
2619
2758
  const source = childByName(segment, "source");
2620
2759
  if (source !== null) {
2621
2760
  const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
2622
- units.push({ key, source, target: childByName(segment, "target"), container: segment });
2761
+ units.push({
2762
+ key,
2763
+ source,
2764
+ target: childByName(segment, "target"),
2765
+ container: segment,
2766
+ ...description !== void 0 ? { description } : {}
2767
+ });
2623
2768
  }
2624
2769
  });
2625
2770
  });
@@ -2658,7 +2803,8 @@ function parseXliffEntries(content, namespace) {
2658
2803
  namespace,
2659
2804
  value,
2660
2805
  placeholders: extractXliffPlaceholders(value),
2661
- isPlural: false
2806
+ isPlural: false,
2807
+ ...unit.description !== void 0 ? { description: unit.description } : {}
2662
2808
  });
2663
2809
  }
2664
2810
  return out;
@@ -3549,10 +3695,13 @@ var INSTRUCTIONS_LINES = [
3549
3695
  " structure and the argument names exactly. Translate only the human-readable text.",
3550
3696
  "6. Do not edit the 'Key' column or the hidden 'Source hash' column. They map your translation",
3551
3697
  " back to the right string. You may sort or filter rows freely; mapping does not depend on row order.",
3698
+ "7. The 'Context' column, when present, is reference text from the source file explaining what the",
3699
+ " string means or where it is used. It is read-only; leave it as-is.",
3552
3700
  "",
3553
3701
  "Status values:",
3554
3702
  " new - this string has no translation yet.",
3555
3703
  " changed - the source string changed and the translation needs updating.",
3704
+ " unchanged - already up to date, included for reference. No action needed.",
3556
3705
  "",
3557
3706
  "When you are done, save the file and send it back. verbatra checks every value on import:",
3558
3707
  "a value with a broken placeholder, invalid ICU, or a source that changed since export is",
@@ -3564,7 +3713,8 @@ var COLUMN = {
3564
3713
  current: 3,
3565
3714
  status: 4,
3566
3715
  translation: 5,
3567
- sourceHash: 6
3716
+ sourceHash: 6,
3717
+ context: 7
3568
3718
  };
3569
3719
  var HEADERS = [
3570
3720
  "Key",
@@ -3572,7 +3722,8 @@ var HEADERS = [
3572
3722
  "Current translation",
3573
3723
  "Status",
3574
3724
  "Translation",
3575
- "Source hash"
3725
+ "Source hash",
3726
+ "Context"
3576
3727
  ];
3577
3728
  var HEADER_ROW = 1;
3578
3729
  var INSTRUCTIONS_SHEET_NAME = "Instructions";
@@ -3591,7 +3742,8 @@ var COLUMN_WIDTHS = {
3591
3742
  [COLUMN.source]: 50,
3592
3743
  [COLUMN.current]: 50,
3593
3744
  [COLUMN.status]: 12,
3594
- [COLUMN.translation]: 50
3745
+ [COLUMN.translation]: 50,
3746
+ [COLUMN.context]: 50
3595
3747
  };
3596
3748
  var TEXT_NUMBER_FORMAT = "@";
3597
3749
  function styleHeader(sheet) {
@@ -3622,7 +3774,8 @@ function writeRow(sheet, sheetRow) {
3622
3774
  translationCell.numFmt = TEXT_NUMBER_FORMAT;
3623
3775
  translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
3624
3776
  row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
3625
- for (let column = COLUMN.key; column <= COLUMN.sourceHash; column += 1) {
3777
+ row.getCell(COLUMN.context).value = sheetRow.context;
3778
+ for (let column = COLUMN.key; column <= COLUMN.context; column += 1) {
3626
3779
  const cell = row.getCell(column);
3627
3780
  cell.protection = { locked: column !== COLUMN.translation };
3628
3781
  if (column !== COLUMN.translation) {
@@ -3647,6 +3800,26 @@ function assertValidWorksheetName(locale) {
3647
3800
  );
3648
3801
  }
3649
3802
  }
3803
+ function assertNoWorksheetNameCollisions(sheets) {
3804
+ const reservedKey = INSTRUCTIONS_SHEET_NAME.toLowerCase();
3805
+ const seen = /* @__PURE__ */ new Set();
3806
+ for (const sheet of sheets) {
3807
+ const key = sheet.locale.toLowerCase();
3808
+ if (key === reservedKey) {
3809
+ throw new ExchangeError(
3810
+ "WORKBOOK_INVALID",
3811
+ `The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with the reserved "${INSTRUCTIONS_SHEET_NAME}" sheet.`
3812
+ );
3813
+ }
3814
+ if (seen.has(key)) {
3815
+ throw new ExchangeError(
3816
+ "WORKBOOK_INVALID",
3817
+ `The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with another target locale's sheet name.`
3818
+ );
3819
+ }
3820
+ seen.add(key);
3821
+ }
3822
+ }
3650
3823
  async function buildDataSheet(workbook, sheet) {
3651
3824
  assertValidWorksheetName(sheet.locale);
3652
3825
  const worksheet = workbook.addWorksheet(sheet.locale);
@@ -3673,6 +3846,7 @@ function buildInstructionsSheet(workbook) {
3673
3846
  sheet.getRow(1).font = { bold: true };
3674
3847
  }
3675
3848
  async function buildWorkbook(model) {
3849
+ assertNoWorksheetNameCollisions(model.sheets);
3676
3850
  const workbook = new ExcelJS__default.default.Workbook();
3677
3851
  buildInstructionsSheet(workbook);
3678
3852
  for (const sheet of model.sheets) {
@@ -3773,9 +3947,10 @@ var rowSchema = zod.z.object({
3773
3947
  key: zod.z.string().min(1),
3774
3948
  source: zod.z.string(),
3775
3949
  currentTarget: zod.z.string(),
3776
- status: zod.z.enum(["new", "changed"]),
3950
+ status: zod.z.enum(["new", "changed", "unchanged"]),
3777
3951
  sourceHash: zod.z.string(),
3778
- translation: zod.z.string()
3952
+ translation: zod.z.string(),
3953
+ context: zod.z.string()
3779
3954
  });
3780
3955
  function cellString(cell) {
3781
3956
  const value = cell.value;
@@ -3808,7 +3983,10 @@ function parseRow(sheet, row) {
3808
3983
  currentTarget: cellString(row.getCell(COLUMN.current)),
3809
3984
  status: cellString(row.getCell(COLUMN.status)),
3810
3985
  sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
3811
- translation: cellString(row.getCell(COLUMN.translation))
3986
+ translation: cellString(row.getCell(COLUMN.translation)),
3987
+ // A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
3988
+ // empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
3989
+ context: cellString(row.getCell(COLUMN.context))
3812
3990
  };
3813
3991
  const result = rowSchema.safeParse(candidate);
3814
3992
  if (!result.success) {
@@ -3897,14 +4075,15 @@ function buildRows(source, target, baseline, includeUnchanged) {
3897
4075
  currentTarget: target.entries.get(key)?.value ?? "",
3898
4076
  status,
3899
4077
  sourceHash: contentHash(sourceEntry),
3900
- translation: ""
4078
+ translation: "",
4079
+ context: sourceEntry.description ?? ""
3901
4080
  });
3902
4081
  }
3903
4082
  };
3904
4083
  add(diff2.missing, "new");
3905
4084
  add(diff2.changed, "changed");
3906
4085
  if (includeUnchanged) {
3907
- add(diff2.unchanged, "changed");
4086
+ add(diff2.unchanged, "unchanged");
3908
4087
  }
3909
4088
  return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
3910
4089
  }