@verbatra/sdk 0.4.2 → 0.4.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
@@ -17,6 +17,7 @@ var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
17
17
  var xmldom = require('@xmldom/xmldom');
18
18
  var yaml = require('yaml');
19
19
  var ExcelJS = require('exceljs');
20
+ var stream = require('stream');
20
21
  var JSZip = require('jszip');
21
22
  var chokidar = require('chokidar');
22
23
 
@@ -181,7 +182,7 @@ function checkPlaceholders(source, translated) {
181
182
  const extra = multisetExcess(translatedCounts, sourceCounts);
182
183
  const reordered = missing.length === 0 && extra.length === 0 && !sameOrder(source, translated);
183
184
  return {
184
- matches: missing.length === 0 && extra.length === 0 && !reordered,
185
+ matches: missing.length === 0 && extra.length === 0,
185
186
  missing,
186
187
  extra,
187
188
  reordered
@@ -502,6 +503,19 @@ function createDefaultClient2() {
502
503
  };
503
504
  return { client, freeAccount };
504
505
  }
506
+ 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.";
507
+ function partitionByPlaceholders(entries) {
508
+ const protectable = [];
509
+ const unprotectable = [];
510
+ for (const entry of entries) {
511
+ if (entry.placeholders.length > 0) {
512
+ unprotectable.push(entry);
513
+ } else {
514
+ protectable.push(entry);
515
+ }
516
+ }
517
+ return { protectable, unprotectable };
518
+ }
505
519
  var FORMALITY_DOWNGRADED_MESSAGE = "Formality was not applied: the configured DeepL key is a free-tier key, which does not support formality.";
506
520
  var GLOSSARY_IGNORED_MESSAGE = "The supplied glossary term map was not applied: DeepL uses configured glossary IDs, not term maps.";
507
521
  function buildTranslateOptions(input) {
@@ -565,6 +579,7 @@ function resolveClient(deps) {
565
579
  }
566
580
  async function translate(bundle, config, request) {
567
581
  const data = validateRequest(request);
582
+ const { protectable, unprotectable } = partitionByPlaceholders(data.entries);
568
583
  const genericGlossarySupplied = request.glossary !== void 0 && Object.keys(request.glossary).length > 0;
569
584
  const { options, notices } = buildTranslateOptions({
570
585
  freeAccount: bundle.freeAccount,
@@ -572,18 +587,28 @@ async function translate(bundle, config, request) {
572
587
  ...data.tone !== void 0 ? { tone: data.tone } : {},
573
588
  ...config.glossaryId !== void 0 ? { glossaryId: config.glossaryId } : {}
574
589
  });
575
- const texts = data.entries.map((entry) => entry.value);
576
- const results = await callClient2(
590
+ const { values, integrity } = await translateProtectable(
577
591
  bundle.client,
578
- texts,
579
- data.sourceLocale,
580
- data.targetLocale,
581
- options
592
+ data,
593
+ protectable,
594
+ options,
595
+ request.extractPlaceholders
582
596
  );
583
- const { values, integrityInputs } = zipResults(data.entries, results);
584
- const integrity = checkBatchIntegrity(integrityInputs, request.extractPlaceholders);
597
+ if (unprotectable.length > 0) {
598
+ notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
599
+ }
585
600
  return { values, integrity, notices };
586
601
  }
602
+ async function translateProtectable(client, data, protectable, options, extract) {
603
+ if (protectable.length === 0) {
604
+ return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
605
+ }
606
+ const texts = protectable.map((entry) => entry.value);
607
+ const results = await callClient2(client, texts, data.sourceLocale, data.targetLocale, options);
608
+ const { values, integrityInputs } = zipResults(protectable, results);
609
+ const integrity = checkBatchIntegrity(integrityInputs, extract);
610
+ return { values, integrity };
611
+ }
587
612
  function callClient2(client, texts, sourceLang, targetLang, options) {
588
613
  return guardProviderCall(() => client.translateText(texts, sourceLang, targetLang, options));
589
614
  }
@@ -2087,6 +2112,24 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
2087
2112
  );
2088
2113
  }
2089
2114
 
2115
+ // src/flow/select-locales.ts
2116
+ function selectLocales(config, requested) {
2117
+ if (requested === void 0) {
2118
+ return config.targetLocales;
2119
+ }
2120
+ const configured = new Set(config.targetLocales);
2121
+ const unknown = requested.filter((locale) => !configured.has(locale));
2122
+ if (unknown.length > 0) {
2123
+ const label = unknown.length === 1 ? "locale" : "locales";
2124
+ throw new SdkError(
2125
+ "UNKNOWN_LOCALE",
2126
+ `Requested ${label} not in the configured target locales: ${unknown.join(", ")}. Configured targets: ${config.targetLocales.join(", ")}.`
2127
+ );
2128
+ }
2129
+ const wanted = new Set(requested);
2130
+ return config.targetLocales.filter((locale) => wanted.has(locale));
2131
+ }
2132
+
2090
2133
  // src/flow/source.ts
2091
2134
  async function readSource(config, cwd, fs, adapter) {
2092
2135
  const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
@@ -2115,13 +2158,6 @@ async function readTarget(cwd, config, adapter, fs, locale) {
2115
2158
  }
2116
2159
  return (await adapter.read(path, locale)).resource;
2117
2160
  }
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
2161
  async function diffLocales(input, deps = {}) {
2126
2162
  const config = input.config;
2127
2163
  const cwd = input.cwd ?? process.cwd();
@@ -2130,7 +2166,7 @@ async function diffLocales(input, deps = {}) {
2130
2166
  const source = await readSource(config, cwd, fs, adapter);
2131
2167
  const lock = await readLockFile(lockFilePath(cwd), fs);
2132
2168
  return Promise.all(
2133
- selectedLocales(config, input.locales).map(async (locale) => {
2169
+ selectLocales(config, input.locales).map(async (locale) => {
2134
2170
  const target = await readTarget(cwd, config, adapter, fs, locale);
2135
2171
  const diff2 = diffResources(source.resource, target, { baseline: baselineFor(lock, locale) });
2136
2172
  return { locale, diff: diff2 };
@@ -2856,6 +2892,34 @@ function declaredSize(file) {
2856
2892
  const size = data?.uncompressedSize;
2857
2893
  return typeof size === "number" && Number.isFinite(size) ? size : void 0;
2858
2894
  }
2895
+ async function streamEntryBounded(file, remaining) {
2896
+ const chunks = [];
2897
+ let raw = 0;
2898
+ let oversize = false;
2899
+ const source = new stream.Readable({ objectMode: true }).wrap(
2900
+ file.nodeStream("nodebuffer")
2901
+ );
2902
+ try {
2903
+ for await (const chunk2 of source) {
2904
+ const buf = Buffer.isBuffer(chunk2) ? chunk2 : Buffer.from(chunk2);
2905
+ raw += buf.length;
2906
+ if (raw > remaining) {
2907
+ oversize = true;
2908
+ break;
2909
+ }
2910
+ chunks.push(buf);
2911
+ }
2912
+ } catch {
2913
+ throw new ExchangeError("WORKBOOK_INVALID", "A workbook entry could not be decompressed.");
2914
+ }
2915
+ if (oversize) {
2916
+ throw new ExchangeError(
2917
+ "WORKBOOK_INVALID",
2918
+ "The workbook decompresses to more than the permitted maximum bytes."
2919
+ );
2920
+ }
2921
+ return Buffer.concat(chunks).toString("utf8");
2922
+ }
2859
2923
  async function guardWorkbookBytes(bytes, limits) {
2860
2924
  let zip;
2861
2925
  try {
@@ -2885,12 +2949,8 @@ async function guardWorkbookBytes(bytes, limits) {
2885
2949
  }
2886
2950
  let actualTotal = 0;
2887
2951
  for (const file of files) {
2888
- let content;
2889
- try {
2890
- content = await file.async("string");
2891
- } catch {
2892
- throw new ExchangeError("WORKBOOK_INVALID", "A workbook entry could not be decompressed.");
2893
- }
2952
+ const remaining = limits.maxDecompressedBytes - actualTotal;
2953
+ const content = await streamEntryBounded(file, remaining);
2894
2954
  actualTotal += Buffer.byteLength(content, "utf8");
2895
2955
  if (actualTotal > limits.maxDecompressedBytes) {
2896
2956
  throw new ExchangeError(
@@ -3040,13 +3100,6 @@ function buildRows(source, target, baseline, includeUnchanged) {
3040
3100
  }
3041
3101
  return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
3042
3102
  }
3043
- function selectedLocales2(config, requested) {
3044
- if (requested === void 0) {
3045
- return config.targetLocales;
3046
- }
3047
- const wanted = new Set(requested);
3048
- return config.targetLocales.filter((locale) => wanted.has(locale));
3049
- }
3050
3103
  async function exportWorkbook(input, deps = {}) {
3051
3104
  const config = input.config;
3052
3105
  const cwd = input.cwd ?? process.cwd();
@@ -3054,7 +3107,7 @@ async function exportWorkbook(input, deps = {}) {
3054
3107
  const adapter = selectAdapter(config.format, deps.adapterRegistry);
3055
3108
  const source = await readSource(config, cwd, fs, adapter);
3056
3109
  const lock = await readLockFile(lockFilePath(cwd), fs);
3057
- const locales = selectedLocales2(config, input.locales);
3110
+ const locales = selectLocales(config, input.locales);
3058
3111
  const sheets = await Promise.all(
3059
3112
  locales.map(async (locale) => {
3060
3113
  const target = await readTarget3(cwd, config, adapter, fs, locale);