@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.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
|
}
|
|
@@ -385,7 +481,7 @@ function toIntegrityInputs(entries, values) {
|
|
|
385
481
|
"The provider response is missing one or more keys."
|
|
386
482
|
);
|
|
387
483
|
}
|
|
388
|
-
return { key: entry.key,
|
|
484
|
+
return { key: entry.key, sourceValue: entry.value, translatedValue };
|
|
389
485
|
});
|
|
390
486
|
}
|
|
391
487
|
function toItem(entry) {
|
|
@@ -452,11 +548,16 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
452
548
|
const data = validateRequest(request);
|
|
453
549
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
454
550
|
const requestedKeys = data.entries.map((entry) => entry.key);
|
|
455
|
-
const completion = await mechanism.translate({
|
|
551
|
+
const completion = await mechanism.translate({
|
|
552
|
+
payloadJson,
|
|
553
|
+
requestedKeys,
|
|
554
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
555
|
+
});
|
|
456
556
|
const values = reconcileResult(completion.raw, requestedKeys);
|
|
457
557
|
const integrity = checkBatchIntegrity(
|
|
458
558
|
toIntegrityInputs(data.entries, values),
|
|
459
|
-
request.extractPlaceholders
|
|
559
|
+
request.extractPlaceholders,
|
|
560
|
+
request.comparePlaceholders
|
|
460
561
|
);
|
|
461
562
|
return completion.usage === void 0 ? { values, integrity } : { values, integrity, usage: completion.usage };
|
|
462
563
|
}
|
|
@@ -491,12 +592,29 @@ function requireGeminiKey() {
|
|
|
491
592
|
function requireDeepLKey() {
|
|
492
593
|
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
493
594
|
}
|
|
595
|
+
var OPENAI_COMPATIBLE_ENV_VAR = "OPENAI_COMPATIBLE_API_KEY";
|
|
596
|
+
var OPENAI_COMPATIBLE_KEY_PLACEHOLDER = "local";
|
|
597
|
+
function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
598
|
+
const varName = customEnvVar ?? OPENAI_COMPATIBLE_ENV_VAR;
|
|
599
|
+
const value = process.env[varName];
|
|
600
|
+
if (value !== void 0 && value.length > 0) {
|
|
601
|
+
return value;
|
|
602
|
+
}
|
|
603
|
+
if (customEnvVar !== void 0) {
|
|
604
|
+
throw new ProviderError(
|
|
605
|
+
"MISSING_API_KEY",
|
|
606
|
+
`The ${customEnvVar} environment variable is not set.`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return OPENAI_COMPATIBLE_KEY_PLACEHOLDER;
|
|
610
|
+
}
|
|
494
611
|
function createDefaultClient() {
|
|
495
612
|
const sdk = new Anthropic({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
496
613
|
return {
|
|
497
614
|
messages: {
|
|
498
|
-
create: async (body) => await sdk.messages.create(
|
|
499
|
-
body
|
|
615
|
+
create: async (body, options) => await sdk.messages.create(
|
|
616
|
+
body,
|
|
617
|
+
options
|
|
500
618
|
)
|
|
501
619
|
}
|
|
502
620
|
};
|
|
@@ -564,9 +682,9 @@ function createAnthropicProvider(config, deps = {}) {
|
|
|
564
682
|
}
|
|
565
683
|
function createMechanism(client, config) {
|
|
566
684
|
return {
|
|
567
|
-
translate: async ({ payloadJson }) => {
|
|
685
|
+
translate: async ({ payloadJson, signal }) => {
|
|
568
686
|
const body = buildRequest(config, payloadJson);
|
|
569
|
-
const message = await callClient(client, body);
|
|
687
|
+
const message = await callClient(client, body, signal);
|
|
570
688
|
assertNotTruncated(message.stop_reason === "max_tokens");
|
|
571
689
|
const raw = requireToolInput(message.content);
|
|
572
690
|
const usage = toUsage(message.usage);
|
|
@@ -574,8 +692,11 @@ function createMechanism(client, config) {
|
|
|
574
692
|
}
|
|
575
693
|
};
|
|
576
694
|
}
|
|
577
|
-
function callClient(client, body) {
|
|
578
|
-
return guardProviderCall(
|
|
695
|
+
function callClient(client, body, signal) {
|
|
696
|
+
return guardProviderCall(
|
|
697
|
+
() => client.messages.create(body, signal !== void 0 ? { signal } : void 0),
|
|
698
|
+
signal
|
|
699
|
+
);
|
|
579
700
|
}
|
|
580
701
|
function toUsage(usage) {
|
|
581
702
|
if (usage === void 0) {
|
|
@@ -670,7 +791,7 @@ function zipResults(entries, results) {
|
|
|
670
791
|
values.set(entry.key, translatedValue);
|
|
671
792
|
integrityInputs.push({
|
|
672
793
|
key: entry.key,
|
|
673
|
-
|
|
794
|
+
sourceValue: entry.value,
|
|
674
795
|
translatedValue
|
|
675
796
|
});
|
|
676
797
|
}
|
|
@@ -711,36 +832,97 @@ async function translate(bundle, config, request) {
|
|
|
711
832
|
data,
|
|
712
833
|
protectable,
|
|
713
834
|
options,
|
|
714
|
-
request.extractPlaceholders
|
|
835
|
+
request.extractPlaceholders,
|
|
836
|
+
request.comparePlaceholders,
|
|
837
|
+
request.signal
|
|
715
838
|
);
|
|
716
839
|
if (unprotectable.length > 0) {
|
|
717
840
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
718
841
|
}
|
|
719
842
|
return { values, integrity, notices };
|
|
720
843
|
}
|
|
721
|
-
async function translateProtectable(client, data, protectable, options, extract) {
|
|
844
|
+
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
722
845
|
if (protectable.length === 0) {
|
|
723
846
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
724
847
|
}
|
|
725
848
|
const texts = protectable.map((entry) => entry.value);
|
|
726
|
-
const results = await callClient2(
|
|
849
|
+
const results = await callClient2(
|
|
850
|
+
client,
|
|
851
|
+
texts,
|
|
852
|
+
data.sourceLocale,
|
|
853
|
+
data.targetLocale,
|
|
854
|
+
options,
|
|
855
|
+
signal
|
|
856
|
+
);
|
|
727
857
|
const { values, integrityInputs } = zipResults(protectable, results);
|
|
728
|
-
const integrity = checkBatchIntegrity(integrityInputs, extract);
|
|
858
|
+
const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
|
|
729
859
|
return { values, integrity };
|
|
730
860
|
}
|
|
731
|
-
function callClient2(client, texts, sourceLang, targetLang, options) {
|
|
732
|
-
return guardProviderCall(
|
|
861
|
+
function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
862
|
+
return guardProviderCall(
|
|
863
|
+
() => client.translateText(texts, sourceLang, targetLang, options),
|
|
864
|
+
signal
|
|
865
|
+
);
|
|
733
866
|
}
|
|
734
867
|
var geminiConfigSchema = z.object({
|
|
735
868
|
model: z.string().min(1),
|
|
736
869
|
maxOutputTokens: z.number().int().positive()
|
|
737
870
|
});
|
|
871
|
+
var DEFAULT_GEMINI_RETRY = { attempts: 3, baseDelayMs: 250 };
|
|
872
|
+
function isRetryableStatus(error) {
|
|
873
|
+
const status = getErrorStatus(error);
|
|
874
|
+
return status === 429 || status !== void 0 && status >= 500 && status < 600;
|
|
875
|
+
}
|
|
876
|
+
function isAborted(signal) {
|
|
877
|
+
return signal?.aborted === true;
|
|
878
|
+
}
|
|
879
|
+
function throwAbort(signal) {
|
|
880
|
+
signal?.throwIfAborted();
|
|
881
|
+
throw new DOMException("This operation was aborted.", "AbortError");
|
|
882
|
+
}
|
|
883
|
+
function delay(ms, signal) {
|
|
884
|
+
return new Promise((resolve7) => {
|
|
885
|
+
const onAbort = () => {
|
|
886
|
+
clearTimeout(timer);
|
|
887
|
+
resolve7();
|
|
888
|
+
};
|
|
889
|
+
const timer = setTimeout(() => {
|
|
890
|
+
signal?.removeEventListener("abort", onAbort);
|
|
891
|
+
resolve7();
|
|
892
|
+
}, ms);
|
|
893
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
async function withGeminiRetry(call, signal, config = DEFAULT_GEMINI_RETRY) {
|
|
897
|
+
let attempt = 1;
|
|
898
|
+
while (true) {
|
|
899
|
+
try {
|
|
900
|
+
return await call();
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (isAborted(signal)) {
|
|
903
|
+
throwAbort(signal);
|
|
904
|
+
}
|
|
905
|
+
const exhausted = attempt >= config.attempts;
|
|
906
|
+
if (exhausted || !isRetryableStatus(error)) {
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
await delay(config.baseDelayMs * 2 ** (attempt - 1), signal);
|
|
910
|
+
if (isAborted(signal)) {
|
|
911
|
+
throwAbort(signal);
|
|
912
|
+
}
|
|
913
|
+
attempt += 1;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
738
917
|
function createDefaultClient3() {
|
|
739
918
|
const ai = new GoogleGenAI({ apiKey: requireGeminiKey() });
|
|
740
919
|
return {
|
|
741
920
|
models: {
|
|
742
|
-
generateContent:
|
|
743
|
-
|
|
921
|
+
generateContent: (request) => withGeminiRetry(
|
|
922
|
+
async () => await ai.models.generateContent(
|
|
923
|
+
request
|
|
924
|
+
),
|
|
925
|
+
request.config.abortSignal
|
|
744
926
|
)
|
|
745
927
|
}
|
|
746
928
|
};
|
|
@@ -804,7 +986,7 @@ var GEMINI_SYSTEM_RULES = [
|
|
|
804
986
|
"When a tone is provided, honor it.",
|
|
805
987
|
"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
988
|
].join("\n");
|
|
807
|
-
function buildGeminiRequest(config, payloadJson) {
|
|
989
|
+
function buildGeminiRequest(config, payloadJson, signal) {
|
|
808
990
|
return {
|
|
809
991
|
model: config.model,
|
|
810
992
|
contents: [{ role: "user", parts: [{ text: payloadJson }] }],
|
|
@@ -812,7 +994,8 @@ function buildGeminiRequest(config, payloadJson) {
|
|
|
812
994
|
systemInstruction: GEMINI_SYSTEM_RULES,
|
|
813
995
|
responseMimeType: "application/json",
|
|
814
996
|
responseSchema: toGeminiSchema(deriveJsonSchema(translationsResultSchema)),
|
|
815
|
-
maxOutputTokens: config.maxOutputTokens
|
|
997
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
998
|
+
...signal !== void 0 ? { abortSignal: signal } : {}
|
|
816
999
|
}
|
|
817
1000
|
};
|
|
818
1001
|
}
|
|
@@ -876,15 +1059,15 @@ function createGeminiProvider(config, deps = {}) {
|
|
|
876
1059
|
}
|
|
877
1060
|
function createMechanism2(client, config) {
|
|
878
1061
|
return {
|
|
879
|
-
translate: async ({ payloadJson }) => {
|
|
880
|
-
const request = buildGeminiRequest(config, payloadJson);
|
|
881
|
-
const response = await callClient3(client, request);
|
|
1062
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1063
|
+
const request = buildGeminiRequest(config, payloadJson, signal);
|
|
1064
|
+
const response = await callClient3(client, request, signal);
|
|
882
1065
|
return extractGeminiResult(response);
|
|
883
1066
|
}
|
|
884
1067
|
};
|
|
885
1068
|
}
|
|
886
|
-
function callClient3(client, request) {
|
|
887
|
-
return guardProviderCall(() => client.models.generateContent(request));
|
|
1069
|
+
function callClient3(client, request, signal) {
|
|
1070
|
+
return guardProviderCall(() => client.models.generateContent(request), signal);
|
|
888
1071
|
}
|
|
889
1072
|
var openAiConfigSchema = z.object({
|
|
890
1073
|
model: z.string().min(1),
|
|
@@ -895,8 +1078,9 @@ function createDefaultClient4() {
|
|
|
895
1078
|
return {
|
|
896
1079
|
chat: {
|
|
897
1080
|
completions: {
|
|
898
|
-
create: async (body) => await sdk.chat.completions.create(
|
|
899
|
-
body
|
|
1081
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1082
|
+
body,
|
|
1083
|
+
options
|
|
900
1084
|
)
|
|
901
1085
|
}
|
|
902
1086
|
}
|
|
@@ -914,7 +1098,20 @@ var OPENAI_SYSTEM_RULES = [
|
|
|
914
1098
|
"When a tone is provided, honor it.",
|
|
915
1099
|
"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
1100
|
].join("\n");
|
|
917
|
-
function
|
|
1101
|
+
function buildResponseFormat(mode) {
|
|
1102
|
+
if (mode === "json-object") {
|
|
1103
|
+
return { type: "json_object" };
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
type: "json_schema",
|
|
1107
|
+
json_schema: {
|
|
1108
|
+
name: RESULT_SCHEMA_NAME,
|
|
1109
|
+
strict: true,
|
|
1110
|
+
schema: deriveJsonSchema(translationsResultSchema)
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema") {
|
|
918
1115
|
return {
|
|
919
1116
|
model: config.model,
|
|
920
1117
|
max_completion_tokens: config.maxOutputTokens,
|
|
@@ -922,19 +1119,66 @@ function buildOpenAiRequest(config, payloadJson) {
|
|
|
922
1119
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
923
1120
|
{ role: "user", content: payloadJson }
|
|
924
1121
|
],
|
|
925
|
-
response_format:
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1122
|
+
response_format: buildResponseFormat(mode)
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function advanceStringScan(char, escaped) {
|
|
1126
|
+
if (escaped) {
|
|
1127
|
+
return { inString: true, escaped: false };
|
|
1128
|
+
}
|
|
1129
|
+
if (char === "\\") {
|
|
1130
|
+
return { inString: true, escaped: true };
|
|
1131
|
+
}
|
|
1132
|
+
return { inString: char !== '"', escaped: false };
|
|
1133
|
+
}
|
|
1134
|
+
function scanBalancedObjectEnd(content, start) {
|
|
1135
|
+
let depth = 0;
|
|
1136
|
+
let inString = false;
|
|
1137
|
+
let escaped = false;
|
|
1138
|
+
for (let i = start; i < content.length; i += 1) {
|
|
1139
|
+
const char = content[i];
|
|
1140
|
+
if (inString) {
|
|
1141
|
+
({ inString, escaped } = advanceStringScan(char, escaped));
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
if (char === '"') {
|
|
1145
|
+
inString = true;
|
|
1146
|
+
} else if (char === "{") {
|
|
1147
|
+
depth += 1;
|
|
1148
|
+
} else if (char === "}") {
|
|
1149
|
+
depth -= 1;
|
|
1150
|
+
if (depth === 0) {
|
|
1151
|
+
return i;
|
|
931
1152
|
}
|
|
932
1153
|
}
|
|
933
|
-
}
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
934
1156
|
}
|
|
935
|
-
function
|
|
1157
|
+
function extractBalancedJson(content) {
|
|
1158
|
+
let searchFrom = 0;
|
|
1159
|
+
while (searchFrom < content.length) {
|
|
1160
|
+
const start = content.indexOf("{", searchFrom);
|
|
1161
|
+
if (start === -1) {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
const end = scanBalancedObjectEnd(content, start);
|
|
1165
|
+
if (end === null) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const candidate = content.slice(start, end + 1);
|
|
1169
|
+
try {
|
|
1170
|
+
JSON.parse(candidate);
|
|
1171
|
+
return candidate;
|
|
1172
|
+
} catch {
|
|
1173
|
+
searchFrom = end + 1;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
function parseContent2(content, tolerant) {
|
|
1179
|
+
const candidate = tolerant ? extractBalancedJson(content) ?? content : content;
|
|
936
1180
|
try {
|
|
937
|
-
return JSON.parse(
|
|
1181
|
+
return JSON.parse(candidate);
|
|
938
1182
|
} catch {
|
|
939
1183
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned unparseable content.");
|
|
940
1184
|
}
|
|
@@ -949,7 +1193,7 @@ function toUsage3(usage) {
|
|
|
949
1193
|
}
|
|
950
1194
|
return { inputTokens: prompt_tokens, outputTokens: completion_tokens };
|
|
951
1195
|
}
|
|
952
|
-
function extractOpenAiResult(completion) {
|
|
1196
|
+
function extractOpenAiResult(completion, tolerant = false) {
|
|
953
1197
|
const choice = completion.choices[0];
|
|
954
1198
|
if (choice === void 0) {
|
|
955
1199
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no message.");
|
|
@@ -962,7 +1206,7 @@ function extractOpenAiResult(completion) {
|
|
|
962
1206
|
if (message.content === void 0 || message.content === null) {
|
|
963
1207
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no translation content.");
|
|
964
1208
|
}
|
|
965
|
-
const raw = parseContent2(message.content);
|
|
1209
|
+
const raw = parseContent2(message.content, tolerant);
|
|
966
1210
|
const usage = toUsage3(completion.usage);
|
|
967
1211
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
968
1212
|
}
|
|
@@ -980,15 +1224,84 @@ function createOpenAiProvider(config, deps = {}) {
|
|
|
980
1224
|
}
|
|
981
1225
|
function createMechanism3(client, config) {
|
|
982
1226
|
return {
|
|
983
|
-
translate: async ({ payloadJson }) => {
|
|
1227
|
+
translate: async ({ payloadJson, signal }) => {
|
|
984
1228
|
const body = buildOpenAiRequest(config, payloadJson);
|
|
985
|
-
const completion = await callClient4(client, body);
|
|
1229
|
+
const completion = await callClient4(client, body, signal);
|
|
986
1230
|
return extractOpenAiResult(completion);
|
|
987
1231
|
}
|
|
988
1232
|
};
|
|
989
1233
|
}
|
|
990
|
-
function callClient4(client, body) {
|
|
991
|
-
return guardProviderCall(
|
|
1234
|
+
function callClient4(client, body, signal) {
|
|
1235
|
+
return guardProviderCall(
|
|
1236
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1237
|
+
signal
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
var HOSTED_PROVIDER_ENV_VARS = new Set(
|
|
1241
|
+
Object.values(PROVIDER_ENV).map((name) => name.toUpperCase())
|
|
1242
|
+
);
|
|
1243
|
+
function isNotHostedProviderEnvVar(value) {
|
|
1244
|
+
return !HOSTED_PROVIDER_ENV_VARS.has(value.toUpperCase());
|
|
1245
|
+
}
|
|
1246
|
+
function isHttpOrHttpsUrl(value) {
|
|
1247
|
+
try {
|
|
1248
|
+
const { protocol } = new URL(value);
|
|
1249
|
+
return protocol === "http:" || protocol === "https:";
|
|
1250
|
+
} catch {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
var openAiCompatibleConfigSchema = z.object({
|
|
1255
|
+
baseUrl: z.url({ message: "baseUrl must be a valid absolute URL." }).refine(isHttpOrHttpsUrl, { message: "baseUrl must use the http or https scheme." }),
|
|
1256
|
+
model: z.string().min(1),
|
|
1257
|
+
maxOutputTokens: z.number().int().positive(),
|
|
1258
|
+
apiKeyEnvVar: z.string().min(1).refine(isNotHostedProviderEnvVar, {
|
|
1259
|
+
message: "apiKeyEnvVar must not name a hosted provider's environment variable."
|
|
1260
|
+
}).optional()
|
|
1261
|
+
});
|
|
1262
|
+
function createDefaultClient5(config) {
|
|
1263
|
+
const sdk = new OpenAI({
|
|
1264
|
+
apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
|
|
1265
|
+
baseURL: config.baseUrl,
|
|
1266
|
+
logLevel: "off"
|
|
1267
|
+
});
|
|
1268
|
+
return {
|
|
1269
|
+
chat: {
|
|
1270
|
+
completions: {
|
|
1271
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1272
|
+
body,
|
|
1273
|
+
options
|
|
1274
|
+
)
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
var PROVIDER_ID5 = "openai-compatible";
|
|
1280
|
+
function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
1281
|
+
const validConfig = openAiCompatibleConfigSchema.parse(config);
|
|
1282
|
+
const client = deps.client ?? createDefaultClient5(validConfig);
|
|
1283
|
+
const mechanism = createMechanism4(client, validConfig);
|
|
1284
|
+
return {
|
|
1285
|
+
id: PROVIDER_ID5,
|
|
1286
|
+
kind: "llm",
|
|
1287
|
+
supportsGlossary: true,
|
|
1288
|
+
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function createMechanism4(client, config) {
|
|
1292
|
+
return {
|
|
1293
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1294
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema");
|
|
1295
|
+
const completion = await callClient5(client, body, signal);
|
|
1296
|
+
return extractOpenAiResult(completion, true);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function callClient5(client, body, signal) {
|
|
1301
|
+
return guardProviderCall(
|
|
1302
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1303
|
+
signal
|
|
1304
|
+
);
|
|
992
1305
|
}
|
|
993
1306
|
var SCAFFOLD_MODELS = {
|
|
994
1307
|
anthropic: "claude-sonnet-4-6",
|
|
@@ -999,13 +1312,18 @@ var providerConfigSchema = z.discriminatedUnion("id", [
|
|
|
999
1312
|
z.object({ id: z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
|
|
1000
1313
|
z.object({ id: z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
1001
1314
|
z.object({ id: z.literal("gemini"), options: geminiConfigSchema.strict() }),
|
|
1002
|
-
z.object({ id: z.literal("deepl"), options: deepLConfigSchema.strict() })
|
|
1315
|
+
z.object({ id: z.literal("deepl"), options: deepLConfigSchema.strict() }),
|
|
1316
|
+
z.object({
|
|
1317
|
+
id: z.literal("openai-compatible"),
|
|
1318
|
+
options: openAiCompatibleConfigSchema.strict()
|
|
1319
|
+
})
|
|
1003
1320
|
]);
|
|
1004
1321
|
var providerFactories = {
|
|
1005
1322
|
anthropic: (options) => createAnthropicProvider(options),
|
|
1006
1323
|
openai: (options) => createOpenAiProvider(options),
|
|
1007
1324
|
gemini: (options) => createGeminiProvider(options),
|
|
1008
|
-
deepl: (options) => createDeepLProvider(options)
|
|
1325
|
+
deepl: (options) => createDeepLProvider(options),
|
|
1326
|
+
"openai-compatible": (options) => createOpenAiCompatibleProvider(options)
|
|
1009
1327
|
};
|
|
1010
1328
|
function buildProvider(config) {
|
|
1011
1329
|
const create = providerFactories[config.id];
|
|
@@ -1184,6 +1502,12 @@ async function readLockFile(path, fs) {
|
|
|
1184
1502
|
if (!result.success) {
|
|
1185
1503
|
throw new SdkError("LOCK_FILE_INVALID", `The lock-file at ${path} has an unexpected shape.`);
|
|
1186
1504
|
}
|
|
1505
|
+
if (result.data.version !== CURRENT_VERSION) {
|
|
1506
|
+
throw new SdkError(
|
|
1507
|
+
"LOCK_FILE_INVALID",
|
|
1508
|
+
`The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION}.`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1187
1511
|
return result.data;
|
|
1188
1512
|
}
|
|
1189
1513
|
function baselineFor(lock, locale) {
|
|
@@ -1289,7 +1613,7 @@ function analyzeIcuValue(value) {
|
|
|
1289
1613
|
return VALID_EMPTY;
|
|
1290
1614
|
}
|
|
1291
1615
|
try {
|
|
1292
|
-
const ast = parse
|
|
1616
|
+
const ast = parse(value);
|
|
1293
1617
|
const state = { isPlural: false };
|
|
1294
1618
|
const multiset = collect(ast, state);
|
|
1295
1619
|
return { placeholders: flatten(multiset), isPlural: state.isPlural, valid: true };
|
|
@@ -1316,6 +1640,150 @@ function icuInvalidKeys(entries) {
|
|
|
1316
1640
|
}
|
|
1317
1641
|
return invalid;
|
|
1318
1642
|
}
|
|
1643
|
+
function tokenOf2(element) {
|
|
1644
|
+
switch (element.type) {
|
|
1645
|
+
case TYPE.argument:
|
|
1646
|
+
case TYPE.number:
|
|
1647
|
+
case TYPE.date:
|
|
1648
|
+
case TYPE.time:
|
|
1649
|
+
case TYPE.select:
|
|
1650
|
+
case TYPE.plural:
|
|
1651
|
+
return `{${element.value}}`;
|
|
1652
|
+
case TYPE.tag:
|
|
1653
|
+
return `<${element.value}>`;
|
|
1654
|
+
default:
|
|
1655
|
+
return void 0;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function isBranching(element) {
|
|
1659
|
+
return element.type === TYPE.plural || element.type === TYPE.select;
|
|
1660
|
+
}
|
|
1661
|
+
function isTag(element) {
|
|
1662
|
+
return element.type === TYPE.tag;
|
|
1663
|
+
}
|
|
1664
|
+
function layerTokens(elements) {
|
|
1665
|
+
const tokens = [];
|
|
1666
|
+
for (const element of elements) {
|
|
1667
|
+
const token = tokenOf2(element);
|
|
1668
|
+
if (token !== void 0) {
|
|
1669
|
+
tokens.push(token);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return tokens;
|
|
1673
|
+
}
|
|
1674
|
+
function deepTokens(elements) {
|
|
1675
|
+
const tokens = [];
|
|
1676
|
+
for (const element of elements) {
|
|
1677
|
+
const token = tokenOf2(element);
|
|
1678
|
+
if (token !== void 0) {
|
|
1679
|
+
tokens.push(token);
|
|
1680
|
+
}
|
|
1681
|
+
if (isTag(element)) {
|
|
1682
|
+
tokens.push(...deepTokens(element.children));
|
|
1683
|
+
} else if (isBranching(element)) {
|
|
1684
|
+
for (const option of Object.values(element.options)) {
|
|
1685
|
+
tokens.push(...deepTokens(option.value));
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return tokens;
|
|
1690
|
+
}
|
|
1691
|
+
function branchesByCategory(element) {
|
|
1692
|
+
return new Map(
|
|
1693
|
+
Object.entries(element.options).map(([category, option]) => [category, option.value])
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
function findUnconsumed(target, consumed, isMatch) {
|
|
1697
|
+
for (let index = 0; index < target.length; index += 1) {
|
|
1698
|
+
if (consumed.has(index)) {
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
const candidate = target[index];
|
|
1702
|
+
if (candidate !== void 0 && isMatch(candidate)) {
|
|
1703
|
+
return { element: candidate, index };
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return void 0;
|
|
1707
|
+
}
|
|
1708
|
+
function findMatchingBranching(source, target, consumed) {
|
|
1709
|
+
return findUnconsumed(
|
|
1710
|
+
target,
|
|
1711
|
+
consumed,
|
|
1712
|
+
(candidate) => isBranching(candidate) && candidate.value === source.value
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
function findMatchingTag(source, target, consumed) {
|
|
1716
|
+
return findUnconsumed(
|
|
1717
|
+
target,
|
|
1718
|
+
consumed,
|
|
1719
|
+
(candidate) => isTag(candidate) && candidate.value === source.value
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
function combineResults(results) {
|
|
1723
|
+
const missing = results.flatMap((result) => result.missing).sort();
|
|
1724
|
+
const extra = results.flatMap((result) => result.extra).sort();
|
|
1725
|
+
return { matches: missing.length === 0 && extra.length === 0, missing, extra, reordered: false };
|
|
1726
|
+
}
|
|
1727
|
+
function compareAgainstSourceUnion(sourceBranches, targetBranch) {
|
|
1728
|
+
const union = /* @__PURE__ */ new Set();
|
|
1729
|
+
for (const branch of sourceBranches.values()) {
|
|
1730
|
+
for (const token of deepTokens(branch)) {
|
|
1731
|
+
union.add(token);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const extra = deepTokens(targetBranch).filter((token) => !union.has(token)).sort();
|
|
1735
|
+
return { matches: extra.length === 0, missing: [], extra, reordered: false };
|
|
1736
|
+
}
|
|
1737
|
+
function compareBranching(source, target) {
|
|
1738
|
+
const sourceBranches = branchesByCategory(source);
|
|
1739
|
+
const targetBranches = branchesByCategory(target);
|
|
1740
|
+
const categories = /* @__PURE__ */ new Set([...sourceBranches.keys(), ...targetBranches.keys()]);
|
|
1741
|
+
const results = [];
|
|
1742
|
+
for (const category of categories) {
|
|
1743
|
+
const sourceBranch = sourceBranches.get(category);
|
|
1744
|
+
const targetBranch = targetBranches.get(category);
|
|
1745
|
+
if (sourceBranch !== void 0 && targetBranch !== void 0) {
|
|
1746
|
+
results.push(compareElements(sourceBranch, targetBranch));
|
|
1747
|
+
} else if (targetBranch !== void 0) {
|
|
1748
|
+
results.push(compareAgainstSourceUnion(sourceBranches, targetBranch));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return combineResults(results);
|
|
1752
|
+
}
|
|
1753
|
+
function nestedResults(source, target) {
|
|
1754
|
+
const results = [];
|
|
1755
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1756
|
+
for (const element of source) {
|
|
1757
|
+
if (isBranching(element)) {
|
|
1758
|
+
const found = findMatchingBranching(element, target, consumed);
|
|
1759
|
+
if (found !== void 0) {
|
|
1760
|
+
consumed.add(found.index);
|
|
1761
|
+
results.push(compareBranching(element, found.element));
|
|
1762
|
+
}
|
|
1763
|
+
} else if (isTag(element)) {
|
|
1764
|
+
const found = findMatchingTag(element, target, consumed);
|
|
1765
|
+
if (found !== void 0) {
|
|
1766
|
+
consumed.add(found.index);
|
|
1767
|
+
results.push(compareElements(element.children, found.element.children));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return results;
|
|
1772
|
+
}
|
|
1773
|
+
function compareElements(source, target) {
|
|
1774
|
+
const layer = checkPlaceholders(layerTokens(source), layerTokens(target));
|
|
1775
|
+
const nested = nestedResults(source, target);
|
|
1776
|
+
return nested.length === 0 ? layer : combineResults([layer, ...nested]);
|
|
1777
|
+
}
|
|
1778
|
+
function compareIcuPlaceholders(sourceValue, targetValue) {
|
|
1779
|
+
try {
|
|
1780
|
+
const sourceAst = parse(sourceValue);
|
|
1781
|
+
const targetAst = parse(targetValue);
|
|
1782
|
+
return compareElements(sourceAst, targetAst);
|
|
1783
|
+
} catch {
|
|
1784
|
+
return checkPlaceholders(icuPlaceholders(sourceValue), icuPlaceholders(targetValue));
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1319
1787
|
var AdapterError = class extends Error {
|
|
1320
1788
|
code;
|
|
1321
1789
|
constructor(code, message) {
|
|
@@ -1617,9 +2085,9 @@ function unflattenEntries(entries) {
|
|
|
1617
2085
|
}
|
|
1618
2086
|
return root;
|
|
1619
2087
|
}
|
|
1620
|
-
function toEntries(content, namespace,
|
|
2088
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree) {
|
|
1621
2089
|
try {
|
|
1622
|
-
const tree =
|
|
2090
|
+
const tree = parse3(content);
|
|
1623
2091
|
validateTree?.(tree);
|
|
1624
2092
|
return flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
1625
2093
|
} catch (error) {
|
|
@@ -1631,7 +2099,7 @@ function createTreeFileAdapter(options) {
|
|
|
1631
2099
|
format,
|
|
1632
2100
|
extensions,
|
|
1633
2101
|
sniff,
|
|
1634
|
-
parse:
|
|
2102
|
+
parse: parse3,
|
|
1635
2103
|
serialize,
|
|
1636
2104
|
deriveEntry,
|
|
1637
2105
|
extractPlaceholders,
|
|
@@ -1639,6 +2107,7 @@ function createTreeFileAdapter(options) {
|
|
|
1639
2107
|
validateMessage,
|
|
1640
2108
|
validateTree,
|
|
1641
2109
|
buildWriteTree,
|
|
2110
|
+
comparePlaceholders,
|
|
1642
2111
|
keyMode = "literal-leaf"
|
|
1643
2112
|
} = options;
|
|
1644
2113
|
return {
|
|
@@ -1646,10 +2115,11 @@ function createTreeFileAdapter(options) {
|
|
|
1646
2115
|
canHandle: buildCanHandle(extensions, sniff),
|
|
1647
2116
|
extractPlaceholders,
|
|
1648
2117
|
validateMessage: validateMessage ?? (() => true),
|
|
2118
|
+
...comparePlaceholders !== void 0 ? { comparePlaceholders } : {},
|
|
1649
2119
|
async read(filePath, locale) {
|
|
1650
2120
|
const content = await readFileContent(filePath);
|
|
1651
2121
|
const namespace = namespaceOf(filePath);
|
|
1652
|
-
const entries = toEntries(content, namespace,
|
|
2122
|
+
const entries = toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree);
|
|
1653
2123
|
const resource = { locale, namespace, format, entries };
|
|
1654
2124
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1655
2125
|
return { resource, invalidIcuKeys };
|
|
@@ -1745,6 +2215,7 @@ function createArbAdapter() {
|
|
|
1745
2215
|
deriveEntry: icuDeriveEntry,
|
|
1746
2216
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1747
2217
|
validateMessage: icuIsValid,
|
|
2218
|
+
comparePlaceholders: compareIcuPlaceholders,
|
|
1748
2219
|
buildWriteTree: buildArbWriteTree
|
|
1749
2220
|
});
|
|
1750
2221
|
}
|
|
@@ -1808,7 +2279,8 @@ function createNextIntlJsonAdapter() {
|
|
|
1808
2279
|
extractPlaceholders: icuPlaceholders,
|
|
1809
2280
|
deriveEntry: icuDeriveEntry,
|
|
1810
2281
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1811
|
-
validateMessage: icuIsValid
|
|
2282
|
+
validateMessage: icuIsValid,
|
|
2283
|
+
comparePlaceholders: compareIcuPlaceholders
|
|
1812
2284
|
});
|
|
1813
2285
|
}
|
|
1814
2286
|
function assertNoDottedNestedKey(tree) {
|
|
@@ -2010,6 +2482,7 @@ function extractXliffPlaceholders(value) {
|
|
|
2010
2482
|
return result;
|
|
2011
2483
|
}
|
|
2012
2484
|
var ELEMENT_NODE = 1;
|
|
2485
|
+
var TEXT_NODE = 3;
|
|
2013
2486
|
function isElement(node) {
|
|
2014
2487
|
return node.nodeType === ELEMENT_NODE;
|
|
2015
2488
|
}
|
|
@@ -2100,6 +2573,12 @@ function parseXliffEntries(content, namespace) {
|
|
|
2100
2573
|
const serializer = new XMLSerializer();
|
|
2101
2574
|
const out = /* @__PURE__ */ new Map();
|
|
2102
2575
|
for (const unit of walkUnits(root)) {
|
|
2576
|
+
if (out.has(unit.key)) {
|
|
2577
|
+
throw new AdapterError(
|
|
2578
|
+
"INVALID_STRUCTURE",
|
|
2579
|
+
"The XLIFF file has two trans-units with the same id."
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2103
2582
|
const value = unitValue(serializer, unit);
|
|
2104
2583
|
out.set(unit.key, {
|
|
2105
2584
|
key: unit.key,
|
|
@@ -2126,10 +2605,67 @@ async function readDestination(filePath) {
|
|
|
2126
2605
|
}
|
|
2127
2606
|
return outcome.content;
|
|
2128
2607
|
}
|
|
2608
|
+
var XLIFF_INLINE_ELEMENTS = /* @__PURE__ */ new Set(["x", "g", "bx", "ex", "ph", "it", "mrk"]);
|
|
2609
|
+
var XLIFF_NAMESPACES = /* @__PURE__ */ new Set([
|
|
2610
|
+
"urn:oasis:names:tc:xliff:document:1.2",
|
|
2611
|
+
"urn:oasis:names:tc:xliff:document:2.0"
|
|
2612
|
+
]);
|
|
2613
|
+
var INLINE_ELEMENT_ATTRIBUTES = {
|
|
2614
|
+
x: ["id", "ctype"],
|
|
2615
|
+
ph: ["id", "ctype"],
|
|
2616
|
+
g: ["id", "ctype"],
|
|
2617
|
+
bx: ["id", "rid", "ctype"],
|
|
2618
|
+
ex: ["id", "rid"],
|
|
2619
|
+
it: ["id", "pos", "ctype"],
|
|
2620
|
+
mrk: ["id", "mtype"]
|
|
2621
|
+
};
|
|
2622
|
+
function isAllowedInlineElement(element) {
|
|
2623
|
+
const namespace = element.namespaceURI;
|
|
2624
|
+
const hasAllowedNamespace = namespace === null || XLIFF_NAMESPACES.has(namespace);
|
|
2625
|
+
return hasAllowedNamespace && XLIFF_INLINE_ELEMENTS.has(element.localName ?? "");
|
|
2626
|
+
}
|
|
2627
|
+
function isAllowedFragmentNode(node) {
|
|
2628
|
+
if (node.nodeType === TEXT_NODE) {
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
return isElement(node) && isAllowedInlineElement(node);
|
|
2632
|
+
}
|
|
2633
|
+
function allDescendantNodes(node) {
|
|
2634
|
+
const children = Array.from(node.childNodes);
|
|
2635
|
+
return children.flatMap((child) => [child, ...allDescendantNodes(child)]);
|
|
2636
|
+
}
|
|
2637
|
+
function hasDisallowedNode(root) {
|
|
2638
|
+
return allDescendantNodes(root).some((node) => !isAllowedFragmentNode(node));
|
|
2639
|
+
}
|
|
2640
|
+
function attributeNames(element) {
|
|
2641
|
+
const names = [];
|
|
2642
|
+
for (let i = 0; i < element.attributes.length; i += 1) {
|
|
2643
|
+
const attr = element.attributes.item(i);
|
|
2644
|
+
if (attr !== null) {
|
|
2645
|
+
names.push(attr.name);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
return names;
|
|
2649
|
+
}
|
|
2650
|
+
function sanitizeInlineAttributes(root) {
|
|
2651
|
+
for (const el of collectByTag(root, "*")) {
|
|
2652
|
+
const allowed = INLINE_ELEMENT_ATTRIBUTES[el.localName ?? ""] ?? [];
|
|
2653
|
+
for (const name of attributeNames(el)) {
|
|
2654
|
+
if (!allowed.includes(name)) {
|
|
2655
|
+
el.removeAttribute(name);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2129
2660
|
function fragmentNodes(parser, value) {
|
|
2661
|
+
assertNoDoctype(value);
|
|
2130
2662
|
try {
|
|
2131
2663
|
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
2132
|
-
|
|
2664
|
+
if (root === null || hasDisallowedNode(root)) {
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
sanitizeInlineAttributes(root);
|
|
2668
|
+
return Array.from(root.childNodes);
|
|
2133
2669
|
} catch {
|
|
2134
2670
|
return null;
|
|
2135
2671
|
}
|
|
@@ -2179,7 +2715,7 @@ function createXliffAdapter() {
|
|
|
2179
2715
|
function parseYamlObject(content) {
|
|
2180
2716
|
let parsed;
|
|
2181
2717
|
try {
|
|
2182
|
-
parsed = parse(content, { maxAliasCount: 100 });
|
|
2718
|
+
parsed = parse$1(content, { maxAliasCount: 100 });
|
|
2183
2719
|
} catch {
|
|
2184
2720
|
throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
|
|
2185
2721
|
}
|
|
@@ -2583,7 +3119,8 @@ function buildRequest2(context, entries) {
|
|
|
2583
3119
|
entries,
|
|
2584
3120
|
extractPlaceholders: context.adapter.extractPlaceholders,
|
|
2585
3121
|
...context.glossary !== void 0 ? { glossary: context.glossary } : {},
|
|
2586
|
-
...context.tone !== void 0 ? { tone: context.tone } : {}
|
|
3122
|
+
...context.tone !== void 0 ? { tone: context.tone } : {},
|
|
3123
|
+
...context.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: context.adapter.comparePlaceholders } : {}
|
|
2587
3124
|
};
|
|
2588
3125
|
}
|
|
2589
3126
|
async function generatePluralForms(context) {
|
|
@@ -2653,7 +3190,8 @@ function buildRequest3(params, entries) {
|
|
|
2653
3190
|
entries,
|
|
2654
3191
|
extractPlaceholders: params.adapter.extractPlaceholders,
|
|
2655
3192
|
...params.glossary !== void 0 ? { glossary: params.glossary } : {},
|
|
2656
|
-
...params.tone !== void 0 ? { tone: params.tone } : {}
|
|
3193
|
+
...params.tone !== void 0 ? { tone: params.tone } : {},
|
|
3194
|
+
...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
|
|
2657
3195
|
};
|
|
2658
3196
|
}
|
|
2659
3197
|
async function runLocale(params) {
|
|
@@ -3116,7 +3654,7 @@ async function streamEntryBounded(file, remaining) {
|
|
|
3116
3654
|
"The workbook decompresses to more than the permitted maximum bytes."
|
|
3117
3655
|
);
|
|
3118
3656
|
}
|
|
3119
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
3657
|
+
return { raw, content: Buffer.concat(chunks).toString("utf8") };
|
|
3120
3658
|
}
|
|
3121
3659
|
async function guardWorkbookBytes(bytes, limits) {
|
|
3122
3660
|
let zip;
|
|
@@ -3148,14 +3686,8 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
3148
3686
|
let actualTotal = 0;
|
|
3149
3687
|
for (const file of files) {
|
|
3150
3688
|
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
|
-
}
|
|
3689
|
+
const { raw, content } = await streamEntryBounded(file, remaining);
|
|
3690
|
+
actualTotal += raw;
|
|
3159
3691
|
assertNoDoctype2(file.name, content);
|
|
3160
3692
|
}
|
|
3161
3693
|
}
|
|
@@ -3344,10 +3876,7 @@ function judge(row, sourceEntry, adapter) {
|
|
|
3344
3876
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
3345
3877
|
return "drift";
|
|
3346
3878
|
}
|
|
3347
|
-
const integrity = checkPlaceholders(
|
|
3348
|
-
sourceEntry.placeholders,
|
|
3349
|
-
adapter.extractPlaceholders(row.translation)
|
|
3350
|
-
);
|
|
3879
|
+
const integrity = adapter.comparePlaceholders !== void 0 ? adapter.comparePlaceholders(sourceEntry.value, row.translation) : checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(row.translation));
|
|
3351
3880
|
if (!integrity.matches) {
|
|
3352
3881
|
return "placeholder";
|
|
3353
3882
|
}
|