@verbatra/sdk 0.5.0-next.0 → 0.5.0-next.1
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 +0 -1
- package/dist/index.cjs +601 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -7
- package/dist/index.d.ts +85 -7
- package/dist/index.js +605 -76
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -341,6 +341,85 @@ var LOCALE_TOKEN = "{locale}";
|
|
|
341
341
|
function localeFilePath(cwd, pattern, locale) {
|
|
342
342
|
return path.resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
|
|
343
343
|
}
|
|
344
|
+
var RATE_LIMITED_STATUS = 429;
|
|
345
|
+
var TIMEOUT_STATUS = 408;
|
|
346
|
+
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
347
|
+
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
348
|
+
"RateLimitError",
|
|
349
|
+
// openai, @anthropic-ai/sdk
|
|
350
|
+
"TooManyRequestsError"
|
|
351
|
+
// deepl-node
|
|
352
|
+
]);
|
|
353
|
+
var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
354
|
+
"AuthenticationError",
|
|
355
|
+
// openai, @anthropic-ai/sdk (401)
|
|
356
|
+
"PermissionDeniedError",
|
|
357
|
+
// openai, @anthropic-ai/sdk (403)
|
|
358
|
+
"AuthorizationError"
|
|
359
|
+
// deepl-node
|
|
360
|
+
]);
|
|
361
|
+
var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
362
|
+
"APIConnectionTimeoutError",
|
|
363
|
+
// openai, @anthropic-ai/sdk
|
|
364
|
+
// deepl-node has no distinct timeout class; ConnectionError is its only network-failure
|
|
365
|
+
// class, so it is folded into TIMEOUT rather than the PROVIDER_ERROR fallback.
|
|
366
|
+
"ConnectionError"
|
|
367
|
+
]);
|
|
368
|
+
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
369
|
+
"APIUserAbortError"
|
|
370
|
+
// openai, @anthropic-ai/sdk
|
|
371
|
+
]);
|
|
372
|
+
function getErrorStatus(error) {
|
|
373
|
+
if (typeof error !== "object" || error === null || !("status" in error)) {
|
|
374
|
+
return void 0;
|
|
375
|
+
}
|
|
376
|
+
const status = error.status;
|
|
377
|
+
return typeof status === "number" ? status : void 0;
|
|
378
|
+
}
|
|
379
|
+
function readClassName(error) {
|
|
380
|
+
if (typeof error !== "object" || error === null) {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
return error.constructor?.name;
|
|
384
|
+
}
|
|
385
|
+
function classifyProviderError(error) {
|
|
386
|
+
const status = getErrorStatus(error);
|
|
387
|
+
if (status === RATE_LIMITED_STATUS) {
|
|
388
|
+
return "RATE_LIMITED";
|
|
389
|
+
}
|
|
390
|
+
if (status !== void 0 && AUTH_FAILED_STATUSES.has(status)) {
|
|
391
|
+
return "AUTH_FAILED";
|
|
392
|
+
}
|
|
393
|
+
if (status === TIMEOUT_STATUS) {
|
|
394
|
+
return "TIMEOUT";
|
|
395
|
+
}
|
|
396
|
+
const className = readClassName(error);
|
|
397
|
+
if (className !== void 0) {
|
|
398
|
+
if (RATE_LIMITED_CLASS_NAMES.has(className)) {
|
|
399
|
+
return "RATE_LIMITED";
|
|
400
|
+
}
|
|
401
|
+
if (AUTH_FAILED_CLASS_NAMES.has(className)) {
|
|
402
|
+
return "AUTH_FAILED";
|
|
403
|
+
}
|
|
404
|
+
if (TIMEOUT_CLASS_NAMES.has(className)) {
|
|
405
|
+
return "TIMEOUT";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return "PROVIDER_ERROR";
|
|
409
|
+
}
|
|
410
|
+
function isAbortShapedError(error) {
|
|
411
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
const className = readClassName(error);
|
|
415
|
+
return className !== void 0 && ABORT_ERROR_CLASS_NAMES.has(className);
|
|
416
|
+
}
|
|
417
|
+
function isAbortError(error, signal) {
|
|
418
|
+
if (signal !== void 0) {
|
|
419
|
+
return signal.aborted === true && isAbortShapedError(error);
|
|
420
|
+
}
|
|
421
|
+
return isAbortShapedError(error);
|
|
422
|
+
}
|
|
344
423
|
var REDACTED = "[REDACTED]";
|
|
345
424
|
var KEY_PATTERNS = [
|
|
346
425
|
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
@@ -375,17 +454,34 @@ var ProviderError = class extends Error {
|
|
|
375
454
|
}
|
|
376
455
|
};
|
|
377
456
|
var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
|
|
378
|
-
|
|
457
|
+
var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
|
|
458
|
+
var TIMEOUT_MESSAGE = "The translation provider request timed out.";
|
|
459
|
+
var AUTH_FAILED_MESSAGE = "The translation provider rejected the request credentials.";
|
|
460
|
+
var MESSAGE_BY_CODE = {
|
|
461
|
+
RATE_LIMITED: RATE_LIMITED_MESSAGE,
|
|
462
|
+
TIMEOUT: TIMEOUT_MESSAGE,
|
|
463
|
+
AUTH_FAILED: AUTH_FAILED_MESSAGE,
|
|
464
|
+
PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
|
|
465
|
+
};
|
|
466
|
+
async function guardProviderCall(call, signal) {
|
|
467
|
+
signal?.throwIfAborted();
|
|
379
468
|
try {
|
|
380
469
|
return await call();
|
|
381
|
-
} catch {
|
|
382
|
-
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (isAbortError(error, signal)) {
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
const code = classifyProviderError(error);
|
|
475
|
+
throw new ProviderError(code, MESSAGE_BY_CODE[code]);
|
|
383
476
|
}
|
|
384
477
|
}
|
|
385
|
-
function checkBatchIntegrity(inputs, extract) {
|
|
478
|
+
function checkBatchIntegrity(inputs, extract, compare) {
|
|
386
479
|
const integrity = /* @__PURE__ */ new Map();
|
|
387
|
-
for (const { key,
|
|
388
|
-
integrity.set(
|
|
480
|
+
for (const { key, sourceValue, translatedValue } of inputs) {
|
|
481
|
+
integrity.set(
|
|
482
|
+
key,
|
|
483
|
+
compare !== void 0 ? compare(sourceValue, translatedValue) : checkPlaceholders(extract(sourceValue), extract(translatedValue))
|
|
484
|
+
);
|
|
389
485
|
}
|
|
390
486
|
return integrity;
|
|
391
487
|
}
|
|
@@ -415,7 +511,7 @@ function toIntegrityInputs(entries, values) {
|
|
|
415
511
|
"The provider response is missing one or more keys."
|
|
416
512
|
);
|
|
417
513
|
}
|
|
418
|
-
return { key: entry.key,
|
|
514
|
+
return { key: entry.key, sourceValue: entry.value, translatedValue };
|
|
419
515
|
});
|
|
420
516
|
}
|
|
421
517
|
function toItem(entry) {
|
|
@@ -482,11 +578,16 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
482
578
|
const data = validateRequest(request);
|
|
483
579
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
484
580
|
const requestedKeys = data.entries.map((entry) => entry.key);
|
|
485
|
-
const completion = await mechanism.translate({
|
|
581
|
+
const completion = await mechanism.translate({
|
|
582
|
+
payloadJson,
|
|
583
|
+
requestedKeys,
|
|
584
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
585
|
+
});
|
|
486
586
|
const values = reconcileResult(completion.raw, requestedKeys);
|
|
487
587
|
const integrity = checkBatchIntegrity(
|
|
488
588
|
toIntegrityInputs(data.entries, values),
|
|
489
|
-
request.extractPlaceholders
|
|
589
|
+
request.extractPlaceholders,
|
|
590
|
+
request.comparePlaceholders
|
|
490
591
|
);
|
|
491
592
|
return completion.usage === void 0 ? { values, integrity } : { values, integrity, usage: completion.usage };
|
|
492
593
|
}
|
|
@@ -521,12 +622,29 @@ function requireGeminiKey() {
|
|
|
521
622
|
function requireDeepLKey() {
|
|
522
623
|
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
523
624
|
}
|
|
625
|
+
var OPENAI_COMPATIBLE_ENV_VAR = "OPENAI_COMPATIBLE_API_KEY";
|
|
626
|
+
var OPENAI_COMPATIBLE_KEY_PLACEHOLDER = "local";
|
|
627
|
+
function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
628
|
+
const varName = customEnvVar ?? OPENAI_COMPATIBLE_ENV_VAR;
|
|
629
|
+
const value = process.env[varName];
|
|
630
|
+
if (value !== void 0 && value.length > 0) {
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
if (customEnvVar !== void 0) {
|
|
634
|
+
throw new ProviderError(
|
|
635
|
+
"MISSING_API_KEY",
|
|
636
|
+
`The ${customEnvVar} environment variable is not set.`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return OPENAI_COMPATIBLE_KEY_PLACEHOLDER;
|
|
640
|
+
}
|
|
524
641
|
function createDefaultClient() {
|
|
525
642
|
const sdk = new Anthropic__default.default({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
526
643
|
return {
|
|
527
644
|
messages: {
|
|
528
|
-
create: async (body) => await sdk.messages.create(
|
|
529
|
-
body
|
|
645
|
+
create: async (body, options) => await sdk.messages.create(
|
|
646
|
+
body,
|
|
647
|
+
options
|
|
530
648
|
)
|
|
531
649
|
}
|
|
532
650
|
};
|
|
@@ -594,9 +712,9 @@ function createAnthropicProvider(config, deps = {}) {
|
|
|
594
712
|
}
|
|
595
713
|
function createMechanism(client, config) {
|
|
596
714
|
return {
|
|
597
|
-
translate: async ({ payloadJson }) => {
|
|
715
|
+
translate: async ({ payloadJson, signal }) => {
|
|
598
716
|
const body = buildRequest(config, payloadJson);
|
|
599
|
-
const message = await callClient(client, body);
|
|
717
|
+
const message = await callClient(client, body, signal);
|
|
600
718
|
assertNotTruncated(message.stop_reason === "max_tokens");
|
|
601
719
|
const raw = requireToolInput(message.content);
|
|
602
720
|
const usage = toUsage(message.usage);
|
|
@@ -604,8 +722,11 @@ function createMechanism(client, config) {
|
|
|
604
722
|
}
|
|
605
723
|
};
|
|
606
724
|
}
|
|
607
|
-
function callClient(client, body) {
|
|
608
|
-
return guardProviderCall(
|
|
725
|
+
function callClient(client, body, signal) {
|
|
726
|
+
return guardProviderCall(
|
|
727
|
+
() => client.messages.create(body, signal !== void 0 ? { signal } : void 0),
|
|
728
|
+
signal
|
|
729
|
+
);
|
|
609
730
|
}
|
|
610
731
|
function toUsage(usage) {
|
|
611
732
|
if (usage === void 0) {
|
|
@@ -700,7 +821,7 @@ function zipResults(entries, results) {
|
|
|
700
821
|
values.set(entry.key, translatedValue);
|
|
701
822
|
integrityInputs.push({
|
|
702
823
|
key: entry.key,
|
|
703
|
-
|
|
824
|
+
sourceValue: entry.value,
|
|
704
825
|
translatedValue
|
|
705
826
|
});
|
|
706
827
|
}
|
|
@@ -741,36 +862,97 @@ async function translate(bundle, config, request) {
|
|
|
741
862
|
data,
|
|
742
863
|
protectable,
|
|
743
864
|
options,
|
|
744
|
-
request.extractPlaceholders
|
|
865
|
+
request.extractPlaceholders,
|
|
866
|
+
request.comparePlaceholders,
|
|
867
|
+
request.signal
|
|
745
868
|
);
|
|
746
869
|
if (unprotectable.length > 0) {
|
|
747
870
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
748
871
|
}
|
|
749
872
|
return { values, integrity, notices };
|
|
750
873
|
}
|
|
751
|
-
async function translateProtectable(client, data, protectable, options, extract) {
|
|
874
|
+
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
752
875
|
if (protectable.length === 0) {
|
|
753
876
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
754
877
|
}
|
|
755
878
|
const texts = protectable.map((entry) => entry.value);
|
|
756
|
-
const results = await callClient2(
|
|
879
|
+
const results = await callClient2(
|
|
880
|
+
client,
|
|
881
|
+
texts,
|
|
882
|
+
data.sourceLocale,
|
|
883
|
+
data.targetLocale,
|
|
884
|
+
options,
|
|
885
|
+
signal
|
|
886
|
+
);
|
|
757
887
|
const { values, integrityInputs } = zipResults(protectable, results);
|
|
758
|
-
const integrity = checkBatchIntegrity(integrityInputs, extract);
|
|
888
|
+
const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
|
|
759
889
|
return { values, integrity };
|
|
760
890
|
}
|
|
761
|
-
function callClient2(client, texts, sourceLang, targetLang, options) {
|
|
762
|
-
return guardProviderCall(
|
|
891
|
+
function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
892
|
+
return guardProviderCall(
|
|
893
|
+
() => client.translateText(texts, sourceLang, targetLang, options),
|
|
894
|
+
signal
|
|
895
|
+
);
|
|
763
896
|
}
|
|
764
897
|
var geminiConfigSchema = zod.z.object({
|
|
765
898
|
model: zod.z.string().min(1),
|
|
766
899
|
maxOutputTokens: zod.z.number().int().positive()
|
|
767
900
|
});
|
|
901
|
+
var DEFAULT_GEMINI_RETRY = { attempts: 3, baseDelayMs: 250 };
|
|
902
|
+
function isRetryableStatus(error) {
|
|
903
|
+
const status = getErrorStatus(error);
|
|
904
|
+
return status === 429 || status !== void 0 && status >= 500 && status < 600;
|
|
905
|
+
}
|
|
906
|
+
function isAborted(signal) {
|
|
907
|
+
return signal?.aborted === true;
|
|
908
|
+
}
|
|
909
|
+
function throwAbort(signal) {
|
|
910
|
+
signal?.throwIfAborted();
|
|
911
|
+
throw new DOMException("This operation was aborted.", "AbortError");
|
|
912
|
+
}
|
|
913
|
+
function delay(ms, signal) {
|
|
914
|
+
return new Promise((resolve7) => {
|
|
915
|
+
const onAbort = () => {
|
|
916
|
+
clearTimeout(timer);
|
|
917
|
+
resolve7();
|
|
918
|
+
};
|
|
919
|
+
const timer = setTimeout(() => {
|
|
920
|
+
signal?.removeEventListener("abort", onAbort);
|
|
921
|
+
resolve7();
|
|
922
|
+
}, ms);
|
|
923
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
async function withGeminiRetry(call, signal, config = DEFAULT_GEMINI_RETRY) {
|
|
927
|
+
let attempt = 1;
|
|
928
|
+
while (true) {
|
|
929
|
+
try {
|
|
930
|
+
return await call();
|
|
931
|
+
} catch (error) {
|
|
932
|
+
if (isAborted(signal)) {
|
|
933
|
+
throwAbort(signal);
|
|
934
|
+
}
|
|
935
|
+
const exhausted = attempt >= config.attempts;
|
|
936
|
+
if (exhausted || !isRetryableStatus(error)) {
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
939
|
+
await delay(config.baseDelayMs * 2 ** (attempt - 1), signal);
|
|
940
|
+
if (isAborted(signal)) {
|
|
941
|
+
throwAbort(signal);
|
|
942
|
+
}
|
|
943
|
+
attempt += 1;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
768
947
|
function createDefaultClient3() {
|
|
769
948
|
const ai = new genai.GoogleGenAI({ apiKey: requireGeminiKey() });
|
|
770
949
|
return {
|
|
771
950
|
models: {
|
|
772
|
-
generateContent:
|
|
773
|
-
|
|
951
|
+
generateContent: (request) => withGeminiRetry(
|
|
952
|
+
async () => await ai.models.generateContent(
|
|
953
|
+
request
|
|
954
|
+
),
|
|
955
|
+
request.config.abortSignal
|
|
774
956
|
)
|
|
775
957
|
}
|
|
776
958
|
};
|
|
@@ -834,7 +1016,7 @@ var GEMINI_SYSTEM_RULES = [
|
|
|
834
1016
|
"When a tone is provided, honor it.",
|
|
835
1017
|
"Respond only with the structured object: exactly one entry per requested key, no commentary, no extra keys, and no key that was not requested."
|
|
836
1018
|
].join("\n");
|
|
837
|
-
function buildGeminiRequest(config, payloadJson) {
|
|
1019
|
+
function buildGeminiRequest(config, payloadJson, signal) {
|
|
838
1020
|
return {
|
|
839
1021
|
model: config.model,
|
|
840
1022
|
contents: [{ role: "user", parts: [{ text: payloadJson }] }],
|
|
@@ -842,7 +1024,8 @@ function buildGeminiRequest(config, payloadJson) {
|
|
|
842
1024
|
systemInstruction: GEMINI_SYSTEM_RULES,
|
|
843
1025
|
responseMimeType: "application/json",
|
|
844
1026
|
responseSchema: toGeminiSchema(deriveJsonSchema(translationsResultSchema)),
|
|
845
|
-
maxOutputTokens: config.maxOutputTokens
|
|
1027
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
1028
|
+
...signal !== void 0 ? { abortSignal: signal } : {}
|
|
846
1029
|
}
|
|
847
1030
|
};
|
|
848
1031
|
}
|
|
@@ -906,15 +1089,15 @@ function createGeminiProvider(config, deps = {}) {
|
|
|
906
1089
|
}
|
|
907
1090
|
function createMechanism2(client, config) {
|
|
908
1091
|
return {
|
|
909
|
-
translate: async ({ payloadJson }) => {
|
|
910
|
-
const request = buildGeminiRequest(config, payloadJson);
|
|
911
|
-
const response = await callClient3(client, request);
|
|
1092
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1093
|
+
const request = buildGeminiRequest(config, payloadJson, signal);
|
|
1094
|
+
const response = await callClient3(client, request, signal);
|
|
912
1095
|
return extractGeminiResult(response);
|
|
913
1096
|
}
|
|
914
1097
|
};
|
|
915
1098
|
}
|
|
916
|
-
function callClient3(client, request) {
|
|
917
|
-
return guardProviderCall(() => client.models.generateContent(request));
|
|
1099
|
+
function callClient3(client, request, signal) {
|
|
1100
|
+
return guardProviderCall(() => client.models.generateContent(request), signal);
|
|
918
1101
|
}
|
|
919
1102
|
var openAiConfigSchema = zod.z.object({
|
|
920
1103
|
model: zod.z.string().min(1),
|
|
@@ -925,8 +1108,9 @@ function createDefaultClient4() {
|
|
|
925
1108
|
return {
|
|
926
1109
|
chat: {
|
|
927
1110
|
completions: {
|
|
928
|
-
create: async (body) => await sdk.chat.completions.create(
|
|
929
|
-
body
|
|
1111
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1112
|
+
body,
|
|
1113
|
+
options
|
|
930
1114
|
)
|
|
931
1115
|
}
|
|
932
1116
|
}
|
|
@@ -944,7 +1128,20 @@ var OPENAI_SYSTEM_RULES = [
|
|
|
944
1128
|
"When a tone is provided, honor it.",
|
|
945
1129
|
"Respond only with the structured object: exactly one entry per requested key, no commentary, no extra keys, and no key that was not requested."
|
|
946
1130
|
].join("\n");
|
|
947
|
-
function
|
|
1131
|
+
function buildResponseFormat(mode) {
|
|
1132
|
+
if (mode === "json-object") {
|
|
1133
|
+
return { type: "json_object" };
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
type: "json_schema",
|
|
1137
|
+
json_schema: {
|
|
1138
|
+
name: RESULT_SCHEMA_NAME,
|
|
1139
|
+
strict: true,
|
|
1140
|
+
schema: deriveJsonSchema(translationsResultSchema)
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema") {
|
|
948
1145
|
return {
|
|
949
1146
|
model: config.model,
|
|
950
1147
|
max_completion_tokens: config.maxOutputTokens,
|
|
@@ -952,19 +1149,66 @@ function buildOpenAiRequest(config, payloadJson) {
|
|
|
952
1149
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
953
1150
|
{ role: "user", content: payloadJson }
|
|
954
1151
|
],
|
|
955
|
-
response_format:
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1152
|
+
response_format: buildResponseFormat(mode)
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function advanceStringScan(char, escaped) {
|
|
1156
|
+
if (escaped) {
|
|
1157
|
+
return { inString: true, escaped: false };
|
|
1158
|
+
}
|
|
1159
|
+
if (char === "\\") {
|
|
1160
|
+
return { inString: true, escaped: true };
|
|
1161
|
+
}
|
|
1162
|
+
return { inString: char !== '"', escaped: false };
|
|
1163
|
+
}
|
|
1164
|
+
function scanBalancedObjectEnd(content, start) {
|
|
1165
|
+
let depth = 0;
|
|
1166
|
+
let inString = false;
|
|
1167
|
+
let escaped = false;
|
|
1168
|
+
for (let i = start; i < content.length; i += 1) {
|
|
1169
|
+
const char = content[i];
|
|
1170
|
+
if (inString) {
|
|
1171
|
+
({ inString, escaped } = advanceStringScan(char, escaped));
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (char === '"') {
|
|
1175
|
+
inString = true;
|
|
1176
|
+
} else if (char === "{") {
|
|
1177
|
+
depth += 1;
|
|
1178
|
+
} else if (char === "}") {
|
|
1179
|
+
depth -= 1;
|
|
1180
|
+
if (depth === 0) {
|
|
1181
|
+
return i;
|
|
961
1182
|
}
|
|
962
1183
|
}
|
|
963
|
-
}
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
964
1186
|
}
|
|
965
|
-
function
|
|
1187
|
+
function extractBalancedJson(content) {
|
|
1188
|
+
let searchFrom = 0;
|
|
1189
|
+
while (searchFrom < content.length) {
|
|
1190
|
+
const start = content.indexOf("{", searchFrom);
|
|
1191
|
+
if (start === -1) {
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
const end = scanBalancedObjectEnd(content, start);
|
|
1195
|
+
if (end === null) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
const candidate = content.slice(start, end + 1);
|
|
1199
|
+
try {
|
|
1200
|
+
JSON.parse(candidate);
|
|
1201
|
+
return candidate;
|
|
1202
|
+
} catch {
|
|
1203
|
+
searchFrom = end + 1;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
function parseContent2(content, tolerant) {
|
|
1209
|
+
const candidate = tolerant ? extractBalancedJson(content) ?? content : content;
|
|
966
1210
|
try {
|
|
967
|
-
return JSON.parse(
|
|
1211
|
+
return JSON.parse(candidate);
|
|
968
1212
|
} catch {
|
|
969
1213
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned unparseable content.");
|
|
970
1214
|
}
|
|
@@ -979,7 +1223,7 @@ function toUsage3(usage) {
|
|
|
979
1223
|
}
|
|
980
1224
|
return { inputTokens: prompt_tokens, outputTokens: completion_tokens };
|
|
981
1225
|
}
|
|
982
|
-
function extractOpenAiResult(completion) {
|
|
1226
|
+
function extractOpenAiResult(completion, tolerant = false) {
|
|
983
1227
|
const choice = completion.choices[0];
|
|
984
1228
|
if (choice === void 0) {
|
|
985
1229
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no message.");
|
|
@@ -992,7 +1236,7 @@ function extractOpenAiResult(completion) {
|
|
|
992
1236
|
if (message.content === void 0 || message.content === null) {
|
|
993
1237
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no translation content.");
|
|
994
1238
|
}
|
|
995
|
-
const raw = parseContent2(message.content);
|
|
1239
|
+
const raw = parseContent2(message.content, tolerant);
|
|
996
1240
|
const usage = toUsage3(completion.usage);
|
|
997
1241
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
998
1242
|
}
|
|
@@ -1010,15 +1254,84 @@ function createOpenAiProvider(config, deps = {}) {
|
|
|
1010
1254
|
}
|
|
1011
1255
|
function createMechanism3(client, config) {
|
|
1012
1256
|
return {
|
|
1013
|
-
translate: async ({ payloadJson }) => {
|
|
1257
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1014
1258
|
const body = buildOpenAiRequest(config, payloadJson);
|
|
1015
|
-
const completion = await callClient4(client, body);
|
|
1259
|
+
const completion = await callClient4(client, body, signal);
|
|
1016
1260
|
return extractOpenAiResult(completion);
|
|
1017
1261
|
}
|
|
1018
1262
|
};
|
|
1019
1263
|
}
|
|
1020
|
-
function callClient4(client, body) {
|
|
1021
|
-
return guardProviderCall(
|
|
1264
|
+
function callClient4(client, body, signal) {
|
|
1265
|
+
return guardProviderCall(
|
|
1266
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1267
|
+
signal
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
var HOSTED_PROVIDER_ENV_VARS = new Set(
|
|
1271
|
+
Object.values(PROVIDER_ENV).map((name) => name.toUpperCase())
|
|
1272
|
+
);
|
|
1273
|
+
function isNotHostedProviderEnvVar(value) {
|
|
1274
|
+
return !HOSTED_PROVIDER_ENV_VARS.has(value.toUpperCase());
|
|
1275
|
+
}
|
|
1276
|
+
function isHttpOrHttpsUrl(value) {
|
|
1277
|
+
try {
|
|
1278
|
+
const { protocol } = new URL(value);
|
|
1279
|
+
return protocol === "http:" || protocol === "https:";
|
|
1280
|
+
} catch {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
var openAiCompatibleConfigSchema = zod.z.object({
|
|
1285
|
+
baseUrl: zod.z.url({ message: "baseUrl must be a valid absolute URL." }).refine(isHttpOrHttpsUrl, { message: "baseUrl must use the http or https scheme." }),
|
|
1286
|
+
model: zod.z.string().min(1),
|
|
1287
|
+
maxOutputTokens: zod.z.number().int().positive(),
|
|
1288
|
+
apiKeyEnvVar: zod.z.string().min(1).refine(isNotHostedProviderEnvVar, {
|
|
1289
|
+
message: "apiKeyEnvVar must not name a hosted provider's environment variable."
|
|
1290
|
+
}).optional()
|
|
1291
|
+
});
|
|
1292
|
+
function createDefaultClient5(config) {
|
|
1293
|
+
const sdk = new OpenAI__default.default({
|
|
1294
|
+
apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
|
|
1295
|
+
baseURL: config.baseUrl,
|
|
1296
|
+
logLevel: "off"
|
|
1297
|
+
});
|
|
1298
|
+
return {
|
|
1299
|
+
chat: {
|
|
1300
|
+
completions: {
|
|
1301
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1302
|
+
body,
|
|
1303
|
+
options
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
var PROVIDER_ID5 = "openai-compatible";
|
|
1310
|
+
function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
1311
|
+
const validConfig = openAiCompatibleConfigSchema.parse(config);
|
|
1312
|
+
const client = deps.client ?? createDefaultClient5(validConfig);
|
|
1313
|
+
const mechanism = createMechanism4(client, validConfig);
|
|
1314
|
+
return {
|
|
1315
|
+
id: PROVIDER_ID5,
|
|
1316
|
+
kind: "llm",
|
|
1317
|
+
supportsGlossary: true,
|
|
1318
|
+
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function createMechanism4(client, config) {
|
|
1322
|
+
return {
|
|
1323
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1324
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema");
|
|
1325
|
+
const completion = await callClient5(client, body, signal);
|
|
1326
|
+
return extractOpenAiResult(completion, true);
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function callClient5(client, body, signal) {
|
|
1331
|
+
return guardProviderCall(
|
|
1332
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1333
|
+
signal
|
|
1334
|
+
);
|
|
1022
1335
|
}
|
|
1023
1336
|
var SCAFFOLD_MODELS = {
|
|
1024
1337
|
anthropic: "claude-sonnet-4-6",
|
|
@@ -1029,13 +1342,18 @@ var providerConfigSchema = zod.z.discriminatedUnion("id", [
|
|
|
1029
1342
|
zod.z.object({ id: zod.z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
|
|
1030
1343
|
zod.z.object({ id: zod.z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
1031
1344
|
zod.z.object({ id: zod.z.literal("gemini"), options: geminiConfigSchema.strict() }),
|
|
1032
|
-
zod.z.object({ id: zod.z.literal("deepl"), options: deepLConfigSchema.strict() })
|
|
1345
|
+
zod.z.object({ id: zod.z.literal("deepl"), options: deepLConfigSchema.strict() }),
|
|
1346
|
+
zod.z.object({
|
|
1347
|
+
id: zod.z.literal("openai-compatible"),
|
|
1348
|
+
options: openAiCompatibleConfigSchema.strict()
|
|
1349
|
+
})
|
|
1033
1350
|
]);
|
|
1034
1351
|
var providerFactories = {
|
|
1035
1352
|
anthropic: (options) => createAnthropicProvider(options),
|
|
1036
1353
|
openai: (options) => createOpenAiProvider(options),
|
|
1037
1354
|
gemini: (options) => createGeminiProvider(options),
|
|
1038
|
-
deepl: (options) => createDeepLProvider(options)
|
|
1355
|
+
deepl: (options) => createDeepLProvider(options),
|
|
1356
|
+
"openai-compatible": (options) => createOpenAiCompatibleProvider(options)
|
|
1039
1357
|
};
|
|
1040
1358
|
function buildProvider(config) {
|
|
1041
1359
|
const create = providerFactories[config.id];
|
|
@@ -1214,6 +1532,12 @@ async function readLockFile(path, fs) {
|
|
|
1214
1532
|
if (!result.success) {
|
|
1215
1533
|
throw new SdkError("LOCK_FILE_INVALID", `The lock-file at ${path} has an unexpected shape.`);
|
|
1216
1534
|
}
|
|
1535
|
+
if (result.data.version !== CURRENT_VERSION) {
|
|
1536
|
+
throw new SdkError(
|
|
1537
|
+
"LOCK_FILE_INVALID",
|
|
1538
|
+
`The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION}.`
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1217
1541
|
return result.data;
|
|
1218
1542
|
}
|
|
1219
1543
|
function baselineFor(lock, locale) {
|
|
@@ -1346,6 +1670,150 @@ function icuInvalidKeys(entries) {
|
|
|
1346
1670
|
}
|
|
1347
1671
|
return invalid;
|
|
1348
1672
|
}
|
|
1673
|
+
function tokenOf2(element) {
|
|
1674
|
+
switch (element.type) {
|
|
1675
|
+
case icuMessageformatParser.TYPE.argument:
|
|
1676
|
+
case icuMessageformatParser.TYPE.number:
|
|
1677
|
+
case icuMessageformatParser.TYPE.date:
|
|
1678
|
+
case icuMessageformatParser.TYPE.time:
|
|
1679
|
+
case icuMessageformatParser.TYPE.select:
|
|
1680
|
+
case icuMessageformatParser.TYPE.plural:
|
|
1681
|
+
return `{${element.value}}`;
|
|
1682
|
+
case icuMessageformatParser.TYPE.tag:
|
|
1683
|
+
return `<${element.value}>`;
|
|
1684
|
+
default:
|
|
1685
|
+
return void 0;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
function isBranching(element) {
|
|
1689
|
+
return element.type === icuMessageformatParser.TYPE.plural || element.type === icuMessageformatParser.TYPE.select;
|
|
1690
|
+
}
|
|
1691
|
+
function isTag(element) {
|
|
1692
|
+
return element.type === icuMessageformatParser.TYPE.tag;
|
|
1693
|
+
}
|
|
1694
|
+
function layerTokens(elements) {
|
|
1695
|
+
const tokens = [];
|
|
1696
|
+
for (const element of elements) {
|
|
1697
|
+
const token = tokenOf2(element);
|
|
1698
|
+
if (token !== void 0) {
|
|
1699
|
+
tokens.push(token);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return tokens;
|
|
1703
|
+
}
|
|
1704
|
+
function deepTokens(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
|
+
if (isTag(element)) {
|
|
1712
|
+
tokens.push(...deepTokens(element.children));
|
|
1713
|
+
} else if (isBranching(element)) {
|
|
1714
|
+
for (const option of Object.values(element.options)) {
|
|
1715
|
+
tokens.push(...deepTokens(option.value));
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return tokens;
|
|
1720
|
+
}
|
|
1721
|
+
function branchesByCategory(element) {
|
|
1722
|
+
return new Map(
|
|
1723
|
+
Object.entries(element.options).map(([category, option]) => [category, option.value])
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
function findUnconsumed(target, consumed, isMatch) {
|
|
1727
|
+
for (let index = 0; index < target.length; index += 1) {
|
|
1728
|
+
if (consumed.has(index)) {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
const candidate = target[index];
|
|
1732
|
+
if (candidate !== void 0 && isMatch(candidate)) {
|
|
1733
|
+
return { element: candidate, index };
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return void 0;
|
|
1737
|
+
}
|
|
1738
|
+
function findMatchingBranching(source, target, consumed) {
|
|
1739
|
+
return findUnconsumed(
|
|
1740
|
+
target,
|
|
1741
|
+
consumed,
|
|
1742
|
+
(candidate) => isBranching(candidate) && candidate.value === source.value
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
function findMatchingTag(source, target, consumed) {
|
|
1746
|
+
return findUnconsumed(
|
|
1747
|
+
target,
|
|
1748
|
+
consumed,
|
|
1749
|
+
(candidate) => isTag(candidate) && candidate.value === source.value
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
function combineResults(results) {
|
|
1753
|
+
const missing = results.flatMap((result) => result.missing).sort();
|
|
1754
|
+
const extra = results.flatMap((result) => result.extra).sort();
|
|
1755
|
+
return { matches: missing.length === 0 && extra.length === 0, missing, extra, reordered: false };
|
|
1756
|
+
}
|
|
1757
|
+
function compareAgainstSourceUnion(sourceBranches, targetBranch) {
|
|
1758
|
+
const union = /* @__PURE__ */ new Set();
|
|
1759
|
+
for (const branch of sourceBranches.values()) {
|
|
1760
|
+
for (const token of deepTokens(branch)) {
|
|
1761
|
+
union.add(token);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const extra = deepTokens(targetBranch).filter((token) => !union.has(token)).sort();
|
|
1765
|
+
return { matches: extra.length === 0, missing: [], extra, reordered: false };
|
|
1766
|
+
}
|
|
1767
|
+
function compareBranching(source, target) {
|
|
1768
|
+
const sourceBranches = branchesByCategory(source);
|
|
1769
|
+
const targetBranches = branchesByCategory(target);
|
|
1770
|
+
const categories = /* @__PURE__ */ new Set([...sourceBranches.keys(), ...targetBranches.keys()]);
|
|
1771
|
+
const results = [];
|
|
1772
|
+
for (const category of categories) {
|
|
1773
|
+
const sourceBranch = sourceBranches.get(category);
|
|
1774
|
+
const targetBranch = targetBranches.get(category);
|
|
1775
|
+
if (sourceBranch !== void 0 && targetBranch !== void 0) {
|
|
1776
|
+
results.push(compareElements(sourceBranch, targetBranch));
|
|
1777
|
+
} else if (targetBranch !== void 0) {
|
|
1778
|
+
results.push(compareAgainstSourceUnion(sourceBranches, targetBranch));
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return combineResults(results);
|
|
1782
|
+
}
|
|
1783
|
+
function nestedResults(source, target) {
|
|
1784
|
+
const results = [];
|
|
1785
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1786
|
+
for (const element of source) {
|
|
1787
|
+
if (isBranching(element)) {
|
|
1788
|
+
const found = findMatchingBranching(element, target, consumed);
|
|
1789
|
+
if (found !== void 0) {
|
|
1790
|
+
consumed.add(found.index);
|
|
1791
|
+
results.push(compareBranching(element, found.element));
|
|
1792
|
+
}
|
|
1793
|
+
} else if (isTag(element)) {
|
|
1794
|
+
const found = findMatchingTag(element, target, consumed);
|
|
1795
|
+
if (found !== void 0) {
|
|
1796
|
+
consumed.add(found.index);
|
|
1797
|
+
results.push(compareElements(element.children, found.element.children));
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return results;
|
|
1802
|
+
}
|
|
1803
|
+
function compareElements(source, target) {
|
|
1804
|
+
const layer = checkPlaceholders(layerTokens(source), layerTokens(target));
|
|
1805
|
+
const nested = nestedResults(source, target);
|
|
1806
|
+
return nested.length === 0 ? layer : combineResults([layer, ...nested]);
|
|
1807
|
+
}
|
|
1808
|
+
function compareIcuPlaceholders(sourceValue, targetValue) {
|
|
1809
|
+
try {
|
|
1810
|
+
const sourceAst = icuMessageformatParser.parse(sourceValue);
|
|
1811
|
+
const targetAst = icuMessageformatParser.parse(targetValue);
|
|
1812
|
+
return compareElements(sourceAst, targetAst);
|
|
1813
|
+
} catch {
|
|
1814
|
+
return checkPlaceholders(icuPlaceholders(sourceValue), icuPlaceholders(targetValue));
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1349
1817
|
var AdapterError = class extends Error {
|
|
1350
1818
|
code;
|
|
1351
1819
|
constructor(code, message) {
|
|
@@ -1647,9 +2115,9 @@ function unflattenEntries(entries) {
|
|
|
1647
2115
|
}
|
|
1648
2116
|
return root;
|
|
1649
2117
|
}
|
|
1650
|
-
function toEntries(content, namespace,
|
|
2118
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree) {
|
|
1651
2119
|
try {
|
|
1652
|
-
const tree =
|
|
2120
|
+
const tree = parse3(content);
|
|
1653
2121
|
validateTree?.(tree);
|
|
1654
2122
|
return flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
1655
2123
|
} catch (error) {
|
|
@@ -1661,7 +2129,7 @@ function createTreeFileAdapter(options) {
|
|
|
1661
2129
|
format,
|
|
1662
2130
|
extensions,
|
|
1663
2131
|
sniff,
|
|
1664
|
-
parse:
|
|
2132
|
+
parse: parse3,
|
|
1665
2133
|
serialize,
|
|
1666
2134
|
deriveEntry,
|
|
1667
2135
|
extractPlaceholders,
|
|
@@ -1669,6 +2137,7 @@ function createTreeFileAdapter(options) {
|
|
|
1669
2137
|
validateMessage,
|
|
1670
2138
|
validateTree,
|
|
1671
2139
|
buildWriteTree,
|
|
2140
|
+
comparePlaceholders,
|
|
1672
2141
|
keyMode = "literal-leaf"
|
|
1673
2142
|
} = options;
|
|
1674
2143
|
return {
|
|
@@ -1676,10 +2145,11 @@ function createTreeFileAdapter(options) {
|
|
|
1676
2145
|
canHandle: buildCanHandle(extensions, sniff),
|
|
1677
2146
|
extractPlaceholders,
|
|
1678
2147
|
validateMessage: validateMessage ?? (() => true),
|
|
2148
|
+
...comparePlaceholders !== void 0 ? { comparePlaceholders } : {},
|
|
1679
2149
|
async read(filePath, locale) {
|
|
1680
2150
|
const content = await readFileContent(filePath);
|
|
1681
2151
|
const namespace = namespaceOf(filePath);
|
|
1682
|
-
const entries = toEntries(content, namespace,
|
|
2152
|
+
const entries = toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree);
|
|
1683
2153
|
const resource = { locale, namespace, format, entries };
|
|
1684
2154
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1685
2155
|
return { resource, invalidIcuKeys };
|
|
@@ -1775,6 +2245,7 @@ function createArbAdapter() {
|
|
|
1775
2245
|
deriveEntry: icuDeriveEntry,
|
|
1776
2246
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1777
2247
|
validateMessage: icuIsValid,
|
|
2248
|
+
comparePlaceholders: compareIcuPlaceholders,
|
|
1778
2249
|
buildWriteTree: buildArbWriteTree
|
|
1779
2250
|
});
|
|
1780
2251
|
}
|
|
@@ -1838,7 +2309,8 @@ function createNextIntlJsonAdapter() {
|
|
|
1838
2309
|
extractPlaceholders: icuPlaceholders,
|
|
1839
2310
|
deriveEntry: icuDeriveEntry,
|
|
1840
2311
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1841
|
-
validateMessage: icuIsValid
|
|
2312
|
+
validateMessage: icuIsValid,
|
|
2313
|
+
comparePlaceholders: compareIcuPlaceholders
|
|
1842
2314
|
});
|
|
1843
2315
|
}
|
|
1844
2316
|
function assertNoDottedNestedKey(tree) {
|
|
@@ -2040,6 +2512,7 @@ function extractXliffPlaceholders(value) {
|
|
|
2040
2512
|
return result;
|
|
2041
2513
|
}
|
|
2042
2514
|
var ELEMENT_NODE = 1;
|
|
2515
|
+
var TEXT_NODE = 3;
|
|
2043
2516
|
function isElement(node) {
|
|
2044
2517
|
return node.nodeType === ELEMENT_NODE;
|
|
2045
2518
|
}
|
|
@@ -2130,6 +2603,12 @@ function parseXliffEntries(content, namespace) {
|
|
|
2130
2603
|
const serializer = new xmldom.XMLSerializer();
|
|
2131
2604
|
const out = /* @__PURE__ */ new Map();
|
|
2132
2605
|
for (const unit of walkUnits(root)) {
|
|
2606
|
+
if (out.has(unit.key)) {
|
|
2607
|
+
throw new AdapterError(
|
|
2608
|
+
"INVALID_STRUCTURE",
|
|
2609
|
+
"The XLIFF file has two trans-units with the same id."
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2133
2612
|
const value = unitValue(serializer, unit);
|
|
2134
2613
|
out.set(unit.key, {
|
|
2135
2614
|
key: unit.key,
|
|
@@ -2156,10 +2635,67 @@ async function readDestination(filePath) {
|
|
|
2156
2635
|
}
|
|
2157
2636
|
return outcome.content;
|
|
2158
2637
|
}
|
|
2638
|
+
var XLIFF_INLINE_ELEMENTS = /* @__PURE__ */ new Set(["x", "g", "bx", "ex", "ph", "it", "mrk"]);
|
|
2639
|
+
var XLIFF_NAMESPACES = /* @__PURE__ */ new Set([
|
|
2640
|
+
"urn:oasis:names:tc:xliff:document:1.2",
|
|
2641
|
+
"urn:oasis:names:tc:xliff:document:2.0"
|
|
2642
|
+
]);
|
|
2643
|
+
var INLINE_ELEMENT_ATTRIBUTES = {
|
|
2644
|
+
x: ["id", "ctype"],
|
|
2645
|
+
ph: ["id", "ctype"],
|
|
2646
|
+
g: ["id", "ctype"],
|
|
2647
|
+
bx: ["id", "rid", "ctype"],
|
|
2648
|
+
ex: ["id", "rid"],
|
|
2649
|
+
it: ["id", "pos", "ctype"],
|
|
2650
|
+
mrk: ["id", "mtype"]
|
|
2651
|
+
};
|
|
2652
|
+
function isAllowedInlineElement(element) {
|
|
2653
|
+
const namespace = element.namespaceURI;
|
|
2654
|
+
const hasAllowedNamespace = namespace === null || XLIFF_NAMESPACES.has(namespace);
|
|
2655
|
+
return hasAllowedNamespace && XLIFF_INLINE_ELEMENTS.has(element.localName ?? "");
|
|
2656
|
+
}
|
|
2657
|
+
function isAllowedFragmentNode(node) {
|
|
2658
|
+
if (node.nodeType === TEXT_NODE) {
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
return isElement(node) && isAllowedInlineElement(node);
|
|
2662
|
+
}
|
|
2663
|
+
function allDescendantNodes(node) {
|
|
2664
|
+
const children = Array.from(node.childNodes);
|
|
2665
|
+
return children.flatMap((child) => [child, ...allDescendantNodes(child)]);
|
|
2666
|
+
}
|
|
2667
|
+
function hasDisallowedNode(root) {
|
|
2668
|
+
return allDescendantNodes(root).some((node) => !isAllowedFragmentNode(node));
|
|
2669
|
+
}
|
|
2670
|
+
function attributeNames(element) {
|
|
2671
|
+
const names = [];
|
|
2672
|
+
for (let i = 0; i < element.attributes.length; i += 1) {
|
|
2673
|
+
const attr = element.attributes.item(i);
|
|
2674
|
+
if (attr !== null) {
|
|
2675
|
+
names.push(attr.name);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
return names;
|
|
2679
|
+
}
|
|
2680
|
+
function sanitizeInlineAttributes(root) {
|
|
2681
|
+
for (const el of collectByTag(root, "*")) {
|
|
2682
|
+
const allowed = INLINE_ELEMENT_ATTRIBUTES[el.localName ?? ""] ?? [];
|
|
2683
|
+
for (const name of attributeNames(el)) {
|
|
2684
|
+
if (!allowed.includes(name)) {
|
|
2685
|
+
el.removeAttribute(name);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2159
2690
|
function fragmentNodes(parser, value) {
|
|
2691
|
+
assertNoDoctype(value);
|
|
2160
2692
|
try {
|
|
2161
2693
|
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
2162
|
-
|
|
2694
|
+
if (root === null || hasDisallowedNode(root)) {
|
|
2695
|
+
return null;
|
|
2696
|
+
}
|
|
2697
|
+
sanitizeInlineAttributes(root);
|
|
2698
|
+
return Array.from(root.childNodes);
|
|
2163
2699
|
} catch {
|
|
2164
2700
|
return null;
|
|
2165
2701
|
}
|
|
@@ -2613,7 +3149,8 @@ function buildRequest2(context, entries) {
|
|
|
2613
3149
|
entries,
|
|
2614
3150
|
extractPlaceholders: context.adapter.extractPlaceholders,
|
|
2615
3151
|
...context.glossary !== void 0 ? { glossary: context.glossary } : {},
|
|
2616
|
-
...context.tone !== void 0 ? { tone: context.tone } : {}
|
|
3152
|
+
...context.tone !== void 0 ? { tone: context.tone } : {},
|
|
3153
|
+
...context.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: context.adapter.comparePlaceholders } : {}
|
|
2617
3154
|
};
|
|
2618
3155
|
}
|
|
2619
3156
|
async function generatePluralForms(context) {
|
|
@@ -2683,7 +3220,8 @@ function buildRequest3(params, entries) {
|
|
|
2683
3220
|
entries,
|
|
2684
3221
|
extractPlaceholders: params.adapter.extractPlaceholders,
|
|
2685
3222
|
...params.glossary !== void 0 ? { glossary: params.glossary } : {},
|
|
2686
|
-
...params.tone !== void 0 ? { tone: params.tone } : {}
|
|
3223
|
+
...params.tone !== void 0 ? { tone: params.tone } : {},
|
|
3224
|
+
...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
|
|
2687
3225
|
};
|
|
2688
3226
|
}
|
|
2689
3227
|
async function runLocale(params) {
|
|
@@ -3146,7 +3684,7 @@ async function streamEntryBounded(file, remaining) {
|
|
|
3146
3684
|
"The workbook decompresses to more than the permitted maximum bytes."
|
|
3147
3685
|
);
|
|
3148
3686
|
}
|
|
3149
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
3687
|
+
return { raw, content: Buffer.concat(chunks).toString("utf8") };
|
|
3150
3688
|
}
|
|
3151
3689
|
async function guardWorkbookBytes(bytes, limits) {
|
|
3152
3690
|
let zip;
|
|
@@ -3178,14 +3716,8 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
3178
3716
|
let actualTotal = 0;
|
|
3179
3717
|
for (const file of files) {
|
|
3180
3718
|
const remaining = limits.maxDecompressedBytes - actualTotal;
|
|
3181
|
-
const content = await streamEntryBounded(file, remaining);
|
|
3182
|
-
actualTotal +=
|
|
3183
|
-
if (actualTotal > limits.maxDecompressedBytes) {
|
|
3184
|
-
throw new ExchangeError(
|
|
3185
|
-
"WORKBOOK_INVALID",
|
|
3186
|
-
`The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
|
|
3187
|
-
);
|
|
3188
|
-
}
|
|
3719
|
+
const { raw, content } = await streamEntryBounded(file, remaining);
|
|
3720
|
+
actualTotal += raw;
|
|
3189
3721
|
assertNoDoctype2(file.name, content);
|
|
3190
3722
|
}
|
|
3191
3723
|
}
|
|
@@ -3374,10 +3906,7 @@ function judge(row, sourceEntry, adapter) {
|
|
|
3374
3906
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
3375
3907
|
return "drift";
|
|
3376
3908
|
}
|
|
3377
|
-
const integrity = checkPlaceholders(
|
|
3378
|
-
sourceEntry.placeholders,
|
|
3379
|
-
adapter.extractPlaceholders(row.translation)
|
|
3380
|
-
);
|
|
3909
|
+
const integrity = adapter.comparePlaceholders !== void 0 ? adapter.comparePlaceholders(sourceEntry.value, row.translation) : checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(row.translation));
|
|
3381
3910
|
if (!integrity.matches) {
|
|
3382
3911
|
return "placeholder";
|
|
3383
3912
|
}
|