@verbatra/sdk 0.5.0-next.0 → 0.5.0-next.2
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/README.md +1 -2
- package/dist/index.cjs +705 -128
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +123 -35
- package/dist/index.d.ts +123 -35
- package/dist/index.js +709 -132
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -11,9 +11,9 @@ import { createRequire } from 'module';
|
|
|
11
11
|
import log from 'loglevel';
|
|
12
12
|
import { GoogleGenAI } from '@google/genai';
|
|
13
13
|
import OpenAI from 'openai';
|
|
14
|
-
import { parse
|
|
14
|
+
import { parse, TYPE } from '@formatjs/icu-messageformat-parser';
|
|
15
15
|
import { XMLSerializer, DOMParser } from '@xmldom/xmldom';
|
|
16
|
-
import { stringify, parse } from 'yaml';
|
|
16
|
+
import { stringify, parse as parse$1 } from 'yaml';
|
|
17
17
|
import ExcelJS from 'exceljs';
|
|
18
18
|
import { Readable } from 'stream';
|
|
19
19
|
import JSZip from 'jszip';
|
|
@@ -311,6 +311,85 @@ var LOCALE_TOKEN = "{locale}";
|
|
|
311
311
|
function localeFilePath(cwd, pattern, locale) {
|
|
312
312
|
return resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
|
|
313
313
|
}
|
|
314
|
+
var RATE_LIMITED_STATUS = 429;
|
|
315
|
+
var TIMEOUT_STATUS = 408;
|
|
316
|
+
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
317
|
+
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
318
|
+
"RateLimitError",
|
|
319
|
+
// openai, @anthropic-ai/sdk
|
|
320
|
+
"TooManyRequestsError"
|
|
321
|
+
// deepl-node
|
|
322
|
+
]);
|
|
323
|
+
var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
324
|
+
"AuthenticationError",
|
|
325
|
+
// openai, @anthropic-ai/sdk (401)
|
|
326
|
+
"PermissionDeniedError",
|
|
327
|
+
// openai, @anthropic-ai/sdk (403)
|
|
328
|
+
"AuthorizationError"
|
|
329
|
+
// deepl-node
|
|
330
|
+
]);
|
|
331
|
+
var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
332
|
+
"APIConnectionTimeoutError",
|
|
333
|
+
// openai, @anthropic-ai/sdk
|
|
334
|
+
// deepl-node has no distinct timeout class; ConnectionError is its only network-failure
|
|
335
|
+
// class, so it is folded into TIMEOUT rather than the PROVIDER_ERROR fallback.
|
|
336
|
+
"ConnectionError"
|
|
337
|
+
]);
|
|
338
|
+
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
339
|
+
"APIUserAbortError"
|
|
340
|
+
// openai, @anthropic-ai/sdk
|
|
341
|
+
]);
|
|
342
|
+
function getErrorStatus(error) {
|
|
343
|
+
if (typeof error !== "object" || error === null || !("status" in error)) {
|
|
344
|
+
return void 0;
|
|
345
|
+
}
|
|
346
|
+
const status = error.status;
|
|
347
|
+
return typeof status === "number" ? status : void 0;
|
|
348
|
+
}
|
|
349
|
+
function readClassName(error) {
|
|
350
|
+
if (typeof error !== "object" || error === null) {
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
return error.constructor?.name;
|
|
354
|
+
}
|
|
355
|
+
function classifyProviderError(error) {
|
|
356
|
+
const status = getErrorStatus(error);
|
|
357
|
+
if (status === RATE_LIMITED_STATUS) {
|
|
358
|
+
return "RATE_LIMITED";
|
|
359
|
+
}
|
|
360
|
+
if (status !== void 0 && AUTH_FAILED_STATUSES.has(status)) {
|
|
361
|
+
return "AUTH_FAILED";
|
|
362
|
+
}
|
|
363
|
+
if (status === TIMEOUT_STATUS) {
|
|
364
|
+
return "TIMEOUT";
|
|
365
|
+
}
|
|
366
|
+
const className = readClassName(error);
|
|
367
|
+
if (className !== void 0) {
|
|
368
|
+
if (RATE_LIMITED_CLASS_NAMES.has(className)) {
|
|
369
|
+
return "RATE_LIMITED";
|
|
370
|
+
}
|
|
371
|
+
if (AUTH_FAILED_CLASS_NAMES.has(className)) {
|
|
372
|
+
return "AUTH_FAILED";
|
|
373
|
+
}
|
|
374
|
+
if (TIMEOUT_CLASS_NAMES.has(className)) {
|
|
375
|
+
return "TIMEOUT";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return "PROVIDER_ERROR";
|
|
379
|
+
}
|
|
380
|
+
function isAbortShapedError(error) {
|
|
381
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
const className = readClassName(error);
|
|
385
|
+
return className !== void 0 && ABORT_ERROR_CLASS_NAMES.has(className);
|
|
386
|
+
}
|
|
387
|
+
function isAbortError(error, signal) {
|
|
388
|
+
if (signal !== void 0) {
|
|
389
|
+
return signal.aborted === true && isAbortShapedError(error);
|
|
390
|
+
}
|
|
391
|
+
return isAbortShapedError(error);
|
|
392
|
+
}
|
|
314
393
|
var REDACTED = "[REDACTED]";
|
|
315
394
|
var KEY_PATTERNS = [
|
|
316
395
|
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
@@ -345,17 +424,34 @@ var ProviderError = class extends Error {
|
|
|
345
424
|
}
|
|
346
425
|
};
|
|
347
426
|
var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
|
|
348
|
-
|
|
427
|
+
var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
|
|
428
|
+
var TIMEOUT_MESSAGE = "The translation provider request timed out.";
|
|
429
|
+
var AUTH_FAILED_MESSAGE = "The translation provider rejected the request credentials.";
|
|
430
|
+
var MESSAGE_BY_CODE = {
|
|
431
|
+
RATE_LIMITED: RATE_LIMITED_MESSAGE,
|
|
432
|
+
TIMEOUT: TIMEOUT_MESSAGE,
|
|
433
|
+
AUTH_FAILED: AUTH_FAILED_MESSAGE,
|
|
434
|
+
PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
|
|
435
|
+
};
|
|
436
|
+
async function guardProviderCall(call, signal) {
|
|
437
|
+
signal?.throwIfAborted();
|
|
349
438
|
try {
|
|
350
439
|
return await call();
|
|
351
|
-
} catch {
|
|
352
|
-
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (isAbortError(error, signal)) {
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
const code = classifyProviderError(error);
|
|
445
|
+
throw new ProviderError(code, MESSAGE_BY_CODE[code]);
|
|
353
446
|
}
|
|
354
447
|
}
|
|
355
|
-
function checkBatchIntegrity(inputs, extract) {
|
|
448
|
+
function checkBatchIntegrity(inputs, extract, compare) {
|
|
356
449
|
const integrity = /* @__PURE__ */ new Map();
|
|
357
|
-
for (const { key,
|
|
358
|
-
integrity.set(
|
|
450
|
+
for (const { key, sourceValue, translatedValue } of inputs) {
|
|
451
|
+
integrity.set(
|
|
452
|
+
key,
|
|
453
|
+
compare !== void 0 ? compare(sourceValue, translatedValue) : checkPlaceholders(extract(sourceValue), extract(translatedValue))
|
|
454
|
+
);
|
|
359
455
|
}
|
|
360
456
|
return integrity;
|
|
361
457
|
}
|
|
@@ -377,16 +473,14 @@ function validateRequest(request) {
|
|
|
377
473
|
return parsed.data;
|
|
378
474
|
}
|
|
379
475
|
function toIntegrityInputs(entries, values) {
|
|
380
|
-
|
|
476
|
+
const inputs = [];
|
|
477
|
+
for (const entry of entries) {
|
|
381
478
|
const translatedValue = values.get(entry.key);
|
|
382
|
-
if (translatedValue
|
|
383
|
-
|
|
384
|
-
"INVALID_RESPONSE",
|
|
385
|
-
"The provider response is missing one or more keys."
|
|
386
|
-
);
|
|
479
|
+
if (translatedValue !== void 0) {
|
|
480
|
+
inputs.push({ key: entry.key, sourceValue: entry.value, translatedValue });
|
|
387
481
|
}
|
|
388
|
-
|
|
389
|
-
|
|
482
|
+
}
|
|
483
|
+
return inputs;
|
|
390
484
|
}
|
|
391
485
|
function toItem(entry) {
|
|
392
486
|
return {
|
|
@@ -418,25 +512,29 @@ function deriveJsonSchema(schema) {
|
|
|
418
512
|
}
|
|
419
513
|
return result;
|
|
420
514
|
}
|
|
421
|
-
function
|
|
422
|
-
const
|
|
423
|
-
const
|
|
515
|
+
function partitionTranslations(translations, requested) {
|
|
516
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
517
|
+
const duplicated = /* @__PURE__ */ new Set();
|
|
424
518
|
for (const { key, value } of translations) {
|
|
425
|
-
if (!requested.has(key)
|
|
426
|
-
throw new ProviderError(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
);
|
|
519
|
+
if (!requested.has(key)) {
|
|
520
|
+
throw new ProviderError("INVALID_RESPONSE", "The provider returned an unexpected key.");
|
|
521
|
+
}
|
|
522
|
+
if (firstSeen.has(key)) {
|
|
523
|
+
duplicated.add(key);
|
|
524
|
+
continue;
|
|
430
525
|
}
|
|
431
|
-
|
|
526
|
+
firstSeen.set(key, value);
|
|
432
527
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
528
|
+
return { firstSeen, duplicated };
|
|
529
|
+
}
|
|
530
|
+
function reconcile(translations, requestedKeys) {
|
|
531
|
+
const requested = new Set(requestedKeys);
|
|
532
|
+
const { firstSeen, duplicated } = partitionTranslations(translations, requested);
|
|
533
|
+
const missingKeys = requestedKeys.filter((key) => duplicated.has(key) || !firstSeen.has(key));
|
|
534
|
+
for (const key of duplicated) {
|
|
535
|
+
firstSeen.delete(key);
|
|
438
536
|
}
|
|
439
|
-
return
|
|
537
|
+
return { accepted: firstSeen, missingKeys };
|
|
440
538
|
}
|
|
441
539
|
function reconcileResult(raw, requestedKeys) {
|
|
442
540
|
const parsed = translationsResultSchema.safeParse(raw);
|
|
@@ -448,17 +546,54 @@ function reconcileResult(raw, requestedKeys) {
|
|
|
448
546
|
}
|
|
449
547
|
return reconcile(parsed.data.translations, requestedKeys);
|
|
450
548
|
}
|
|
549
|
+
var MAX_REPAIR_ROUNDS = 1;
|
|
451
550
|
async function runLlmTranslation(request, mechanism) {
|
|
452
551
|
const data = validateRequest(request);
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
|
|
552
|
+
const signal = request.signal;
|
|
553
|
+
const first = await requestTranslations(mechanism, data, signal);
|
|
554
|
+
const values = first.outcome.accepted;
|
|
555
|
+
let usage = first.completion.usage;
|
|
556
|
+
let toRepair = entriesFor(data.entries, first.outcome.missingKeys);
|
|
557
|
+
for (let round = 0; round < MAX_REPAIR_ROUNDS && toRepair.length > 0; round += 1) {
|
|
558
|
+
const repair = await requestTranslations(mechanism, { ...data, entries: toRepair }, signal);
|
|
559
|
+
for (const [key, value] of repair.outcome.accepted) {
|
|
560
|
+
values.set(key, value);
|
|
561
|
+
}
|
|
562
|
+
usage = mergeUsage(usage, repair.completion.usage);
|
|
563
|
+
toRepair = entriesFor(data.entries, repair.outcome.missingKeys);
|
|
564
|
+
}
|
|
457
565
|
const integrity = checkBatchIntegrity(
|
|
458
566
|
toIntegrityInputs(data.entries, values),
|
|
459
|
-
request.extractPlaceholders
|
|
567
|
+
request.extractPlaceholders,
|
|
568
|
+
request.comparePlaceholders
|
|
460
569
|
);
|
|
461
|
-
return
|
|
570
|
+
return usage === void 0 ? { values, integrity, notices: [] } : { values, integrity, usage, notices: [] };
|
|
571
|
+
}
|
|
572
|
+
async function requestTranslations(mechanism, data, signal) {
|
|
573
|
+
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
574
|
+
const requestedKeys = data.entries.map((entry) => entry.key);
|
|
575
|
+
const completion = await mechanism.translate({
|
|
576
|
+
payloadJson,
|
|
577
|
+
requestedKeys,
|
|
578
|
+
...signal !== void 0 ? { signal } : {}
|
|
579
|
+
});
|
|
580
|
+
return { completion, outcome: reconcileResult(completion.raw, requestedKeys) };
|
|
581
|
+
}
|
|
582
|
+
function entriesFor(entries, keys) {
|
|
583
|
+
const wanted = new Set(keys);
|
|
584
|
+
return entries.filter((entry) => wanted.has(entry.key));
|
|
585
|
+
}
|
|
586
|
+
function mergeUsage(first, second) {
|
|
587
|
+
if (first === void 0) {
|
|
588
|
+
return second;
|
|
589
|
+
}
|
|
590
|
+
if (second === void 0) {
|
|
591
|
+
return first;
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
inputTokens: first.inputTokens + second.inputTokens,
|
|
595
|
+
outputTokens: first.outputTokens + second.outputTokens
|
|
596
|
+
};
|
|
462
597
|
}
|
|
463
598
|
var OUTPUT_TRUNCATED_MESSAGE = "The provider stopped because the output-token limit was reached. Reduce the batch size or raise the configured max output tokens.";
|
|
464
599
|
function assertNotTruncated(truncated) {
|
|
@@ -491,12 +626,29 @@ function requireGeminiKey() {
|
|
|
491
626
|
function requireDeepLKey() {
|
|
492
627
|
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
493
628
|
}
|
|
629
|
+
var OPENAI_COMPATIBLE_ENV_VAR = "OPENAI_COMPATIBLE_API_KEY";
|
|
630
|
+
var OPENAI_COMPATIBLE_KEY_PLACEHOLDER = "local";
|
|
631
|
+
function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
632
|
+
const varName = customEnvVar ?? OPENAI_COMPATIBLE_ENV_VAR;
|
|
633
|
+
const value = process.env[varName];
|
|
634
|
+
if (value !== void 0 && value.length > 0) {
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
if (customEnvVar !== void 0) {
|
|
638
|
+
throw new ProviderError(
|
|
639
|
+
"MISSING_API_KEY",
|
|
640
|
+
`The ${customEnvVar} environment variable is not set.`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
return OPENAI_COMPATIBLE_KEY_PLACEHOLDER;
|
|
644
|
+
}
|
|
494
645
|
function createDefaultClient() {
|
|
495
646
|
const sdk = new Anthropic({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
496
647
|
return {
|
|
497
648
|
messages: {
|
|
498
|
-
create: async (body) => await sdk.messages.create(
|
|
499
|
-
body
|
|
649
|
+
create: async (body, options) => await sdk.messages.create(
|
|
650
|
+
body,
|
|
651
|
+
options
|
|
500
652
|
)
|
|
501
653
|
}
|
|
502
654
|
};
|
|
@@ -564,9 +716,9 @@ function createAnthropicProvider(config, deps = {}) {
|
|
|
564
716
|
}
|
|
565
717
|
function createMechanism(client, config) {
|
|
566
718
|
return {
|
|
567
|
-
translate: async ({ payloadJson }) => {
|
|
719
|
+
translate: async ({ payloadJson, signal }) => {
|
|
568
720
|
const body = buildRequest(config, payloadJson);
|
|
569
|
-
const message = await callClient(client, body);
|
|
721
|
+
const message = await callClient(client, body, signal);
|
|
570
722
|
assertNotTruncated(message.stop_reason === "max_tokens");
|
|
571
723
|
const raw = requireToolInput(message.content);
|
|
572
724
|
const usage = toUsage(message.usage);
|
|
@@ -574,8 +726,11 @@ function createMechanism(client, config) {
|
|
|
574
726
|
}
|
|
575
727
|
};
|
|
576
728
|
}
|
|
577
|
-
function callClient(client, body) {
|
|
578
|
-
return guardProviderCall(
|
|
729
|
+
function callClient(client, body, signal) {
|
|
730
|
+
return guardProviderCall(
|
|
731
|
+
() => client.messages.create(body, signal !== void 0 ? { signal } : void 0),
|
|
732
|
+
signal
|
|
733
|
+
);
|
|
579
734
|
}
|
|
580
735
|
function toUsage(usage) {
|
|
581
736
|
if (usage === void 0) {
|
|
@@ -670,7 +825,7 @@ function zipResults(entries, results) {
|
|
|
670
825
|
values.set(entry.key, translatedValue);
|
|
671
826
|
integrityInputs.push({
|
|
672
827
|
key: entry.key,
|
|
673
|
-
|
|
828
|
+
sourceValue: entry.value,
|
|
674
829
|
translatedValue
|
|
675
830
|
});
|
|
676
831
|
}
|
|
@@ -686,7 +841,10 @@ function createDeepLProvider(config, deps = {}) {
|
|
|
686
841
|
return {
|
|
687
842
|
id: PROVIDER_ID2,
|
|
688
843
|
kind: "machine-translation",
|
|
689
|
-
|
|
844
|
+
// Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
|
|
845
|
+
// generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
|
|
846
|
+
// silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
|
|
847
|
+
supportsGlossary: validConfig.glossaryId !== void 0,
|
|
690
848
|
translateBatch: (request) => translate(bundle, validConfig, request)
|
|
691
849
|
};
|
|
692
850
|
}
|
|
@@ -711,36 +869,97 @@ async function translate(bundle, config, request) {
|
|
|
711
869
|
data,
|
|
712
870
|
protectable,
|
|
713
871
|
options,
|
|
714
|
-
request.extractPlaceholders
|
|
872
|
+
request.extractPlaceholders,
|
|
873
|
+
request.comparePlaceholders,
|
|
874
|
+
request.signal
|
|
715
875
|
);
|
|
716
876
|
if (unprotectable.length > 0) {
|
|
717
877
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
718
878
|
}
|
|
719
879
|
return { values, integrity, notices };
|
|
720
880
|
}
|
|
721
|
-
async function translateProtectable(client, data, protectable, options, extract) {
|
|
881
|
+
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
722
882
|
if (protectable.length === 0) {
|
|
723
883
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
724
884
|
}
|
|
725
885
|
const texts = protectable.map((entry) => entry.value);
|
|
726
|
-
const results = await callClient2(
|
|
886
|
+
const results = await callClient2(
|
|
887
|
+
client,
|
|
888
|
+
texts,
|
|
889
|
+
data.sourceLocale,
|
|
890
|
+
data.targetLocale,
|
|
891
|
+
options,
|
|
892
|
+
signal
|
|
893
|
+
);
|
|
727
894
|
const { values, integrityInputs } = zipResults(protectable, results);
|
|
728
|
-
const integrity = checkBatchIntegrity(integrityInputs, extract);
|
|
895
|
+
const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
|
|
729
896
|
return { values, integrity };
|
|
730
897
|
}
|
|
731
|
-
function callClient2(client, texts, sourceLang, targetLang, options) {
|
|
732
|
-
return guardProviderCall(
|
|
898
|
+
function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
899
|
+
return guardProviderCall(
|
|
900
|
+
() => client.translateText(texts, sourceLang, targetLang, options),
|
|
901
|
+
signal
|
|
902
|
+
);
|
|
733
903
|
}
|
|
734
904
|
var geminiConfigSchema = z.object({
|
|
735
905
|
model: z.string().min(1),
|
|
736
906
|
maxOutputTokens: z.number().int().positive()
|
|
737
907
|
});
|
|
908
|
+
var DEFAULT_GEMINI_RETRY = { attempts: 3, baseDelayMs: 250 };
|
|
909
|
+
function isRetryableStatus(error) {
|
|
910
|
+
const status = getErrorStatus(error);
|
|
911
|
+
return status === 429 || status !== void 0 && status >= 500 && status < 600;
|
|
912
|
+
}
|
|
913
|
+
function isAborted(signal) {
|
|
914
|
+
return signal?.aborted === true;
|
|
915
|
+
}
|
|
916
|
+
function throwAbort(signal) {
|
|
917
|
+
signal?.throwIfAborted();
|
|
918
|
+
throw new DOMException("This operation was aborted.", "AbortError");
|
|
919
|
+
}
|
|
920
|
+
function delay(ms, signal) {
|
|
921
|
+
return new Promise((resolve7) => {
|
|
922
|
+
const onAbort = () => {
|
|
923
|
+
clearTimeout(timer);
|
|
924
|
+
resolve7();
|
|
925
|
+
};
|
|
926
|
+
const timer = setTimeout(() => {
|
|
927
|
+
signal?.removeEventListener("abort", onAbort);
|
|
928
|
+
resolve7();
|
|
929
|
+
}, ms);
|
|
930
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
async function withGeminiRetry(call, signal, config = DEFAULT_GEMINI_RETRY) {
|
|
934
|
+
let attempt = 1;
|
|
935
|
+
while (true) {
|
|
936
|
+
try {
|
|
937
|
+
return await call();
|
|
938
|
+
} catch (error) {
|
|
939
|
+
if (isAborted(signal)) {
|
|
940
|
+
throwAbort(signal);
|
|
941
|
+
}
|
|
942
|
+
const exhausted = attempt >= config.attempts;
|
|
943
|
+
if (exhausted || !isRetryableStatus(error)) {
|
|
944
|
+
throw error;
|
|
945
|
+
}
|
|
946
|
+
await delay(config.baseDelayMs * 2 ** (attempt - 1), signal);
|
|
947
|
+
if (isAborted(signal)) {
|
|
948
|
+
throwAbort(signal);
|
|
949
|
+
}
|
|
950
|
+
attempt += 1;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
738
954
|
function createDefaultClient3() {
|
|
739
955
|
const ai = new GoogleGenAI({ apiKey: requireGeminiKey() });
|
|
740
956
|
return {
|
|
741
957
|
models: {
|
|
742
|
-
generateContent:
|
|
743
|
-
|
|
958
|
+
generateContent: (request) => withGeminiRetry(
|
|
959
|
+
async () => await ai.models.generateContent(
|
|
960
|
+
request
|
|
961
|
+
),
|
|
962
|
+
request.config.abortSignal
|
|
744
963
|
)
|
|
745
964
|
}
|
|
746
965
|
};
|
|
@@ -804,7 +1023,7 @@ var GEMINI_SYSTEM_RULES = [
|
|
|
804
1023
|
"When a tone is provided, honor it.",
|
|
805
1024
|
"Respond only with the structured object: exactly one entry per requested key, no commentary, no extra keys, and no key that was not requested."
|
|
806
1025
|
].join("\n");
|
|
807
|
-
function buildGeminiRequest(config, payloadJson) {
|
|
1026
|
+
function buildGeminiRequest(config, payloadJson, signal) {
|
|
808
1027
|
return {
|
|
809
1028
|
model: config.model,
|
|
810
1029
|
contents: [{ role: "user", parts: [{ text: payloadJson }] }],
|
|
@@ -812,7 +1031,8 @@ function buildGeminiRequest(config, payloadJson) {
|
|
|
812
1031
|
systemInstruction: GEMINI_SYSTEM_RULES,
|
|
813
1032
|
responseMimeType: "application/json",
|
|
814
1033
|
responseSchema: toGeminiSchema(deriveJsonSchema(translationsResultSchema)),
|
|
815
|
-
maxOutputTokens: config.maxOutputTokens
|
|
1034
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
1035
|
+
...signal !== void 0 ? { abortSignal: signal } : {}
|
|
816
1036
|
}
|
|
817
1037
|
};
|
|
818
1038
|
}
|
|
@@ -876,15 +1096,15 @@ function createGeminiProvider(config, deps = {}) {
|
|
|
876
1096
|
}
|
|
877
1097
|
function createMechanism2(client, config) {
|
|
878
1098
|
return {
|
|
879
|
-
translate: async ({ payloadJson }) => {
|
|
880
|
-
const request = buildGeminiRequest(config, payloadJson);
|
|
881
|
-
const response = await callClient3(client, request);
|
|
1099
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1100
|
+
const request = buildGeminiRequest(config, payloadJson, signal);
|
|
1101
|
+
const response = await callClient3(client, request, signal);
|
|
882
1102
|
return extractGeminiResult(response);
|
|
883
1103
|
}
|
|
884
1104
|
};
|
|
885
1105
|
}
|
|
886
|
-
function callClient3(client, request) {
|
|
887
|
-
return guardProviderCall(() => client.models.generateContent(request));
|
|
1106
|
+
function callClient3(client, request, signal) {
|
|
1107
|
+
return guardProviderCall(() => client.models.generateContent(request), signal);
|
|
888
1108
|
}
|
|
889
1109
|
var openAiConfigSchema = z.object({
|
|
890
1110
|
model: z.string().min(1),
|
|
@@ -895,8 +1115,9 @@ function createDefaultClient4() {
|
|
|
895
1115
|
return {
|
|
896
1116
|
chat: {
|
|
897
1117
|
completions: {
|
|
898
|
-
create: async (body) => await sdk.chat.completions.create(
|
|
899
|
-
body
|
|
1118
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1119
|
+
body,
|
|
1120
|
+
options
|
|
900
1121
|
)
|
|
901
1122
|
}
|
|
902
1123
|
}
|
|
@@ -914,27 +1135,90 @@ var OPENAI_SYSTEM_RULES = [
|
|
|
914
1135
|
"When a tone is provided, honor it.",
|
|
915
1136
|
"Respond only with the structured object: exactly one entry per requested key, no commentary, no extra keys, and no key that was not requested."
|
|
916
1137
|
].join("\n");
|
|
917
|
-
function
|
|
1138
|
+
function buildResponseFormat(mode) {
|
|
1139
|
+
if (mode === "json-object") {
|
|
1140
|
+
return { type: "json_object" };
|
|
1141
|
+
}
|
|
918
1142
|
return {
|
|
919
|
-
|
|
920
|
-
|
|
1143
|
+
type: "json_schema",
|
|
1144
|
+
json_schema: {
|
|
1145
|
+
name: RESULT_SCHEMA_NAME,
|
|
1146
|
+
strict: true,
|
|
1147
|
+
schema: deriveJsonSchema(translationsResultSchema)
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema", tokenLimitField = "max_completion_tokens") {
|
|
1152
|
+
const base = {
|
|
921
1153
|
messages: [
|
|
922
1154
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
923
1155
|
{ role: "user", content: payloadJson }
|
|
924
1156
|
],
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1157
|
+
model: config.model,
|
|
1158
|
+
response_format: buildResponseFormat(mode)
|
|
1159
|
+
};
|
|
1160
|
+
if (tokenLimitField === "max_tokens") {
|
|
1161
|
+
return { ...base, max_tokens: config.maxOutputTokens };
|
|
1162
|
+
}
|
|
1163
|
+
return { ...base, max_completion_tokens: config.maxOutputTokens };
|
|
1164
|
+
}
|
|
1165
|
+
function advanceStringScan(char, escaped) {
|
|
1166
|
+
if (escaped) {
|
|
1167
|
+
return { inString: true, escaped: false };
|
|
1168
|
+
}
|
|
1169
|
+
if (char === "\\") {
|
|
1170
|
+
return { inString: true, escaped: true };
|
|
1171
|
+
}
|
|
1172
|
+
return { inString: char !== '"', escaped: false };
|
|
1173
|
+
}
|
|
1174
|
+
function scanBalancedObjectEnd(content, start) {
|
|
1175
|
+
let depth = 0;
|
|
1176
|
+
let inString = false;
|
|
1177
|
+
let escaped = false;
|
|
1178
|
+
for (let i = start; i < content.length; i += 1) {
|
|
1179
|
+
const char = content[i];
|
|
1180
|
+
if (inString) {
|
|
1181
|
+
({ inString, escaped } = advanceStringScan(char, escaped));
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (char === '"') {
|
|
1185
|
+
inString = true;
|
|
1186
|
+
} else if (char === "{") {
|
|
1187
|
+
depth += 1;
|
|
1188
|
+
} else if (char === "}") {
|
|
1189
|
+
depth -= 1;
|
|
1190
|
+
if (depth === 0) {
|
|
1191
|
+
return i;
|
|
931
1192
|
}
|
|
932
1193
|
}
|
|
933
|
-
}
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
934
1196
|
}
|
|
935
|
-
function
|
|
1197
|
+
function extractBalancedJson(content) {
|
|
1198
|
+
let searchFrom = 0;
|
|
1199
|
+
while (searchFrom < content.length) {
|
|
1200
|
+
const start = content.indexOf("{", searchFrom);
|
|
1201
|
+
if (start === -1) {
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
const end = scanBalancedObjectEnd(content, start);
|
|
1205
|
+
if (end === null) {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
const candidate = content.slice(start, end + 1);
|
|
1209
|
+
try {
|
|
1210
|
+
JSON.parse(candidate);
|
|
1211
|
+
return candidate;
|
|
1212
|
+
} catch {
|
|
1213
|
+
searchFrom = end + 1;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
function parseContent2(content, tolerant) {
|
|
1219
|
+
const candidate = tolerant ? extractBalancedJson(content) ?? content : content;
|
|
936
1220
|
try {
|
|
937
|
-
return JSON.parse(
|
|
1221
|
+
return JSON.parse(candidate);
|
|
938
1222
|
} catch {
|
|
939
1223
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned unparseable content.");
|
|
940
1224
|
}
|
|
@@ -949,7 +1233,7 @@ function toUsage3(usage) {
|
|
|
949
1233
|
}
|
|
950
1234
|
return { inputTokens: prompt_tokens, outputTokens: completion_tokens };
|
|
951
1235
|
}
|
|
952
|
-
function extractOpenAiResult(completion) {
|
|
1236
|
+
function extractOpenAiResult(completion, tolerant = false) {
|
|
953
1237
|
const choice = completion.choices[0];
|
|
954
1238
|
if (choice === void 0) {
|
|
955
1239
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no message.");
|
|
@@ -962,7 +1246,7 @@ function extractOpenAiResult(completion) {
|
|
|
962
1246
|
if (message.content === void 0 || message.content === null) {
|
|
963
1247
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no translation content.");
|
|
964
1248
|
}
|
|
965
|
-
const raw = parseContent2(message.content);
|
|
1249
|
+
const raw = parseContent2(message.content, tolerant);
|
|
966
1250
|
const usage = toUsage3(completion.usage);
|
|
967
1251
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
968
1252
|
}
|
|
@@ -980,15 +1264,84 @@ function createOpenAiProvider(config, deps = {}) {
|
|
|
980
1264
|
}
|
|
981
1265
|
function createMechanism3(client, config) {
|
|
982
1266
|
return {
|
|
983
|
-
translate: async ({ payloadJson }) => {
|
|
1267
|
+
translate: async ({ payloadJson, signal }) => {
|
|
984
1268
|
const body = buildOpenAiRequest(config, payloadJson);
|
|
985
|
-
const completion = await callClient4(client, body);
|
|
1269
|
+
const completion = await callClient4(client, body, signal);
|
|
986
1270
|
return extractOpenAiResult(completion);
|
|
987
1271
|
}
|
|
988
1272
|
};
|
|
989
1273
|
}
|
|
990
|
-
function callClient4(client, body) {
|
|
991
|
-
return guardProviderCall(
|
|
1274
|
+
function callClient4(client, body, signal) {
|
|
1275
|
+
return guardProviderCall(
|
|
1276
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1277
|
+
signal
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
var HOSTED_PROVIDER_ENV_VARS = new Set(
|
|
1281
|
+
Object.values(PROVIDER_ENV).map((name) => name.toUpperCase())
|
|
1282
|
+
);
|
|
1283
|
+
function isNotHostedProviderEnvVar(value) {
|
|
1284
|
+
return !HOSTED_PROVIDER_ENV_VARS.has(value.toUpperCase());
|
|
1285
|
+
}
|
|
1286
|
+
function isHttpOrHttpsUrl(value) {
|
|
1287
|
+
try {
|
|
1288
|
+
const { protocol } = new URL(value);
|
|
1289
|
+
return protocol === "http:" || protocol === "https:";
|
|
1290
|
+
} catch {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
var openAiCompatibleConfigSchema = z.object({
|
|
1295
|
+
baseUrl: z.url({ message: "baseUrl must be a valid absolute URL." }).refine(isHttpOrHttpsUrl, { message: "baseUrl must use the http or https scheme." }),
|
|
1296
|
+
model: z.string().min(1),
|
|
1297
|
+
maxOutputTokens: z.number().int().positive(),
|
|
1298
|
+
apiKeyEnvVar: z.string().min(1).refine(isNotHostedProviderEnvVar, {
|
|
1299
|
+
message: "apiKeyEnvVar must not name a hosted provider's environment variable."
|
|
1300
|
+
}).optional()
|
|
1301
|
+
});
|
|
1302
|
+
function createDefaultClient5(config) {
|
|
1303
|
+
const sdk = new OpenAI({
|
|
1304
|
+
apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
|
|
1305
|
+
baseURL: config.baseUrl,
|
|
1306
|
+
logLevel: "off"
|
|
1307
|
+
});
|
|
1308
|
+
return {
|
|
1309
|
+
chat: {
|
|
1310
|
+
completions: {
|
|
1311
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1312
|
+
body,
|
|
1313
|
+
options
|
|
1314
|
+
)
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
var PROVIDER_ID5 = "openai-compatible";
|
|
1320
|
+
function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
1321
|
+
const validConfig = openAiCompatibleConfigSchema.parse(config);
|
|
1322
|
+
const client = deps.client ?? createDefaultClient5(validConfig);
|
|
1323
|
+
const mechanism = createMechanism4(client, validConfig);
|
|
1324
|
+
return {
|
|
1325
|
+
id: PROVIDER_ID5,
|
|
1326
|
+
kind: "llm",
|
|
1327
|
+
supportsGlossary: true,
|
|
1328
|
+
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
function createMechanism4(client, config) {
|
|
1332
|
+
return {
|
|
1333
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1334
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
|
|
1335
|
+
const completion = await callClient5(client, body, signal);
|
|
1336
|
+
return extractOpenAiResult(completion, true);
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
function callClient5(client, body, signal) {
|
|
1341
|
+
return guardProviderCall(
|
|
1342
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1343
|
+
signal
|
|
1344
|
+
);
|
|
992
1345
|
}
|
|
993
1346
|
var SCAFFOLD_MODELS = {
|
|
994
1347
|
anthropic: "claude-sonnet-4-6",
|
|
@@ -999,13 +1352,18 @@ var providerConfigSchema = z.discriminatedUnion("id", [
|
|
|
999
1352
|
z.object({ id: z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
|
|
1000
1353
|
z.object({ id: z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
1001
1354
|
z.object({ id: z.literal("gemini"), options: geminiConfigSchema.strict() }),
|
|
1002
|
-
z.object({ id: z.literal("deepl"), options: deepLConfigSchema.strict() })
|
|
1355
|
+
z.object({ id: z.literal("deepl"), options: deepLConfigSchema.strict() }),
|
|
1356
|
+
z.object({
|
|
1357
|
+
id: z.literal("openai-compatible"),
|
|
1358
|
+
options: openAiCompatibleConfigSchema.strict()
|
|
1359
|
+
})
|
|
1003
1360
|
]);
|
|
1004
1361
|
var providerFactories = {
|
|
1005
1362
|
anthropic: (options) => createAnthropicProvider(options),
|
|
1006
1363
|
openai: (options) => createOpenAiProvider(options),
|
|
1007
1364
|
gemini: (options) => createGeminiProvider(options),
|
|
1008
|
-
deepl: (options) => createDeepLProvider(options)
|
|
1365
|
+
deepl: (options) => createDeepLProvider(options),
|
|
1366
|
+
"openai-compatible": (options) => createOpenAiCompatibleProvider(options)
|
|
1009
1367
|
};
|
|
1010
1368
|
function buildProvider(config) {
|
|
1011
1369
|
const create = providerFactories[config.id];
|
|
@@ -1184,6 +1542,12 @@ async function readLockFile(path, fs) {
|
|
|
1184
1542
|
if (!result.success) {
|
|
1185
1543
|
throw new SdkError("LOCK_FILE_INVALID", `The lock-file at ${path} has an unexpected shape.`);
|
|
1186
1544
|
}
|
|
1545
|
+
if (result.data.version !== CURRENT_VERSION) {
|
|
1546
|
+
throw new SdkError(
|
|
1547
|
+
"LOCK_FILE_INVALID",
|
|
1548
|
+
`The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION}.`
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1187
1551
|
return result.data;
|
|
1188
1552
|
}
|
|
1189
1553
|
function baselineFor(lock, locale) {
|
|
@@ -1289,7 +1653,7 @@ function analyzeIcuValue(value) {
|
|
|
1289
1653
|
return VALID_EMPTY;
|
|
1290
1654
|
}
|
|
1291
1655
|
try {
|
|
1292
|
-
const ast = parse
|
|
1656
|
+
const ast = parse(value);
|
|
1293
1657
|
const state = { isPlural: false };
|
|
1294
1658
|
const multiset = collect(ast, state);
|
|
1295
1659
|
return { placeholders: flatten(multiset), isPlural: state.isPlural, valid: true };
|
|
@@ -1316,6 +1680,150 @@ function icuInvalidKeys(entries) {
|
|
|
1316
1680
|
}
|
|
1317
1681
|
return invalid;
|
|
1318
1682
|
}
|
|
1683
|
+
function tokenOf2(element) {
|
|
1684
|
+
switch (element.type) {
|
|
1685
|
+
case TYPE.argument:
|
|
1686
|
+
case TYPE.number:
|
|
1687
|
+
case TYPE.date:
|
|
1688
|
+
case TYPE.time:
|
|
1689
|
+
case TYPE.select:
|
|
1690
|
+
case TYPE.plural:
|
|
1691
|
+
return `{${element.value}}`;
|
|
1692
|
+
case TYPE.tag:
|
|
1693
|
+
return `<${element.value}>`;
|
|
1694
|
+
default:
|
|
1695
|
+
return void 0;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function isBranching(element) {
|
|
1699
|
+
return element.type === TYPE.plural || element.type === TYPE.select;
|
|
1700
|
+
}
|
|
1701
|
+
function isTag(element) {
|
|
1702
|
+
return element.type === TYPE.tag;
|
|
1703
|
+
}
|
|
1704
|
+
function layerTokens(elements) {
|
|
1705
|
+
const tokens = [];
|
|
1706
|
+
for (const element of elements) {
|
|
1707
|
+
const token = tokenOf2(element);
|
|
1708
|
+
if (token !== void 0) {
|
|
1709
|
+
tokens.push(token);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return tokens;
|
|
1713
|
+
}
|
|
1714
|
+
function deepTokens(elements) {
|
|
1715
|
+
const tokens = [];
|
|
1716
|
+
for (const element of elements) {
|
|
1717
|
+
const token = tokenOf2(element);
|
|
1718
|
+
if (token !== void 0) {
|
|
1719
|
+
tokens.push(token);
|
|
1720
|
+
}
|
|
1721
|
+
if (isTag(element)) {
|
|
1722
|
+
tokens.push(...deepTokens(element.children));
|
|
1723
|
+
} else if (isBranching(element)) {
|
|
1724
|
+
for (const option of Object.values(element.options)) {
|
|
1725
|
+
tokens.push(...deepTokens(option.value));
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return tokens;
|
|
1730
|
+
}
|
|
1731
|
+
function branchesByCategory(element) {
|
|
1732
|
+
return new Map(
|
|
1733
|
+
Object.entries(element.options).map(([category, option]) => [category, option.value])
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
function findUnconsumed(target, consumed, isMatch) {
|
|
1737
|
+
for (let index = 0; index < target.length; index += 1) {
|
|
1738
|
+
if (consumed.has(index)) {
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
const candidate = target[index];
|
|
1742
|
+
if (candidate !== void 0 && isMatch(candidate)) {
|
|
1743
|
+
return { element: candidate, index };
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return void 0;
|
|
1747
|
+
}
|
|
1748
|
+
function findMatchingBranching(source, target, consumed) {
|
|
1749
|
+
return findUnconsumed(
|
|
1750
|
+
target,
|
|
1751
|
+
consumed,
|
|
1752
|
+
(candidate) => isBranching(candidate) && candidate.value === source.value
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
function findMatchingTag(source, target, consumed) {
|
|
1756
|
+
return findUnconsumed(
|
|
1757
|
+
target,
|
|
1758
|
+
consumed,
|
|
1759
|
+
(candidate) => isTag(candidate) && candidate.value === source.value
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
function combineResults(results) {
|
|
1763
|
+
const missing = results.flatMap((result) => result.missing).sort();
|
|
1764
|
+
const extra = results.flatMap((result) => result.extra).sort();
|
|
1765
|
+
return { matches: missing.length === 0 && extra.length === 0, missing, extra, reordered: false };
|
|
1766
|
+
}
|
|
1767
|
+
function compareAgainstSourceUnion(sourceBranches, targetBranch) {
|
|
1768
|
+
const union = /* @__PURE__ */ new Set();
|
|
1769
|
+
for (const branch of sourceBranches.values()) {
|
|
1770
|
+
for (const token of deepTokens(branch)) {
|
|
1771
|
+
union.add(token);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const extra = deepTokens(targetBranch).filter((token) => !union.has(token)).sort();
|
|
1775
|
+
return { matches: extra.length === 0, missing: [], extra, reordered: false };
|
|
1776
|
+
}
|
|
1777
|
+
function compareBranching(source, target) {
|
|
1778
|
+
const sourceBranches = branchesByCategory(source);
|
|
1779
|
+
const targetBranches = branchesByCategory(target);
|
|
1780
|
+
const categories = /* @__PURE__ */ new Set([...sourceBranches.keys(), ...targetBranches.keys()]);
|
|
1781
|
+
const results = [];
|
|
1782
|
+
for (const category of categories) {
|
|
1783
|
+
const sourceBranch = sourceBranches.get(category);
|
|
1784
|
+
const targetBranch = targetBranches.get(category);
|
|
1785
|
+
if (sourceBranch !== void 0 && targetBranch !== void 0) {
|
|
1786
|
+
results.push(compareElements(sourceBranch, targetBranch));
|
|
1787
|
+
} else if (targetBranch !== void 0) {
|
|
1788
|
+
results.push(compareAgainstSourceUnion(sourceBranches, targetBranch));
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return combineResults(results);
|
|
1792
|
+
}
|
|
1793
|
+
function nestedResults(source, target) {
|
|
1794
|
+
const results = [];
|
|
1795
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1796
|
+
for (const element of source) {
|
|
1797
|
+
if (isBranching(element)) {
|
|
1798
|
+
const found = findMatchingBranching(element, target, consumed);
|
|
1799
|
+
if (found !== void 0) {
|
|
1800
|
+
consumed.add(found.index);
|
|
1801
|
+
results.push(compareBranching(element, found.element));
|
|
1802
|
+
}
|
|
1803
|
+
} else if (isTag(element)) {
|
|
1804
|
+
const found = findMatchingTag(element, target, consumed);
|
|
1805
|
+
if (found !== void 0) {
|
|
1806
|
+
consumed.add(found.index);
|
|
1807
|
+
results.push(compareElements(element.children, found.element.children));
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return results;
|
|
1812
|
+
}
|
|
1813
|
+
function compareElements(source, target) {
|
|
1814
|
+
const layer = checkPlaceholders(layerTokens(source), layerTokens(target));
|
|
1815
|
+
const nested = nestedResults(source, target);
|
|
1816
|
+
return nested.length === 0 ? layer : combineResults([layer, ...nested]);
|
|
1817
|
+
}
|
|
1818
|
+
function compareIcuPlaceholders(sourceValue, targetValue) {
|
|
1819
|
+
try {
|
|
1820
|
+
const sourceAst = parse(sourceValue);
|
|
1821
|
+
const targetAst = parse(targetValue);
|
|
1822
|
+
return compareElements(sourceAst, targetAst);
|
|
1823
|
+
} catch {
|
|
1824
|
+
return checkPlaceholders(icuPlaceholders(sourceValue), icuPlaceholders(targetValue));
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1319
1827
|
var AdapterError = class extends Error {
|
|
1320
1828
|
code;
|
|
1321
1829
|
constructor(code, message) {
|
|
@@ -1424,6 +1932,9 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1424
1932
|
throw error;
|
|
1425
1933
|
}
|
|
1426
1934
|
}
|
|
1935
|
+
function stripLeadingBom(content) {
|
|
1936
|
+
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
1937
|
+
}
|
|
1427
1938
|
async function readBoundedUtf82(handle, size) {
|
|
1428
1939
|
const buffer = Buffer.allocUnsafe(size);
|
|
1429
1940
|
let offset = 0;
|
|
@@ -1434,7 +1945,7 @@ async function readBoundedUtf82(handle, size) {
|
|
|
1434
1945
|
}
|
|
1435
1946
|
offset += bytesRead;
|
|
1436
1947
|
}
|
|
1437
|
-
return buffer.toString("utf8", 0, offset);
|
|
1948
|
+
return stripLeadingBom(buffer.toString("utf8", 0, offset));
|
|
1438
1949
|
}
|
|
1439
1950
|
async function readBounded2(filePath) {
|
|
1440
1951
|
const handle = await open(filePath, "r");
|
|
@@ -1617,9 +2128,9 @@ function unflattenEntries(entries) {
|
|
|
1617
2128
|
}
|
|
1618
2129
|
return root;
|
|
1619
2130
|
}
|
|
1620
|
-
function toEntries(content, namespace,
|
|
2131
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree) {
|
|
1621
2132
|
try {
|
|
1622
|
-
const tree =
|
|
2133
|
+
const tree = parse3(content);
|
|
1623
2134
|
validateTree?.(tree);
|
|
1624
2135
|
return flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
1625
2136
|
} catch (error) {
|
|
@@ -1631,7 +2142,7 @@ function createTreeFileAdapter(options) {
|
|
|
1631
2142
|
format,
|
|
1632
2143
|
extensions,
|
|
1633
2144
|
sniff,
|
|
1634
|
-
parse:
|
|
2145
|
+
parse: parse3,
|
|
1635
2146
|
serialize,
|
|
1636
2147
|
deriveEntry,
|
|
1637
2148
|
extractPlaceholders,
|
|
@@ -1639,6 +2150,7 @@ function createTreeFileAdapter(options) {
|
|
|
1639
2150
|
validateMessage,
|
|
1640
2151
|
validateTree,
|
|
1641
2152
|
buildWriteTree,
|
|
2153
|
+
comparePlaceholders,
|
|
1642
2154
|
keyMode = "literal-leaf"
|
|
1643
2155
|
} = options;
|
|
1644
2156
|
return {
|
|
@@ -1646,10 +2158,11 @@ function createTreeFileAdapter(options) {
|
|
|
1646
2158
|
canHandle: buildCanHandle(extensions, sniff),
|
|
1647
2159
|
extractPlaceholders,
|
|
1648
2160
|
validateMessage: validateMessage ?? (() => true),
|
|
2161
|
+
...comparePlaceholders !== void 0 ? { comparePlaceholders } : {},
|
|
1649
2162
|
async read(filePath, locale) {
|
|
1650
2163
|
const content = await readFileContent(filePath);
|
|
1651
2164
|
const namespace = namespaceOf(filePath);
|
|
1652
|
-
const entries = toEntries(content, namespace,
|
|
2165
|
+
const entries = toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree);
|
|
1653
2166
|
const resource = { locale, namespace, format, entries };
|
|
1654
2167
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1655
2168
|
return { resource, invalidIcuKeys };
|
|
@@ -1745,6 +2258,7 @@ function createArbAdapter() {
|
|
|
1745
2258
|
deriveEntry: icuDeriveEntry,
|
|
1746
2259
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1747
2260
|
validateMessage: icuIsValid,
|
|
2261
|
+
comparePlaceholders: compareIcuPlaceholders,
|
|
1748
2262
|
buildWriteTree: buildArbWriteTree
|
|
1749
2263
|
});
|
|
1750
2264
|
}
|
|
@@ -1808,7 +2322,8 @@ function createNextIntlJsonAdapter() {
|
|
|
1808
2322
|
extractPlaceholders: icuPlaceholders,
|
|
1809
2323
|
deriveEntry: icuDeriveEntry,
|
|
1810
2324
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1811
|
-
validateMessage: icuIsValid
|
|
2325
|
+
validateMessage: icuIsValid,
|
|
2326
|
+
comparePlaceholders: compareIcuPlaceholders
|
|
1812
2327
|
});
|
|
1813
2328
|
}
|
|
1814
2329
|
function assertNoDottedNestedKey(tree) {
|
|
@@ -2010,6 +2525,7 @@ function extractXliffPlaceholders(value) {
|
|
|
2010
2525
|
return result;
|
|
2011
2526
|
}
|
|
2012
2527
|
var ELEMENT_NODE = 1;
|
|
2528
|
+
var TEXT_NODE = 3;
|
|
2013
2529
|
function isElement(node) {
|
|
2014
2530
|
return node.nodeType === ELEMENT_NODE;
|
|
2015
2531
|
}
|
|
@@ -2100,6 +2616,12 @@ function parseXliffEntries(content, namespace) {
|
|
|
2100
2616
|
const serializer = new XMLSerializer();
|
|
2101
2617
|
const out = /* @__PURE__ */ new Map();
|
|
2102
2618
|
for (const unit of walkUnits(root)) {
|
|
2619
|
+
if (out.has(unit.key)) {
|
|
2620
|
+
throw new AdapterError(
|
|
2621
|
+
"INVALID_STRUCTURE",
|
|
2622
|
+
"The XLIFF file has two trans-units with the same id."
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2103
2625
|
const value = unitValue(serializer, unit);
|
|
2104
2626
|
out.set(unit.key, {
|
|
2105
2627
|
key: unit.key,
|
|
@@ -2126,10 +2648,67 @@ async function readDestination(filePath) {
|
|
|
2126
2648
|
}
|
|
2127
2649
|
return outcome.content;
|
|
2128
2650
|
}
|
|
2651
|
+
var XLIFF_INLINE_ELEMENTS = /* @__PURE__ */ new Set(["x", "g", "bx", "ex", "ph", "it", "mrk"]);
|
|
2652
|
+
var XLIFF_NAMESPACES = /* @__PURE__ */ new Set([
|
|
2653
|
+
"urn:oasis:names:tc:xliff:document:1.2",
|
|
2654
|
+
"urn:oasis:names:tc:xliff:document:2.0"
|
|
2655
|
+
]);
|
|
2656
|
+
var INLINE_ELEMENT_ATTRIBUTES = {
|
|
2657
|
+
x: ["id", "ctype"],
|
|
2658
|
+
ph: ["id", "ctype"],
|
|
2659
|
+
g: ["id", "ctype"],
|
|
2660
|
+
bx: ["id", "rid", "ctype"],
|
|
2661
|
+
ex: ["id", "rid"],
|
|
2662
|
+
it: ["id", "pos", "ctype"],
|
|
2663
|
+
mrk: ["id", "mtype"]
|
|
2664
|
+
};
|
|
2665
|
+
function isAllowedInlineElement(element) {
|
|
2666
|
+
const namespace = element.namespaceURI;
|
|
2667
|
+
const hasAllowedNamespace = namespace === null || XLIFF_NAMESPACES.has(namespace);
|
|
2668
|
+
return hasAllowedNamespace && XLIFF_INLINE_ELEMENTS.has(element.localName ?? "");
|
|
2669
|
+
}
|
|
2670
|
+
function isAllowedFragmentNode(node) {
|
|
2671
|
+
if (node.nodeType === TEXT_NODE) {
|
|
2672
|
+
return true;
|
|
2673
|
+
}
|
|
2674
|
+
return isElement(node) && isAllowedInlineElement(node);
|
|
2675
|
+
}
|
|
2676
|
+
function allDescendantNodes(node) {
|
|
2677
|
+
const children = Array.from(node.childNodes);
|
|
2678
|
+
return children.flatMap((child) => [child, ...allDescendantNodes(child)]);
|
|
2679
|
+
}
|
|
2680
|
+
function hasDisallowedNode(root) {
|
|
2681
|
+
return allDescendantNodes(root).some((node) => !isAllowedFragmentNode(node));
|
|
2682
|
+
}
|
|
2683
|
+
function attributeNames(element) {
|
|
2684
|
+
const names = [];
|
|
2685
|
+
for (let i = 0; i < element.attributes.length; i += 1) {
|
|
2686
|
+
const attr = element.attributes.item(i);
|
|
2687
|
+
if (attr !== null) {
|
|
2688
|
+
names.push(attr.name);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return names;
|
|
2692
|
+
}
|
|
2693
|
+
function sanitizeInlineAttributes(root) {
|
|
2694
|
+
for (const el of collectByTag(root, "*")) {
|
|
2695
|
+
const allowed = INLINE_ELEMENT_ATTRIBUTES[el.localName ?? ""] ?? [];
|
|
2696
|
+
for (const name of attributeNames(el)) {
|
|
2697
|
+
if (!allowed.includes(name)) {
|
|
2698
|
+
el.removeAttribute(name);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2129
2703
|
function fragmentNodes(parser, value) {
|
|
2704
|
+
assertNoDoctype(value);
|
|
2130
2705
|
try {
|
|
2131
2706
|
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
2132
|
-
|
|
2707
|
+
if (root === null || hasDisallowedNode(root)) {
|
|
2708
|
+
return null;
|
|
2709
|
+
}
|
|
2710
|
+
sanitizeInlineAttributes(root);
|
|
2711
|
+
return Array.from(root.childNodes);
|
|
2133
2712
|
} catch {
|
|
2134
2713
|
return null;
|
|
2135
2714
|
}
|
|
@@ -2179,7 +2758,7 @@ function createXliffAdapter() {
|
|
|
2179
2758
|
function parseYamlObject(content) {
|
|
2180
2759
|
let parsed;
|
|
2181
2760
|
try {
|
|
2182
|
-
parsed = parse(content, { maxAliasCount: 100 });
|
|
2761
|
+
parsed = parse$1(content, { maxAliasCount: 100 });
|
|
2183
2762
|
} catch {
|
|
2184
2763
|
throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
|
|
2185
2764
|
}
|
|
@@ -2409,15 +2988,8 @@ function subBatchFailedNotice(count, error) {
|
|
|
2409
2988
|
}
|
|
2410
2989
|
|
|
2411
2990
|
// src/flow/notices.ts
|
|
2412
|
-
function isNotice(value) {
|
|
2413
|
-
return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
|
|
2414
|
-
}
|
|
2415
2991
|
function readNotices(result) {
|
|
2416
|
-
|
|
2417
|
-
if (!Array.isArray(candidate)) {
|
|
2418
|
-
return [];
|
|
2419
|
-
}
|
|
2420
|
-
return candidate.filter(isNotice);
|
|
2992
|
+
return result.notices ?? [];
|
|
2421
2993
|
}
|
|
2422
2994
|
|
|
2423
2995
|
// src/flow/plural-categories.ts
|
|
@@ -2583,7 +3155,8 @@ function buildRequest2(context, entries) {
|
|
|
2583
3155
|
entries,
|
|
2584
3156
|
extractPlaceholders: context.adapter.extractPlaceholders,
|
|
2585
3157
|
...context.glossary !== void 0 ? { glossary: context.glossary } : {},
|
|
2586
|
-
...context.tone !== void 0 ? { tone: context.tone } : {}
|
|
3158
|
+
...context.tone !== void 0 ? { tone: context.tone } : {},
|
|
3159
|
+
...context.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: context.adapter.comparePlaceholders } : {}
|
|
2587
3160
|
};
|
|
2588
3161
|
}
|
|
2589
3162
|
async function generatePluralForms(context) {
|
|
@@ -2620,20 +3193,26 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
2620
3193
|
return [subBatchFailedNotice(batch.length, error)];
|
|
2621
3194
|
}
|
|
2622
3195
|
for (const item of batch) {
|
|
2623
|
-
|
|
2624
|
-
const integrity = result.integrity.get(item.targetKey);
|
|
2625
|
-
if (value !== void 0 && integrity?.matches === true) {
|
|
2626
|
-
accepted.push({
|
|
2627
|
-
targetKey: item.targetKey,
|
|
2628
|
-
entry: { ...syntheticEntry(item), value },
|
|
2629
|
-
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
2630
|
-
});
|
|
2631
|
-
} else {
|
|
2632
|
-
withheld.push(item.targetKey);
|
|
2633
|
-
}
|
|
3196
|
+
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
2634
3197
|
}
|
|
2635
3198
|
return readNotices(result);
|
|
2636
3199
|
}
|
|
3200
|
+
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
3201
|
+
const value = result.values.get(item.targetKey);
|
|
3202
|
+
if (value === void 0) {
|
|
3203
|
+
providerFailures.push(item.targetKey);
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
if (result.integrity.get(item.targetKey)?.matches === true) {
|
|
3207
|
+
accepted.push({
|
|
3208
|
+
targetKey: item.targetKey,
|
|
3209
|
+
entry: { ...syntheticEntry(item), value },
|
|
3210
|
+
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
3211
|
+
});
|
|
3212
|
+
} else {
|
|
3213
|
+
withheld.push(item.targetKey);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
2637
3216
|
|
|
2638
3217
|
// src/flow/locale-run.ts
|
|
2639
3218
|
function emptyResource(locale, format) {
|
|
@@ -2653,7 +3232,8 @@ function buildRequest3(params, entries) {
|
|
|
2653
3232
|
entries,
|
|
2654
3233
|
extractPlaceholders: params.adapter.extractPlaceholders,
|
|
2655
3234
|
...params.glossary !== void 0 ? { glossary: params.glossary } : {},
|
|
2656
|
-
...params.tone !== void 0 ? { tone: params.tone } : {}
|
|
3235
|
+
...params.tone !== void 0 ? { tone: params.tone } : {},
|
|
3236
|
+
...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
|
|
2657
3237
|
};
|
|
2658
3238
|
}
|
|
2659
3239
|
async function runLocale(params) {
|
|
@@ -2820,16 +3400,22 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
2820
3400
|
return [subBatchFailedNotice(batch.length, error)];
|
|
2821
3401
|
}
|
|
2822
3402
|
for (const entry of batch) {
|
|
2823
|
-
|
|
2824
|
-
const integrity = result.integrity.get(entry.key);
|
|
2825
|
-
if (value !== void 0 && integrity?.matches === true) {
|
|
2826
|
-
accepted.set(entry.key, { value, source: entry });
|
|
2827
|
-
} else {
|
|
2828
|
-
integrityMismatches.push(entry.key);
|
|
2829
|
-
}
|
|
3403
|
+
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
2830
3404
|
}
|
|
2831
3405
|
return readNotices(result);
|
|
2832
3406
|
}
|
|
3407
|
+
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
3408
|
+
const value = result.values.get(entry.key);
|
|
3409
|
+
if (value === void 0) {
|
|
3410
|
+
providerFailures.push(entry.key);
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
if (result.integrity.get(entry.key)?.matches === true) {
|
|
3414
|
+
accepted.set(entry.key, { value, source: entry });
|
|
3415
|
+
} else {
|
|
3416
|
+
integrityMismatches.push(entry.key);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
2833
3419
|
function computeLockEntries(params, merged, withheld, generated) {
|
|
2834
3420
|
const lockEntries = {};
|
|
2835
3421
|
const sourceBaseKeys = sourcePluralBaseKeys(params.source);
|
|
@@ -3116,7 +3702,7 @@ async function streamEntryBounded(file, remaining) {
|
|
|
3116
3702
|
"The workbook decompresses to more than the permitted maximum bytes."
|
|
3117
3703
|
);
|
|
3118
3704
|
}
|
|
3119
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
3705
|
+
return { raw, content: Buffer.concat(chunks).toString("utf8") };
|
|
3120
3706
|
}
|
|
3121
3707
|
async function guardWorkbookBytes(bytes, limits) {
|
|
3122
3708
|
let zip;
|
|
@@ -3148,14 +3734,8 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
3148
3734
|
let actualTotal = 0;
|
|
3149
3735
|
for (const file of files) {
|
|
3150
3736
|
const remaining = limits.maxDecompressedBytes - actualTotal;
|
|
3151
|
-
const content = await streamEntryBounded(file, remaining);
|
|
3152
|
-
actualTotal +=
|
|
3153
|
-
if (actualTotal > limits.maxDecompressedBytes) {
|
|
3154
|
-
throw new ExchangeError(
|
|
3155
|
-
"WORKBOOK_INVALID",
|
|
3156
|
-
`The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
|
|
3157
|
-
);
|
|
3158
|
-
}
|
|
3737
|
+
const { raw, content } = await streamEntryBounded(file, remaining);
|
|
3738
|
+
actualTotal += raw;
|
|
3159
3739
|
assertNoDoctype2(file.name, content);
|
|
3160
3740
|
}
|
|
3161
3741
|
}
|
|
@@ -3344,10 +3924,7 @@ function judge(row, sourceEntry, adapter) {
|
|
|
3344
3924
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
3345
3925
|
return "drift";
|
|
3346
3926
|
}
|
|
3347
|
-
const integrity = checkPlaceholders(
|
|
3348
|
-
sourceEntry.placeholders,
|
|
3349
|
-
adapter.extractPlaceholders(row.translation)
|
|
3350
|
-
);
|
|
3927
|
+
const integrity = adapter.comparePlaceholders !== void 0 ? adapter.comparePlaceholders(sourceEntry.value, row.translation) : checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(row.translation));
|
|
3351
3928
|
if (!integrity.matches) {
|
|
3352
3929
|
return "placeholder";
|
|
3353
3930
|
}
|