@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.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
|
}
|
|
@@ -407,16 +503,14 @@ function validateRequest(request) {
|
|
|
407
503
|
return parsed.data;
|
|
408
504
|
}
|
|
409
505
|
function toIntegrityInputs(entries, values) {
|
|
410
|
-
|
|
506
|
+
const inputs = [];
|
|
507
|
+
for (const entry of entries) {
|
|
411
508
|
const translatedValue = values.get(entry.key);
|
|
412
|
-
if (translatedValue
|
|
413
|
-
|
|
414
|
-
"INVALID_RESPONSE",
|
|
415
|
-
"The provider response is missing one or more keys."
|
|
416
|
-
);
|
|
509
|
+
if (translatedValue !== void 0) {
|
|
510
|
+
inputs.push({ key: entry.key, sourceValue: entry.value, translatedValue });
|
|
417
511
|
}
|
|
418
|
-
|
|
419
|
-
|
|
512
|
+
}
|
|
513
|
+
return inputs;
|
|
420
514
|
}
|
|
421
515
|
function toItem(entry) {
|
|
422
516
|
return {
|
|
@@ -448,25 +542,29 @@ function deriveJsonSchema(schema) {
|
|
|
448
542
|
}
|
|
449
543
|
return result;
|
|
450
544
|
}
|
|
451
|
-
function
|
|
452
|
-
const
|
|
453
|
-
const
|
|
545
|
+
function partitionTranslations(translations, requested) {
|
|
546
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
547
|
+
const duplicated = /* @__PURE__ */ new Set();
|
|
454
548
|
for (const { key, value } of translations) {
|
|
455
|
-
if (!requested.has(key)
|
|
456
|
-
throw new ProviderError(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
);
|
|
549
|
+
if (!requested.has(key)) {
|
|
550
|
+
throw new ProviderError("INVALID_RESPONSE", "The provider returned an unexpected key.");
|
|
551
|
+
}
|
|
552
|
+
if (firstSeen.has(key)) {
|
|
553
|
+
duplicated.add(key);
|
|
554
|
+
continue;
|
|
460
555
|
}
|
|
461
|
-
|
|
556
|
+
firstSeen.set(key, value);
|
|
462
557
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
558
|
+
return { firstSeen, duplicated };
|
|
559
|
+
}
|
|
560
|
+
function reconcile(translations, requestedKeys) {
|
|
561
|
+
const requested = new Set(requestedKeys);
|
|
562
|
+
const { firstSeen, duplicated } = partitionTranslations(translations, requested);
|
|
563
|
+
const missingKeys = requestedKeys.filter((key) => duplicated.has(key) || !firstSeen.has(key));
|
|
564
|
+
for (const key of duplicated) {
|
|
565
|
+
firstSeen.delete(key);
|
|
468
566
|
}
|
|
469
|
-
return
|
|
567
|
+
return { accepted: firstSeen, missingKeys };
|
|
470
568
|
}
|
|
471
569
|
function reconcileResult(raw, requestedKeys) {
|
|
472
570
|
const parsed = translationsResultSchema.safeParse(raw);
|
|
@@ -478,17 +576,54 @@ function reconcileResult(raw, requestedKeys) {
|
|
|
478
576
|
}
|
|
479
577
|
return reconcile(parsed.data.translations, requestedKeys);
|
|
480
578
|
}
|
|
579
|
+
var MAX_REPAIR_ROUNDS = 1;
|
|
481
580
|
async function runLlmTranslation(request, mechanism) {
|
|
482
581
|
const data = validateRequest(request);
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
|
|
582
|
+
const signal = request.signal;
|
|
583
|
+
const first = await requestTranslations(mechanism, data, signal);
|
|
584
|
+
const values = first.outcome.accepted;
|
|
585
|
+
let usage = first.completion.usage;
|
|
586
|
+
let toRepair = entriesFor(data.entries, first.outcome.missingKeys);
|
|
587
|
+
for (let round = 0; round < MAX_REPAIR_ROUNDS && toRepair.length > 0; round += 1) {
|
|
588
|
+
const repair = await requestTranslations(mechanism, { ...data, entries: toRepair }, signal);
|
|
589
|
+
for (const [key, value] of repair.outcome.accepted) {
|
|
590
|
+
values.set(key, value);
|
|
591
|
+
}
|
|
592
|
+
usage = mergeUsage(usage, repair.completion.usage);
|
|
593
|
+
toRepair = entriesFor(data.entries, repair.outcome.missingKeys);
|
|
594
|
+
}
|
|
487
595
|
const integrity = checkBatchIntegrity(
|
|
488
596
|
toIntegrityInputs(data.entries, values),
|
|
489
|
-
request.extractPlaceholders
|
|
597
|
+
request.extractPlaceholders,
|
|
598
|
+
request.comparePlaceholders
|
|
490
599
|
);
|
|
491
|
-
return
|
|
600
|
+
return usage === void 0 ? { values, integrity, notices: [] } : { values, integrity, usage, notices: [] };
|
|
601
|
+
}
|
|
602
|
+
async function requestTranslations(mechanism, data, signal) {
|
|
603
|
+
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
604
|
+
const requestedKeys = data.entries.map((entry) => entry.key);
|
|
605
|
+
const completion = await mechanism.translate({
|
|
606
|
+
payloadJson,
|
|
607
|
+
requestedKeys,
|
|
608
|
+
...signal !== void 0 ? { signal } : {}
|
|
609
|
+
});
|
|
610
|
+
return { completion, outcome: reconcileResult(completion.raw, requestedKeys) };
|
|
611
|
+
}
|
|
612
|
+
function entriesFor(entries, keys) {
|
|
613
|
+
const wanted = new Set(keys);
|
|
614
|
+
return entries.filter((entry) => wanted.has(entry.key));
|
|
615
|
+
}
|
|
616
|
+
function mergeUsage(first, second) {
|
|
617
|
+
if (first === void 0) {
|
|
618
|
+
return second;
|
|
619
|
+
}
|
|
620
|
+
if (second === void 0) {
|
|
621
|
+
return first;
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
inputTokens: first.inputTokens + second.inputTokens,
|
|
625
|
+
outputTokens: first.outputTokens + second.outputTokens
|
|
626
|
+
};
|
|
492
627
|
}
|
|
493
628
|
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.";
|
|
494
629
|
function assertNotTruncated(truncated) {
|
|
@@ -521,12 +656,29 @@ function requireGeminiKey() {
|
|
|
521
656
|
function requireDeepLKey() {
|
|
522
657
|
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
523
658
|
}
|
|
659
|
+
var OPENAI_COMPATIBLE_ENV_VAR = "OPENAI_COMPATIBLE_API_KEY";
|
|
660
|
+
var OPENAI_COMPATIBLE_KEY_PLACEHOLDER = "local";
|
|
661
|
+
function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
662
|
+
const varName = customEnvVar ?? OPENAI_COMPATIBLE_ENV_VAR;
|
|
663
|
+
const value = process.env[varName];
|
|
664
|
+
if (value !== void 0 && value.length > 0) {
|
|
665
|
+
return value;
|
|
666
|
+
}
|
|
667
|
+
if (customEnvVar !== void 0) {
|
|
668
|
+
throw new ProviderError(
|
|
669
|
+
"MISSING_API_KEY",
|
|
670
|
+
`The ${customEnvVar} environment variable is not set.`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return OPENAI_COMPATIBLE_KEY_PLACEHOLDER;
|
|
674
|
+
}
|
|
524
675
|
function createDefaultClient() {
|
|
525
676
|
const sdk = new Anthropic__default.default({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
526
677
|
return {
|
|
527
678
|
messages: {
|
|
528
|
-
create: async (body) => await sdk.messages.create(
|
|
529
|
-
body
|
|
679
|
+
create: async (body, options) => await sdk.messages.create(
|
|
680
|
+
body,
|
|
681
|
+
options
|
|
530
682
|
)
|
|
531
683
|
}
|
|
532
684
|
};
|
|
@@ -594,9 +746,9 @@ function createAnthropicProvider(config, deps = {}) {
|
|
|
594
746
|
}
|
|
595
747
|
function createMechanism(client, config) {
|
|
596
748
|
return {
|
|
597
|
-
translate: async ({ payloadJson }) => {
|
|
749
|
+
translate: async ({ payloadJson, signal }) => {
|
|
598
750
|
const body = buildRequest(config, payloadJson);
|
|
599
|
-
const message = await callClient(client, body);
|
|
751
|
+
const message = await callClient(client, body, signal);
|
|
600
752
|
assertNotTruncated(message.stop_reason === "max_tokens");
|
|
601
753
|
const raw = requireToolInput(message.content);
|
|
602
754
|
const usage = toUsage(message.usage);
|
|
@@ -604,8 +756,11 @@ function createMechanism(client, config) {
|
|
|
604
756
|
}
|
|
605
757
|
};
|
|
606
758
|
}
|
|
607
|
-
function callClient(client, body) {
|
|
608
|
-
return guardProviderCall(
|
|
759
|
+
function callClient(client, body, signal) {
|
|
760
|
+
return guardProviderCall(
|
|
761
|
+
() => client.messages.create(body, signal !== void 0 ? { signal } : void 0),
|
|
762
|
+
signal
|
|
763
|
+
);
|
|
609
764
|
}
|
|
610
765
|
function toUsage(usage) {
|
|
611
766
|
if (usage === void 0) {
|
|
@@ -700,7 +855,7 @@ function zipResults(entries, results) {
|
|
|
700
855
|
values.set(entry.key, translatedValue);
|
|
701
856
|
integrityInputs.push({
|
|
702
857
|
key: entry.key,
|
|
703
|
-
|
|
858
|
+
sourceValue: entry.value,
|
|
704
859
|
translatedValue
|
|
705
860
|
});
|
|
706
861
|
}
|
|
@@ -716,7 +871,10 @@ function createDeepLProvider(config, deps = {}) {
|
|
|
716
871
|
return {
|
|
717
872
|
id: PROVIDER_ID2,
|
|
718
873
|
kind: "machine-translation",
|
|
719
|
-
|
|
874
|
+
// Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
|
|
875
|
+
// generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
|
|
876
|
+
// silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
|
|
877
|
+
supportsGlossary: validConfig.glossaryId !== void 0,
|
|
720
878
|
translateBatch: (request) => translate(bundle, validConfig, request)
|
|
721
879
|
};
|
|
722
880
|
}
|
|
@@ -741,36 +899,97 @@ async function translate(bundle, config, request) {
|
|
|
741
899
|
data,
|
|
742
900
|
protectable,
|
|
743
901
|
options,
|
|
744
|
-
request.extractPlaceholders
|
|
902
|
+
request.extractPlaceholders,
|
|
903
|
+
request.comparePlaceholders,
|
|
904
|
+
request.signal
|
|
745
905
|
);
|
|
746
906
|
if (unprotectable.length > 0) {
|
|
747
907
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
748
908
|
}
|
|
749
909
|
return { values, integrity, notices };
|
|
750
910
|
}
|
|
751
|
-
async function translateProtectable(client, data, protectable, options, extract) {
|
|
911
|
+
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
752
912
|
if (protectable.length === 0) {
|
|
753
913
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
754
914
|
}
|
|
755
915
|
const texts = protectable.map((entry) => entry.value);
|
|
756
|
-
const results = await callClient2(
|
|
916
|
+
const results = await callClient2(
|
|
917
|
+
client,
|
|
918
|
+
texts,
|
|
919
|
+
data.sourceLocale,
|
|
920
|
+
data.targetLocale,
|
|
921
|
+
options,
|
|
922
|
+
signal
|
|
923
|
+
);
|
|
757
924
|
const { values, integrityInputs } = zipResults(protectable, results);
|
|
758
|
-
const integrity = checkBatchIntegrity(integrityInputs, extract);
|
|
925
|
+
const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
|
|
759
926
|
return { values, integrity };
|
|
760
927
|
}
|
|
761
|
-
function callClient2(client, texts, sourceLang, targetLang, options) {
|
|
762
|
-
return guardProviderCall(
|
|
928
|
+
function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
929
|
+
return guardProviderCall(
|
|
930
|
+
() => client.translateText(texts, sourceLang, targetLang, options),
|
|
931
|
+
signal
|
|
932
|
+
);
|
|
763
933
|
}
|
|
764
934
|
var geminiConfigSchema = zod.z.object({
|
|
765
935
|
model: zod.z.string().min(1),
|
|
766
936
|
maxOutputTokens: zod.z.number().int().positive()
|
|
767
937
|
});
|
|
938
|
+
var DEFAULT_GEMINI_RETRY = { attempts: 3, baseDelayMs: 250 };
|
|
939
|
+
function isRetryableStatus(error) {
|
|
940
|
+
const status = getErrorStatus(error);
|
|
941
|
+
return status === 429 || status !== void 0 && status >= 500 && status < 600;
|
|
942
|
+
}
|
|
943
|
+
function isAborted(signal) {
|
|
944
|
+
return signal?.aborted === true;
|
|
945
|
+
}
|
|
946
|
+
function throwAbort(signal) {
|
|
947
|
+
signal?.throwIfAborted();
|
|
948
|
+
throw new DOMException("This operation was aborted.", "AbortError");
|
|
949
|
+
}
|
|
950
|
+
function delay(ms, signal) {
|
|
951
|
+
return new Promise((resolve7) => {
|
|
952
|
+
const onAbort = () => {
|
|
953
|
+
clearTimeout(timer);
|
|
954
|
+
resolve7();
|
|
955
|
+
};
|
|
956
|
+
const timer = setTimeout(() => {
|
|
957
|
+
signal?.removeEventListener("abort", onAbort);
|
|
958
|
+
resolve7();
|
|
959
|
+
}, ms);
|
|
960
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
async function withGeminiRetry(call, signal, config = DEFAULT_GEMINI_RETRY) {
|
|
964
|
+
let attempt = 1;
|
|
965
|
+
while (true) {
|
|
966
|
+
try {
|
|
967
|
+
return await call();
|
|
968
|
+
} catch (error) {
|
|
969
|
+
if (isAborted(signal)) {
|
|
970
|
+
throwAbort(signal);
|
|
971
|
+
}
|
|
972
|
+
const exhausted = attempt >= config.attempts;
|
|
973
|
+
if (exhausted || !isRetryableStatus(error)) {
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
await delay(config.baseDelayMs * 2 ** (attempt - 1), signal);
|
|
977
|
+
if (isAborted(signal)) {
|
|
978
|
+
throwAbort(signal);
|
|
979
|
+
}
|
|
980
|
+
attempt += 1;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
768
984
|
function createDefaultClient3() {
|
|
769
985
|
const ai = new genai.GoogleGenAI({ apiKey: requireGeminiKey() });
|
|
770
986
|
return {
|
|
771
987
|
models: {
|
|
772
|
-
generateContent:
|
|
773
|
-
|
|
988
|
+
generateContent: (request) => withGeminiRetry(
|
|
989
|
+
async () => await ai.models.generateContent(
|
|
990
|
+
request
|
|
991
|
+
),
|
|
992
|
+
request.config.abortSignal
|
|
774
993
|
)
|
|
775
994
|
}
|
|
776
995
|
};
|
|
@@ -834,7 +1053,7 @@ var GEMINI_SYSTEM_RULES = [
|
|
|
834
1053
|
"When a tone is provided, honor it.",
|
|
835
1054
|
"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
1055
|
].join("\n");
|
|
837
|
-
function buildGeminiRequest(config, payloadJson) {
|
|
1056
|
+
function buildGeminiRequest(config, payloadJson, signal) {
|
|
838
1057
|
return {
|
|
839
1058
|
model: config.model,
|
|
840
1059
|
contents: [{ role: "user", parts: [{ text: payloadJson }] }],
|
|
@@ -842,7 +1061,8 @@ function buildGeminiRequest(config, payloadJson) {
|
|
|
842
1061
|
systemInstruction: GEMINI_SYSTEM_RULES,
|
|
843
1062
|
responseMimeType: "application/json",
|
|
844
1063
|
responseSchema: toGeminiSchema(deriveJsonSchema(translationsResultSchema)),
|
|
845
|
-
maxOutputTokens: config.maxOutputTokens
|
|
1064
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
1065
|
+
...signal !== void 0 ? { abortSignal: signal } : {}
|
|
846
1066
|
}
|
|
847
1067
|
};
|
|
848
1068
|
}
|
|
@@ -906,15 +1126,15 @@ function createGeminiProvider(config, deps = {}) {
|
|
|
906
1126
|
}
|
|
907
1127
|
function createMechanism2(client, config) {
|
|
908
1128
|
return {
|
|
909
|
-
translate: async ({ payloadJson }) => {
|
|
910
|
-
const request = buildGeminiRequest(config, payloadJson);
|
|
911
|
-
const response = await callClient3(client, request);
|
|
1129
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1130
|
+
const request = buildGeminiRequest(config, payloadJson, signal);
|
|
1131
|
+
const response = await callClient3(client, request, signal);
|
|
912
1132
|
return extractGeminiResult(response);
|
|
913
1133
|
}
|
|
914
1134
|
};
|
|
915
1135
|
}
|
|
916
|
-
function callClient3(client, request) {
|
|
917
|
-
return guardProviderCall(() => client.models.generateContent(request));
|
|
1136
|
+
function callClient3(client, request, signal) {
|
|
1137
|
+
return guardProviderCall(() => client.models.generateContent(request), signal);
|
|
918
1138
|
}
|
|
919
1139
|
var openAiConfigSchema = zod.z.object({
|
|
920
1140
|
model: zod.z.string().min(1),
|
|
@@ -925,8 +1145,9 @@ function createDefaultClient4() {
|
|
|
925
1145
|
return {
|
|
926
1146
|
chat: {
|
|
927
1147
|
completions: {
|
|
928
|
-
create: async (body) => await sdk.chat.completions.create(
|
|
929
|
-
body
|
|
1148
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1149
|
+
body,
|
|
1150
|
+
options
|
|
930
1151
|
)
|
|
931
1152
|
}
|
|
932
1153
|
}
|
|
@@ -944,27 +1165,90 @@ var OPENAI_SYSTEM_RULES = [
|
|
|
944
1165
|
"When a tone is provided, honor it.",
|
|
945
1166
|
"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
1167
|
].join("\n");
|
|
947
|
-
function
|
|
1168
|
+
function buildResponseFormat(mode) {
|
|
1169
|
+
if (mode === "json-object") {
|
|
1170
|
+
return { type: "json_object" };
|
|
1171
|
+
}
|
|
948
1172
|
return {
|
|
949
|
-
|
|
950
|
-
|
|
1173
|
+
type: "json_schema",
|
|
1174
|
+
json_schema: {
|
|
1175
|
+
name: RESULT_SCHEMA_NAME,
|
|
1176
|
+
strict: true,
|
|
1177
|
+
schema: deriveJsonSchema(translationsResultSchema)
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema", tokenLimitField = "max_completion_tokens") {
|
|
1182
|
+
const base = {
|
|
951
1183
|
messages: [
|
|
952
1184
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
953
1185
|
{ role: "user", content: payloadJson }
|
|
954
1186
|
],
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1187
|
+
model: config.model,
|
|
1188
|
+
response_format: buildResponseFormat(mode)
|
|
1189
|
+
};
|
|
1190
|
+
if (tokenLimitField === "max_tokens") {
|
|
1191
|
+
return { ...base, max_tokens: config.maxOutputTokens };
|
|
1192
|
+
}
|
|
1193
|
+
return { ...base, max_completion_tokens: config.maxOutputTokens };
|
|
1194
|
+
}
|
|
1195
|
+
function advanceStringScan(char, escaped) {
|
|
1196
|
+
if (escaped) {
|
|
1197
|
+
return { inString: true, escaped: false };
|
|
1198
|
+
}
|
|
1199
|
+
if (char === "\\") {
|
|
1200
|
+
return { inString: true, escaped: true };
|
|
1201
|
+
}
|
|
1202
|
+
return { inString: char !== '"', escaped: false };
|
|
1203
|
+
}
|
|
1204
|
+
function scanBalancedObjectEnd(content, start) {
|
|
1205
|
+
let depth = 0;
|
|
1206
|
+
let inString = false;
|
|
1207
|
+
let escaped = false;
|
|
1208
|
+
for (let i = start; i < content.length; i += 1) {
|
|
1209
|
+
const char = content[i];
|
|
1210
|
+
if (inString) {
|
|
1211
|
+
({ inString, escaped } = advanceStringScan(char, escaped));
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (char === '"') {
|
|
1215
|
+
inString = true;
|
|
1216
|
+
} else if (char === "{") {
|
|
1217
|
+
depth += 1;
|
|
1218
|
+
} else if (char === "}") {
|
|
1219
|
+
depth -= 1;
|
|
1220
|
+
if (depth === 0) {
|
|
1221
|
+
return i;
|
|
961
1222
|
}
|
|
962
1223
|
}
|
|
963
|
-
}
|
|
1224
|
+
}
|
|
1225
|
+
return null;
|
|
964
1226
|
}
|
|
965
|
-
function
|
|
1227
|
+
function extractBalancedJson(content) {
|
|
1228
|
+
let searchFrom = 0;
|
|
1229
|
+
while (searchFrom < content.length) {
|
|
1230
|
+
const start = content.indexOf("{", searchFrom);
|
|
1231
|
+
if (start === -1) {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
const end = scanBalancedObjectEnd(content, start);
|
|
1235
|
+
if (end === null) {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
const candidate = content.slice(start, end + 1);
|
|
1239
|
+
try {
|
|
1240
|
+
JSON.parse(candidate);
|
|
1241
|
+
return candidate;
|
|
1242
|
+
} catch {
|
|
1243
|
+
searchFrom = end + 1;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
function parseContent2(content, tolerant) {
|
|
1249
|
+
const candidate = tolerant ? extractBalancedJson(content) ?? content : content;
|
|
966
1250
|
try {
|
|
967
|
-
return JSON.parse(
|
|
1251
|
+
return JSON.parse(candidate);
|
|
968
1252
|
} catch {
|
|
969
1253
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned unparseable content.");
|
|
970
1254
|
}
|
|
@@ -979,7 +1263,7 @@ function toUsage3(usage) {
|
|
|
979
1263
|
}
|
|
980
1264
|
return { inputTokens: prompt_tokens, outputTokens: completion_tokens };
|
|
981
1265
|
}
|
|
982
|
-
function extractOpenAiResult(completion) {
|
|
1266
|
+
function extractOpenAiResult(completion, tolerant = false) {
|
|
983
1267
|
const choice = completion.choices[0];
|
|
984
1268
|
if (choice === void 0) {
|
|
985
1269
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no message.");
|
|
@@ -992,7 +1276,7 @@ function extractOpenAiResult(completion) {
|
|
|
992
1276
|
if (message.content === void 0 || message.content === null) {
|
|
993
1277
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no translation content.");
|
|
994
1278
|
}
|
|
995
|
-
const raw = parseContent2(message.content);
|
|
1279
|
+
const raw = parseContent2(message.content, tolerant);
|
|
996
1280
|
const usage = toUsage3(completion.usage);
|
|
997
1281
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
998
1282
|
}
|
|
@@ -1010,15 +1294,84 @@ function createOpenAiProvider(config, deps = {}) {
|
|
|
1010
1294
|
}
|
|
1011
1295
|
function createMechanism3(client, config) {
|
|
1012
1296
|
return {
|
|
1013
|
-
translate: async ({ payloadJson }) => {
|
|
1297
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1014
1298
|
const body = buildOpenAiRequest(config, payloadJson);
|
|
1015
|
-
const completion = await callClient4(client, body);
|
|
1299
|
+
const completion = await callClient4(client, body, signal);
|
|
1016
1300
|
return extractOpenAiResult(completion);
|
|
1017
1301
|
}
|
|
1018
1302
|
};
|
|
1019
1303
|
}
|
|
1020
|
-
function callClient4(client, body) {
|
|
1021
|
-
return guardProviderCall(
|
|
1304
|
+
function callClient4(client, body, signal) {
|
|
1305
|
+
return guardProviderCall(
|
|
1306
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1307
|
+
signal
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
var HOSTED_PROVIDER_ENV_VARS = new Set(
|
|
1311
|
+
Object.values(PROVIDER_ENV).map((name) => name.toUpperCase())
|
|
1312
|
+
);
|
|
1313
|
+
function isNotHostedProviderEnvVar(value) {
|
|
1314
|
+
return !HOSTED_PROVIDER_ENV_VARS.has(value.toUpperCase());
|
|
1315
|
+
}
|
|
1316
|
+
function isHttpOrHttpsUrl(value) {
|
|
1317
|
+
try {
|
|
1318
|
+
const { protocol } = new URL(value);
|
|
1319
|
+
return protocol === "http:" || protocol === "https:";
|
|
1320
|
+
} catch {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
var openAiCompatibleConfigSchema = zod.z.object({
|
|
1325
|
+
baseUrl: zod.z.url({ message: "baseUrl must be a valid absolute URL." }).refine(isHttpOrHttpsUrl, { message: "baseUrl must use the http or https scheme." }),
|
|
1326
|
+
model: zod.z.string().min(1),
|
|
1327
|
+
maxOutputTokens: zod.z.number().int().positive(),
|
|
1328
|
+
apiKeyEnvVar: zod.z.string().min(1).refine(isNotHostedProviderEnvVar, {
|
|
1329
|
+
message: "apiKeyEnvVar must not name a hosted provider's environment variable."
|
|
1330
|
+
}).optional()
|
|
1331
|
+
});
|
|
1332
|
+
function createDefaultClient5(config) {
|
|
1333
|
+
const sdk = new OpenAI__default.default({
|
|
1334
|
+
apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
|
|
1335
|
+
baseURL: config.baseUrl,
|
|
1336
|
+
logLevel: "off"
|
|
1337
|
+
});
|
|
1338
|
+
return {
|
|
1339
|
+
chat: {
|
|
1340
|
+
completions: {
|
|
1341
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1342
|
+
body,
|
|
1343
|
+
options
|
|
1344
|
+
)
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
var PROVIDER_ID5 = "openai-compatible";
|
|
1350
|
+
function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
1351
|
+
const validConfig = openAiCompatibleConfigSchema.parse(config);
|
|
1352
|
+
const client = deps.client ?? createDefaultClient5(validConfig);
|
|
1353
|
+
const mechanism = createMechanism4(client, validConfig);
|
|
1354
|
+
return {
|
|
1355
|
+
id: PROVIDER_ID5,
|
|
1356
|
+
kind: "llm",
|
|
1357
|
+
supportsGlossary: true,
|
|
1358
|
+
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function createMechanism4(client, config) {
|
|
1362
|
+
return {
|
|
1363
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1364
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
|
|
1365
|
+
const completion = await callClient5(client, body, signal);
|
|
1366
|
+
return extractOpenAiResult(completion, true);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function callClient5(client, body, signal) {
|
|
1371
|
+
return guardProviderCall(
|
|
1372
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1373
|
+
signal
|
|
1374
|
+
);
|
|
1022
1375
|
}
|
|
1023
1376
|
var SCAFFOLD_MODELS = {
|
|
1024
1377
|
anthropic: "claude-sonnet-4-6",
|
|
@@ -1029,13 +1382,18 @@ var providerConfigSchema = zod.z.discriminatedUnion("id", [
|
|
|
1029
1382
|
zod.z.object({ id: zod.z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
|
|
1030
1383
|
zod.z.object({ id: zod.z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
1031
1384
|
zod.z.object({ id: zod.z.literal("gemini"), options: geminiConfigSchema.strict() }),
|
|
1032
|
-
zod.z.object({ id: zod.z.literal("deepl"), options: deepLConfigSchema.strict() })
|
|
1385
|
+
zod.z.object({ id: zod.z.literal("deepl"), options: deepLConfigSchema.strict() }),
|
|
1386
|
+
zod.z.object({
|
|
1387
|
+
id: zod.z.literal("openai-compatible"),
|
|
1388
|
+
options: openAiCompatibleConfigSchema.strict()
|
|
1389
|
+
})
|
|
1033
1390
|
]);
|
|
1034
1391
|
var providerFactories = {
|
|
1035
1392
|
anthropic: (options) => createAnthropicProvider(options),
|
|
1036
1393
|
openai: (options) => createOpenAiProvider(options),
|
|
1037
1394
|
gemini: (options) => createGeminiProvider(options),
|
|
1038
|
-
deepl: (options) => createDeepLProvider(options)
|
|
1395
|
+
deepl: (options) => createDeepLProvider(options),
|
|
1396
|
+
"openai-compatible": (options) => createOpenAiCompatibleProvider(options)
|
|
1039
1397
|
};
|
|
1040
1398
|
function buildProvider(config) {
|
|
1041
1399
|
const create = providerFactories[config.id];
|
|
@@ -1214,6 +1572,12 @@ async function readLockFile(path, fs) {
|
|
|
1214
1572
|
if (!result.success) {
|
|
1215
1573
|
throw new SdkError("LOCK_FILE_INVALID", `The lock-file at ${path} has an unexpected shape.`);
|
|
1216
1574
|
}
|
|
1575
|
+
if (result.data.version !== CURRENT_VERSION) {
|
|
1576
|
+
throw new SdkError(
|
|
1577
|
+
"LOCK_FILE_INVALID",
|
|
1578
|
+
`The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION}.`
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1217
1581
|
return result.data;
|
|
1218
1582
|
}
|
|
1219
1583
|
function baselineFor(lock, locale) {
|
|
@@ -1346,6 +1710,150 @@ function icuInvalidKeys(entries) {
|
|
|
1346
1710
|
}
|
|
1347
1711
|
return invalid;
|
|
1348
1712
|
}
|
|
1713
|
+
function tokenOf2(element) {
|
|
1714
|
+
switch (element.type) {
|
|
1715
|
+
case icuMessageformatParser.TYPE.argument:
|
|
1716
|
+
case icuMessageformatParser.TYPE.number:
|
|
1717
|
+
case icuMessageformatParser.TYPE.date:
|
|
1718
|
+
case icuMessageformatParser.TYPE.time:
|
|
1719
|
+
case icuMessageformatParser.TYPE.select:
|
|
1720
|
+
case icuMessageformatParser.TYPE.plural:
|
|
1721
|
+
return `{${element.value}}`;
|
|
1722
|
+
case icuMessageformatParser.TYPE.tag:
|
|
1723
|
+
return `<${element.value}>`;
|
|
1724
|
+
default:
|
|
1725
|
+
return void 0;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
function isBranching(element) {
|
|
1729
|
+
return element.type === icuMessageformatParser.TYPE.plural || element.type === icuMessageformatParser.TYPE.select;
|
|
1730
|
+
}
|
|
1731
|
+
function isTag(element) {
|
|
1732
|
+
return element.type === icuMessageformatParser.TYPE.tag;
|
|
1733
|
+
}
|
|
1734
|
+
function layerTokens(elements) {
|
|
1735
|
+
const tokens = [];
|
|
1736
|
+
for (const element of elements) {
|
|
1737
|
+
const token = tokenOf2(element);
|
|
1738
|
+
if (token !== void 0) {
|
|
1739
|
+
tokens.push(token);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return tokens;
|
|
1743
|
+
}
|
|
1744
|
+
function deepTokens(elements) {
|
|
1745
|
+
const tokens = [];
|
|
1746
|
+
for (const element of elements) {
|
|
1747
|
+
const token = tokenOf2(element);
|
|
1748
|
+
if (token !== void 0) {
|
|
1749
|
+
tokens.push(token);
|
|
1750
|
+
}
|
|
1751
|
+
if (isTag(element)) {
|
|
1752
|
+
tokens.push(...deepTokens(element.children));
|
|
1753
|
+
} else if (isBranching(element)) {
|
|
1754
|
+
for (const option of Object.values(element.options)) {
|
|
1755
|
+
tokens.push(...deepTokens(option.value));
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return tokens;
|
|
1760
|
+
}
|
|
1761
|
+
function branchesByCategory(element) {
|
|
1762
|
+
return new Map(
|
|
1763
|
+
Object.entries(element.options).map(([category, option]) => [category, option.value])
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
function findUnconsumed(target, consumed, isMatch) {
|
|
1767
|
+
for (let index = 0; index < target.length; index += 1) {
|
|
1768
|
+
if (consumed.has(index)) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
const candidate = target[index];
|
|
1772
|
+
if (candidate !== void 0 && isMatch(candidate)) {
|
|
1773
|
+
return { element: candidate, index };
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return void 0;
|
|
1777
|
+
}
|
|
1778
|
+
function findMatchingBranching(source, target, consumed) {
|
|
1779
|
+
return findUnconsumed(
|
|
1780
|
+
target,
|
|
1781
|
+
consumed,
|
|
1782
|
+
(candidate) => isBranching(candidate) && candidate.value === source.value
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
function findMatchingTag(source, target, consumed) {
|
|
1786
|
+
return findUnconsumed(
|
|
1787
|
+
target,
|
|
1788
|
+
consumed,
|
|
1789
|
+
(candidate) => isTag(candidate) && candidate.value === source.value
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
function combineResults(results) {
|
|
1793
|
+
const missing = results.flatMap((result) => result.missing).sort();
|
|
1794
|
+
const extra = results.flatMap((result) => result.extra).sort();
|
|
1795
|
+
return { matches: missing.length === 0 && extra.length === 0, missing, extra, reordered: false };
|
|
1796
|
+
}
|
|
1797
|
+
function compareAgainstSourceUnion(sourceBranches, targetBranch) {
|
|
1798
|
+
const union = /* @__PURE__ */ new Set();
|
|
1799
|
+
for (const branch of sourceBranches.values()) {
|
|
1800
|
+
for (const token of deepTokens(branch)) {
|
|
1801
|
+
union.add(token);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const extra = deepTokens(targetBranch).filter((token) => !union.has(token)).sort();
|
|
1805
|
+
return { matches: extra.length === 0, missing: [], extra, reordered: false };
|
|
1806
|
+
}
|
|
1807
|
+
function compareBranching(source, target) {
|
|
1808
|
+
const sourceBranches = branchesByCategory(source);
|
|
1809
|
+
const targetBranches = branchesByCategory(target);
|
|
1810
|
+
const categories = /* @__PURE__ */ new Set([...sourceBranches.keys(), ...targetBranches.keys()]);
|
|
1811
|
+
const results = [];
|
|
1812
|
+
for (const category of categories) {
|
|
1813
|
+
const sourceBranch = sourceBranches.get(category);
|
|
1814
|
+
const targetBranch = targetBranches.get(category);
|
|
1815
|
+
if (sourceBranch !== void 0 && targetBranch !== void 0) {
|
|
1816
|
+
results.push(compareElements(sourceBranch, targetBranch));
|
|
1817
|
+
} else if (targetBranch !== void 0) {
|
|
1818
|
+
results.push(compareAgainstSourceUnion(sourceBranches, targetBranch));
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return combineResults(results);
|
|
1822
|
+
}
|
|
1823
|
+
function nestedResults(source, target) {
|
|
1824
|
+
const results = [];
|
|
1825
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1826
|
+
for (const element of source) {
|
|
1827
|
+
if (isBranching(element)) {
|
|
1828
|
+
const found = findMatchingBranching(element, target, consumed);
|
|
1829
|
+
if (found !== void 0) {
|
|
1830
|
+
consumed.add(found.index);
|
|
1831
|
+
results.push(compareBranching(element, found.element));
|
|
1832
|
+
}
|
|
1833
|
+
} else if (isTag(element)) {
|
|
1834
|
+
const found = findMatchingTag(element, target, consumed);
|
|
1835
|
+
if (found !== void 0) {
|
|
1836
|
+
consumed.add(found.index);
|
|
1837
|
+
results.push(compareElements(element.children, found.element.children));
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
return results;
|
|
1842
|
+
}
|
|
1843
|
+
function compareElements(source, target) {
|
|
1844
|
+
const layer = checkPlaceholders(layerTokens(source), layerTokens(target));
|
|
1845
|
+
const nested = nestedResults(source, target);
|
|
1846
|
+
return nested.length === 0 ? layer : combineResults([layer, ...nested]);
|
|
1847
|
+
}
|
|
1848
|
+
function compareIcuPlaceholders(sourceValue, targetValue) {
|
|
1849
|
+
try {
|
|
1850
|
+
const sourceAst = icuMessageformatParser.parse(sourceValue);
|
|
1851
|
+
const targetAst = icuMessageformatParser.parse(targetValue);
|
|
1852
|
+
return compareElements(sourceAst, targetAst);
|
|
1853
|
+
} catch {
|
|
1854
|
+
return checkPlaceholders(icuPlaceholders(sourceValue), icuPlaceholders(targetValue));
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1349
1857
|
var AdapterError = class extends Error {
|
|
1350
1858
|
code;
|
|
1351
1859
|
constructor(code, message) {
|
|
@@ -1454,6 +1962,9 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1454
1962
|
throw error;
|
|
1455
1963
|
}
|
|
1456
1964
|
}
|
|
1965
|
+
function stripLeadingBom(content) {
|
|
1966
|
+
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
1967
|
+
}
|
|
1457
1968
|
async function readBoundedUtf82(handle, size) {
|
|
1458
1969
|
const buffer = Buffer.allocUnsafe(size);
|
|
1459
1970
|
let offset = 0;
|
|
@@ -1464,7 +1975,7 @@ async function readBoundedUtf82(handle, size) {
|
|
|
1464
1975
|
}
|
|
1465
1976
|
offset += bytesRead;
|
|
1466
1977
|
}
|
|
1467
|
-
return buffer.toString("utf8", 0, offset);
|
|
1978
|
+
return stripLeadingBom(buffer.toString("utf8", 0, offset));
|
|
1468
1979
|
}
|
|
1469
1980
|
async function readBounded2(filePath) {
|
|
1470
1981
|
const handle = await promises.open(filePath, "r");
|
|
@@ -1647,9 +2158,9 @@ function unflattenEntries(entries) {
|
|
|
1647
2158
|
}
|
|
1648
2159
|
return root;
|
|
1649
2160
|
}
|
|
1650
|
-
function toEntries(content, namespace,
|
|
2161
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree) {
|
|
1651
2162
|
try {
|
|
1652
|
-
const tree =
|
|
2163
|
+
const tree = parse3(content);
|
|
1653
2164
|
validateTree?.(tree);
|
|
1654
2165
|
return flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
1655
2166
|
} catch (error) {
|
|
@@ -1661,7 +2172,7 @@ function createTreeFileAdapter(options) {
|
|
|
1661
2172
|
format,
|
|
1662
2173
|
extensions,
|
|
1663
2174
|
sniff,
|
|
1664
|
-
parse:
|
|
2175
|
+
parse: parse3,
|
|
1665
2176
|
serialize,
|
|
1666
2177
|
deriveEntry,
|
|
1667
2178
|
extractPlaceholders,
|
|
@@ -1669,6 +2180,7 @@ function createTreeFileAdapter(options) {
|
|
|
1669
2180
|
validateMessage,
|
|
1670
2181
|
validateTree,
|
|
1671
2182
|
buildWriteTree,
|
|
2183
|
+
comparePlaceholders,
|
|
1672
2184
|
keyMode = "literal-leaf"
|
|
1673
2185
|
} = options;
|
|
1674
2186
|
return {
|
|
@@ -1676,10 +2188,11 @@ function createTreeFileAdapter(options) {
|
|
|
1676
2188
|
canHandle: buildCanHandle(extensions, sniff),
|
|
1677
2189
|
extractPlaceholders,
|
|
1678
2190
|
validateMessage: validateMessage ?? (() => true),
|
|
2191
|
+
...comparePlaceholders !== void 0 ? { comparePlaceholders } : {},
|
|
1679
2192
|
async read(filePath, locale) {
|
|
1680
2193
|
const content = await readFileContent(filePath);
|
|
1681
2194
|
const namespace = namespaceOf(filePath);
|
|
1682
|
-
const entries = toEntries(content, namespace,
|
|
2195
|
+
const entries = toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree);
|
|
1683
2196
|
const resource = { locale, namespace, format, entries };
|
|
1684
2197
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1685
2198
|
return { resource, invalidIcuKeys };
|
|
@@ -1775,6 +2288,7 @@ function createArbAdapter() {
|
|
|
1775
2288
|
deriveEntry: icuDeriveEntry,
|
|
1776
2289
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1777
2290
|
validateMessage: icuIsValid,
|
|
2291
|
+
comparePlaceholders: compareIcuPlaceholders,
|
|
1778
2292
|
buildWriteTree: buildArbWriteTree
|
|
1779
2293
|
});
|
|
1780
2294
|
}
|
|
@@ -1838,7 +2352,8 @@ function createNextIntlJsonAdapter() {
|
|
|
1838
2352
|
extractPlaceholders: icuPlaceholders,
|
|
1839
2353
|
deriveEntry: icuDeriveEntry,
|
|
1840
2354
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1841
|
-
validateMessage: icuIsValid
|
|
2355
|
+
validateMessage: icuIsValid,
|
|
2356
|
+
comparePlaceholders: compareIcuPlaceholders
|
|
1842
2357
|
});
|
|
1843
2358
|
}
|
|
1844
2359
|
function assertNoDottedNestedKey(tree) {
|
|
@@ -2040,6 +2555,7 @@ function extractXliffPlaceholders(value) {
|
|
|
2040
2555
|
return result;
|
|
2041
2556
|
}
|
|
2042
2557
|
var ELEMENT_NODE = 1;
|
|
2558
|
+
var TEXT_NODE = 3;
|
|
2043
2559
|
function isElement(node) {
|
|
2044
2560
|
return node.nodeType === ELEMENT_NODE;
|
|
2045
2561
|
}
|
|
@@ -2130,6 +2646,12 @@ function parseXliffEntries(content, namespace) {
|
|
|
2130
2646
|
const serializer = new xmldom.XMLSerializer();
|
|
2131
2647
|
const out = /* @__PURE__ */ new Map();
|
|
2132
2648
|
for (const unit of walkUnits(root)) {
|
|
2649
|
+
if (out.has(unit.key)) {
|
|
2650
|
+
throw new AdapterError(
|
|
2651
|
+
"INVALID_STRUCTURE",
|
|
2652
|
+
"The XLIFF file has two trans-units with the same id."
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2133
2655
|
const value = unitValue(serializer, unit);
|
|
2134
2656
|
out.set(unit.key, {
|
|
2135
2657
|
key: unit.key,
|
|
@@ -2156,10 +2678,67 @@ async function readDestination(filePath) {
|
|
|
2156
2678
|
}
|
|
2157
2679
|
return outcome.content;
|
|
2158
2680
|
}
|
|
2681
|
+
var XLIFF_INLINE_ELEMENTS = /* @__PURE__ */ new Set(["x", "g", "bx", "ex", "ph", "it", "mrk"]);
|
|
2682
|
+
var XLIFF_NAMESPACES = /* @__PURE__ */ new Set([
|
|
2683
|
+
"urn:oasis:names:tc:xliff:document:1.2",
|
|
2684
|
+
"urn:oasis:names:tc:xliff:document:2.0"
|
|
2685
|
+
]);
|
|
2686
|
+
var INLINE_ELEMENT_ATTRIBUTES = {
|
|
2687
|
+
x: ["id", "ctype"],
|
|
2688
|
+
ph: ["id", "ctype"],
|
|
2689
|
+
g: ["id", "ctype"],
|
|
2690
|
+
bx: ["id", "rid", "ctype"],
|
|
2691
|
+
ex: ["id", "rid"],
|
|
2692
|
+
it: ["id", "pos", "ctype"],
|
|
2693
|
+
mrk: ["id", "mtype"]
|
|
2694
|
+
};
|
|
2695
|
+
function isAllowedInlineElement(element) {
|
|
2696
|
+
const namespace = element.namespaceURI;
|
|
2697
|
+
const hasAllowedNamespace = namespace === null || XLIFF_NAMESPACES.has(namespace);
|
|
2698
|
+
return hasAllowedNamespace && XLIFF_INLINE_ELEMENTS.has(element.localName ?? "");
|
|
2699
|
+
}
|
|
2700
|
+
function isAllowedFragmentNode(node) {
|
|
2701
|
+
if (node.nodeType === TEXT_NODE) {
|
|
2702
|
+
return true;
|
|
2703
|
+
}
|
|
2704
|
+
return isElement(node) && isAllowedInlineElement(node);
|
|
2705
|
+
}
|
|
2706
|
+
function allDescendantNodes(node) {
|
|
2707
|
+
const children = Array.from(node.childNodes);
|
|
2708
|
+
return children.flatMap((child) => [child, ...allDescendantNodes(child)]);
|
|
2709
|
+
}
|
|
2710
|
+
function hasDisallowedNode(root) {
|
|
2711
|
+
return allDescendantNodes(root).some((node) => !isAllowedFragmentNode(node));
|
|
2712
|
+
}
|
|
2713
|
+
function attributeNames(element) {
|
|
2714
|
+
const names = [];
|
|
2715
|
+
for (let i = 0; i < element.attributes.length; i += 1) {
|
|
2716
|
+
const attr = element.attributes.item(i);
|
|
2717
|
+
if (attr !== null) {
|
|
2718
|
+
names.push(attr.name);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
return names;
|
|
2722
|
+
}
|
|
2723
|
+
function sanitizeInlineAttributes(root) {
|
|
2724
|
+
for (const el of collectByTag(root, "*")) {
|
|
2725
|
+
const allowed = INLINE_ELEMENT_ATTRIBUTES[el.localName ?? ""] ?? [];
|
|
2726
|
+
for (const name of attributeNames(el)) {
|
|
2727
|
+
if (!allowed.includes(name)) {
|
|
2728
|
+
el.removeAttribute(name);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2159
2733
|
function fragmentNodes(parser, value) {
|
|
2734
|
+
assertNoDoctype(value);
|
|
2160
2735
|
try {
|
|
2161
2736
|
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
2162
|
-
|
|
2737
|
+
if (root === null || hasDisallowedNode(root)) {
|
|
2738
|
+
return null;
|
|
2739
|
+
}
|
|
2740
|
+
sanitizeInlineAttributes(root);
|
|
2741
|
+
return Array.from(root.childNodes);
|
|
2163
2742
|
} catch {
|
|
2164
2743
|
return null;
|
|
2165
2744
|
}
|
|
@@ -2439,15 +3018,8 @@ function subBatchFailedNotice(count, error) {
|
|
|
2439
3018
|
}
|
|
2440
3019
|
|
|
2441
3020
|
// src/flow/notices.ts
|
|
2442
|
-
function isNotice(value) {
|
|
2443
|
-
return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
|
|
2444
|
-
}
|
|
2445
3021
|
function readNotices(result) {
|
|
2446
|
-
|
|
2447
|
-
if (!Array.isArray(candidate)) {
|
|
2448
|
-
return [];
|
|
2449
|
-
}
|
|
2450
|
-
return candidate.filter(isNotice);
|
|
3022
|
+
return result.notices ?? [];
|
|
2451
3023
|
}
|
|
2452
3024
|
|
|
2453
3025
|
// src/flow/plural-categories.ts
|
|
@@ -2613,7 +3185,8 @@ function buildRequest2(context, entries) {
|
|
|
2613
3185
|
entries,
|
|
2614
3186
|
extractPlaceholders: context.adapter.extractPlaceholders,
|
|
2615
3187
|
...context.glossary !== void 0 ? { glossary: context.glossary } : {},
|
|
2616
|
-
...context.tone !== void 0 ? { tone: context.tone } : {}
|
|
3188
|
+
...context.tone !== void 0 ? { tone: context.tone } : {},
|
|
3189
|
+
...context.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: context.adapter.comparePlaceholders } : {}
|
|
2617
3190
|
};
|
|
2618
3191
|
}
|
|
2619
3192
|
async function generatePluralForms(context) {
|
|
@@ -2650,20 +3223,26 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
2650
3223
|
return [subBatchFailedNotice(batch.length, error)];
|
|
2651
3224
|
}
|
|
2652
3225
|
for (const item of batch) {
|
|
2653
|
-
|
|
2654
|
-
const integrity = result.integrity.get(item.targetKey);
|
|
2655
|
-
if (value !== void 0 && integrity?.matches === true) {
|
|
2656
|
-
accepted.push({
|
|
2657
|
-
targetKey: item.targetKey,
|
|
2658
|
-
entry: { ...syntheticEntry(item), value },
|
|
2659
|
-
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
2660
|
-
});
|
|
2661
|
-
} else {
|
|
2662
|
-
withheld.push(item.targetKey);
|
|
2663
|
-
}
|
|
3226
|
+
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
2664
3227
|
}
|
|
2665
3228
|
return readNotices(result);
|
|
2666
3229
|
}
|
|
3230
|
+
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
3231
|
+
const value = result.values.get(item.targetKey);
|
|
3232
|
+
if (value === void 0) {
|
|
3233
|
+
providerFailures.push(item.targetKey);
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
if (result.integrity.get(item.targetKey)?.matches === true) {
|
|
3237
|
+
accepted.push({
|
|
3238
|
+
targetKey: item.targetKey,
|
|
3239
|
+
entry: { ...syntheticEntry(item), value },
|
|
3240
|
+
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
3241
|
+
});
|
|
3242
|
+
} else {
|
|
3243
|
+
withheld.push(item.targetKey);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
2667
3246
|
|
|
2668
3247
|
// src/flow/locale-run.ts
|
|
2669
3248
|
function emptyResource(locale, format) {
|
|
@@ -2683,7 +3262,8 @@ function buildRequest3(params, entries) {
|
|
|
2683
3262
|
entries,
|
|
2684
3263
|
extractPlaceholders: params.adapter.extractPlaceholders,
|
|
2685
3264
|
...params.glossary !== void 0 ? { glossary: params.glossary } : {},
|
|
2686
|
-
...params.tone !== void 0 ? { tone: params.tone } : {}
|
|
3265
|
+
...params.tone !== void 0 ? { tone: params.tone } : {},
|
|
3266
|
+
...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
|
|
2687
3267
|
};
|
|
2688
3268
|
}
|
|
2689
3269
|
async function runLocale(params) {
|
|
@@ -2850,16 +3430,22 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
2850
3430
|
return [subBatchFailedNotice(batch.length, error)];
|
|
2851
3431
|
}
|
|
2852
3432
|
for (const entry of batch) {
|
|
2853
|
-
|
|
2854
|
-
const integrity = result.integrity.get(entry.key);
|
|
2855
|
-
if (value !== void 0 && integrity?.matches === true) {
|
|
2856
|
-
accepted.set(entry.key, { value, source: entry });
|
|
2857
|
-
} else {
|
|
2858
|
-
integrityMismatches.push(entry.key);
|
|
2859
|
-
}
|
|
3433
|
+
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
2860
3434
|
}
|
|
2861
3435
|
return readNotices(result);
|
|
2862
3436
|
}
|
|
3437
|
+
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
3438
|
+
const value = result.values.get(entry.key);
|
|
3439
|
+
if (value === void 0) {
|
|
3440
|
+
providerFailures.push(entry.key);
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
if (result.integrity.get(entry.key)?.matches === true) {
|
|
3444
|
+
accepted.set(entry.key, { value, source: entry });
|
|
3445
|
+
} else {
|
|
3446
|
+
integrityMismatches.push(entry.key);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
2863
3449
|
function computeLockEntries(params, merged, withheld, generated) {
|
|
2864
3450
|
const lockEntries = {};
|
|
2865
3451
|
const sourceBaseKeys = sourcePluralBaseKeys(params.source);
|
|
@@ -3146,7 +3732,7 @@ async function streamEntryBounded(file, remaining) {
|
|
|
3146
3732
|
"The workbook decompresses to more than the permitted maximum bytes."
|
|
3147
3733
|
);
|
|
3148
3734
|
}
|
|
3149
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
3735
|
+
return { raw, content: Buffer.concat(chunks).toString("utf8") };
|
|
3150
3736
|
}
|
|
3151
3737
|
async function guardWorkbookBytes(bytes, limits) {
|
|
3152
3738
|
let zip;
|
|
@@ -3178,14 +3764,8 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
3178
3764
|
let actualTotal = 0;
|
|
3179
3765
|
for (const file of files) {
|
|
3180
3766
|
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
|
-
}
|
|
3767
|
+
const { raw, content } = await streamEntryBounded(file, remaining);
|
|
3768
|
+
actualTotal += raw;
|
|
3189
3769
|
assertNoDoctype2(file.name, content);
|
|
3190
3770
|
}
|
|
3191
3771
|
}
|
|
@@ -3374,10 +3954,7 @@ function judge(row, sourceEntry, adapter) {
|
|
|
3374
3954
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
3375
3955
|
return "drift";
|
|
3376
3956
|
}
|
|
3377
|
-
const integrity = checkPlaceholders(
|
|
3378
|
-
sourceEntry.placeholders,
|
|
3379
|
-
adapter.extractPlaceholders(row.translation)
|
|
3380
|
-
);
|
|
3957
|
+
const integrity = adapter.comparePlaceholders !== void 0 ? adapter.comparePlaceholders(sourceEntry.value, row.translation) : checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(row.translation));
|
|
3381
3958
|
if (!integrity.matches) {
|
|
3382
3959
|
return "placeholder";
|
|
3383
3960
|
}
|