@verbatra/sdk 0.5.0-next.2 → 0.5.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +655 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -15
- package/dist/index.d.ts +112 -15
- package/dist/index.js +652 -66
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -472,6 +472,76 @@ function validateRequest(request) {
|
|
|
472
472
|
}
|
|
473
473
|
return parsed.data;
|
|
474
474
|
}
|
|
475
|
+
var LENGTH_RATIO_MIN = 0.35;
|
|
476
|
+
var LENGTH_RATIO_MAX = 3;
|
|
477
|
+
var LENGTH_RATIO_MIN_SOURCE_LENGTH = 12;
|
|
478
|
+
var UNICODE_LETTER = /\p{L}/u;
|
|
479
|
+
var DEGRADATION_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
480
|
+
"FORMALITY_DOWNGRADED",
|
|
481
|
+
"GLOSSARY_IGNORED"
|
|
482
|
+
]);
|
|
483
|
+
function isLengthRatioOutlier(sourceValue, translatedValue) {
|
|
484
|
+
const trimmedSource = sourceValue.trim();
|
|
485
|
+
if (trimmedSource.length < LENGTH_RATIO_MIN_SOURCE_LENGTH) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
const ratio = translatedValue.trim().length / trimmedSource.length;
|
|
489
|
+
return ratio < LENGTH_RATIO_MIN || ratio > LENGTH_RATIO_MAX;
|
|
490
|
+
}
|
|
491
|
+
function isEqualsSource(input) {
|
|
492
|
+
const trimmedSource = input.sourceValue.trim();
|
|
493
|
+
const trimmedTranslated = input.translatedValue.trim();
|
|
494
|
+
return trimmedTranslated === trimmedSource && input.targetLocale !== input.sourceLocale && UNICODE_LETTER.test(trimmedSource);
|
|
495
|
+
}
|
|
496
|
+
function isGlossaryTermMissed(input) {
|
|
497
|
+
const glossary = input.glossary;
|
|
498
|
+
if (glossary === void 0 || Object.keys(glossary).length === 0) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const sourceLower = input.sourceValue.toLowerCase();
|
|
502
|
+
const translatedLower = input.translatedValue.toLowerCase();
|
|
503
|
+
for (const [sourceTerm, targetTerm] of Object.entries(glossary)) {
|
|
504
|
+
const sourceHit = sourceLower.includes(sourceTerm.toLowerCase());
|
|
505
|
+
const targetHit = translatedLower.includes(targetTerm.toLowerCase());
|
|
506
|
+
if (sourceHit && !targetHit) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
function isIntegrityReordered(integrity) {
|
|
513
|
+
return integrity.matches && integrity.reordered;
|
|
514
|
+
}
|
|
515
|
+
function computeReviewFlags(input) {
|
|
516
|
+
const reasons = [];
|
|
517
|
+
if (isLengthRatioOutlier(input.sourceValue, input.translatedValue)) {
|
|
518
|
+
reasons.push("LENGTH_RATIO_OUTLIER");
|
|
519
|
+
}
|
|
520
|
+
if (isEqualsSource(input)) {
|
|
521
|
+
reasons.push("EQUALS_SOURCE");
|
|
522
|
+
}
|
|
523
|
+
if (isGlossaryTermMissed(input)) {
|
|
524
|
+
reasons.push("GLOSSARY_TERM_MISSED");
|
|
525
|
+
}
|
|
526
|
+
if (isIntegrityReordered(input.integrity)) {
|
|
527
|
+
reasons.push("INTEGRITY_REORDERED");
|
|
528
|
+
}
|
|
529
|
+
return reasons.length > 0 ? { status: "review", reasons } : void 0;
|
|
530
|
+
}
|
|
531
|
+
function applyProviderDegraded(reviewFlags, notices, acceptedKeys) {
|
|
532
|
+
if (!notices.some((notice) => DEGRADATION_NOTICE_CODES.has(notice.code))) {
|
|
533
|
+
return reviewFlags;
|
|
534
|
+
}
|
|
535
|
+
const next = new Map(reviewFlags);
|
|
536
|
+
for (const key of acceptedKeys) {
|
|
537
|
+
const existing = next.get(key);
|
|
538
|
+
next.set(key, {
|
|
539
|
+
status: "review",
|
|
540
|
+
reasons: existing !== void 0 ? [...existing.reasons, "PROVIDER_DEGRADED"] : ["PROVIDER_DEGRADED"]
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return next;
|
|
544
|
+
}
|
|
475
545
|
function toIntegrityInputs(entries, values) {
|
|
476
546
|
const inputs = [];
|
|
477
547
|
for (const entry of entries) {
|
|
@@ -567,7 +637,33 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
567
637
|
request.extractPlaceholders,
|
|
568
638
|
request.comparePlaceholders
|
|
569
639
|
);
|
|
570
|
-
|
|
640
|
+
const notices = [];
|
|
641
|
+
const reviewFlags = applyProviderDegraded(buildReviewFlags(data, values, integrity), notices, [
|
|
642
|
+
...values.keys()
|
|
643
|
+
]);
|
|
644
|
+
return usage === void 0 ? { values, integrity, notices, reviewFlags } : { values, integrity, usage, notices, reviewFlags };
|
|
645
|
+
}
|
|
646
|
+
function buildReviewFlags(data, values, integrity) {
|
|
647
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
648
|
+
for (const entry of data.entries) {
|
|
649
|
+
const translatedValue = values.get(entry.key);
|
|
650
|
+
const entryIntegrity = integrity.get(entry.key);
|
|
651
|
+
if (translatedValue === void 0 || entryIntegrity === void 0) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const flag = computeReviewFlags({
|
|
655
|
+
sourceValue: entry.value,
|
|
656
|
+
translatedValue,
|
|
657
|
+
sourceLocale: data.sourceLocale,
|
|
658
|
+
targetLocale: data.targetLocale,
|
|
659
|
+
integrity: entryIntegrity,
|
|
660
|
+
glossary: data.glossary
|
|
661
|
+
});
|
|
662
|
+
if (flag !== void 0) {
|
|
663
|
+
reviewFlags.set(entry.key, flag);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return reviewFlags;
|
|
571
667
|
}
|
|
572
668
|
async function requestTranslations(mechanism, data, signal) {
|
|
573
669
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
@@ -777,6 +873,50 @@ function createDefaultClient2() {
|
|
|
777
873
|
};
|
|
778
874
|
return { client, freeAccount };
|
|
779
875
|
}
|
|
876
|
+
var DEEPL_MAX_TEXTS_PER_REQUEST = 50;
|
|
877
|
+
var DEEPL_MAX_PAYLOAD_BYTES = 128 * 1024;
|
|
878
|
+
var DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES = 4 * 1024;
|
|
879
|
+
var DEEPL_MAX_TEXT_PAYLOAD_BYTES = DEEPL_MAX_PAYLOAD_BYTES - DEEPL_PAYLOAD_OVERHEAD_RESERVE_BYTES;
|
|
880
|
+
function estimateWireBytes(text) {
|
|
881
|
+
return encodeURIComponent(text).length;
|
|
882
|
+
}
|
|
883
|
+
function chunkTextsForDeepL(texts) {
|
|
884
|
+
const chunks = [];
|
|
885
|
+
let current = [];
|
|
886
|
+
let currentBytes = 0;
|
|
887
|
+
for (const text of texts) {
|
|
888
|
+
const textBytes = estimateWireBytes(text);
|
|
889
|
+
const startsNewChunk = current.length > 0 && (current.length >= DEEPL_MAX_TEXTS_PER_REQUEST || currentBytes + textBytes > DEEPL_MAX_TEXT_PAYLOAD_BYTES);
|
|
890
|
+
if (startsNewChunk) {
|
|
891
|
+
chunks.push(current);
|
|
892
|
+
current = [];
|
|
893
|
+
currentBytes = 0;
|
|
894
|
+
}
|
|
895
|
+
current.push(text);
|
|
896
|
+
currentBytes += textBytes;
|
|
897
|
+
}
|
|
898
|
+
if (current.length > 0) {
|
|
899
|
+
chunks.push(current);
|
|
900
|
+
}
|
|
901
|
+
return chunks;
|
|
902
|
+
}
|
|
903
|
+
var DEPRECATED_BARE_TARGET_CODES = /* @__PURE__ */ new Set(["en", "pt"]);
|
|
904
|
+
function assertValidDeepLSourceLocale(locale) {
|
|
905
|
+
if (locale.includes("-")) {
|
|
906
|
+
throw new ProviderError(
|
|
907
|
+
"INVALID_REQUEST",
|
|
908
|
+
`DeepL does not accept a regional or script source locale code: "${locale}". Only the base language code is valid as a DeepL source (for example, use "en" instead of "en-US").`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function assertValidDeepLTargetLocale(locale) {
|
|
913
|
+
if (DEPRECATED_BARE_TARGET_CODES.has(locale.toLowerCase())) {
|
|
914
|
+
throw new ProviderError(
|
|
915
|
+
"INVALID_REQUEST",
|
|
916
|
+
`DeepL requires a disambiguated target locale code instead of "${locale}" (for example, "en-GB" or "en-US" for English, "pt-PT" or "pt-BR" for Portuguese).`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
780
920
|
var PLACEHOLDER_UNSUPPORTED_MESSAGE = "Some entries contain placeholders or ICU syntax that DeepL cannot preserve; they were left untranslated. Use an LLM provider to translate placeholder-bearing strings.";
|
|
781
921
|
function partitionByPlaceholders(entries) {
|
|
782
922
|
const protectable = [];
|
|
@@ -856,6 +996,8 @@ function resolveClient(deps) {
|
|
|
856
996
|
}
|
|
857
997
|
async function translate(bundle, config, request) {
|
|
858
998
|
const data = validateRequest(request);
|
|
999
|
+
assertValidDeepLSourceLocale(data.sourceLocale);
|
|
1000
|
+
assertValidDeepLTargetLocale(data.targetLocale);
|
|
859
1001
|
const { protectable, unprotectable } = partitionByPlaceholders(data.entries);
|
|
860
1002
|
const genericGlossarySupplied = request.glossary !== void 0 && Object.keys(request.glossary).length > 0;
|
|
861
1003
|
const { options, notices } = buildTranslateOptions({
|
|
@@ -876,14 +1018,41 @@ async function translate(bundle, config, request) {
|
|
|
876
1018
|
if (unprotectable.length > 0) {
|
|
877
1019
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
878
1020
|
}
|
|
879
|
-
|
|
1021
|
+
const reviewFlags = applyProviderDegraded(
|
|
1022
|
+
buildReviewFlags2(protectable, data, values, integrity, request.glossary),
|
|
1023
|
+
notices,
|
|
1024
|
+
[...values.keys()]
|
|
1025
|
+
);
|
|
1026
|
+
return { values, integrity, notices, reviewFlags };
|
|
1027
|
+
}
|
|
1028
|
+
function buildReviewFlags2(protectable, data, values, integrity, glossary) {
|
|
1029
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
1030
|
+
for (const entry of protectable) {
|
|
1031
|
+
const translatedValue = values.get(entry.key);
|
|
1032
|
+
const entryIntegrity = integrity.get(entry.key);
|
|
1033
|
+
if (translatedValue === void 0 || entryIntegrity === void 0) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const flag = computeReviewFlags({
|
|
1037
|
+
sourceValue: entry.value,
|
|
1038
|
+
translatedValue,
|
|
1039
|
+
sourceLocale: data.sourceLocale,
|
|
1040
|
+
targetLocale: data.targetLocale,
|
|
1041
|
+
integrity: entryIntegrity,
|
|
1042
|
+
glossary
|
|
1043
|
+
});
|
|
1044
|
+
if (flag !== void 0) {
|
|
1045
|
+
reviewFlags.set(entry.key, flag);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return reviewFlags;
|
|
880
1049
|
}
|
|
881
1050
|
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
882
1051
|
if (protectable.length === 0) {
|
|
883
1052
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
884
1053
|
}
|
|
885
1054
|
const texts = protectable.map((entry) => entry.value);
|
|
886
|
-
const results = await
|
|
1055
|
+
const results = await callClientChunked(
|
|
887
1056
|
client,
|
|
888
1057
|
texts,
|
|
889
1058
|
data.sourceLocale,
|
|
@@ -901,6 +1070,14 @@ function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
|
901
1070
|
signal
|
|
902
1071
|
);
|
|
903
1072
|
}
|
|
1073
|
+
async function callClientChunked(client, texts, sourceLang, targetLang, options, signal) {
|
|
1074
|
+
const results = [];
|
|
1075
|
+
for (const chunk2 of chunkTextsForDeepL(texts)) {
|
|
1076
|
+
const chunkResults = await callClient2(client, chunk2, sourceLang, targetLang, options, signal);
|
|
1077
|
+
results.push(...chunkResults);
|
|
1078
|
+
}
|
|
1079
|
+
return results;
|
|
1080
|
+
}
|
|
904
1081
|
var geminiConfigSchema = z.object({
|
|
905
1082
|
model: z.string().min(1),
|
|
906
1083
|
maxOutputTokens: z.number().int().positive()
|
|
@@ -1372,6 +1549,18 @@ function buildProvider(config) {
|
|
|
1372
1549
|
|
|
1373
1550
|
// src/config/schema.ts
|
|
1374
1551
|
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1552
|
+
var DEFAULT_BUDGET_BEHAVIOR = "warn";
|
|
1553
|
+
function findCaseInsensitiveDuplicate(locales) {
|
|
1554
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1555
|
+
for (const locale of locales) {
|
|
1556
|
+
const key = locale.toLowerCase();
|
|
1557
|
+
if (seen.has(key)) {
|
|
1558
|
+
return locale;
|
|
1559
|
+
}
|
|
1560
|
+
seen.add(key);
|
|
1561
|
+
}
|
|
1562
|
+
return void 0;
|
|
1563
|
+
}
|
|
1375
1564
|
var verbatraConfigSchema = z.strictObject({
|
|
1376
1565
|
sourceLocale: z.string().min(1),
|
|
1377
1566
|
targetLocales: z.array(z.string().min(1)).min(1),
|
|
@@ -1409,10 +1598,34 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
1409
1598
|
* be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
|
|
1410
1599
|
* {@link DEFAULT_MAX_BATCH_SIZE} applies.
|
|
1411
1600
|
*/
|
|
1412
|
-
maxBatchSize: z.number().int().positive().optional()
|
|
1601
|
+
maxBatchSize: z.number().int().positive().optional(),
|
|
1602
|
+
/**
|
|
1603
|
+
* Optional whole-run token ceiling (input plus output tokens summed across every provider call, main
|
|
1604
|
+
* translation and plural generation alike, across all target locales). Checked after each completed
|
|
1605
|
+
* sub-batch, never mid-batch: the sub-batch whose completion crosses the ceiling is retained and
|
|
1606
|
+
* counted, since a call already in flight cannot be undone. Must be a positive integer. Config-only,
|
|
1607
|
+
* no CLI flag. Absent means no budget is enforced. Inert (never a false trip) against a token-less
|
|
1608
|
+
* provider such as DeepL, since it never reports usage to measure against the ceiling.
|
|
1609
|
+
*/
|
|
1610
|
+
maxTokens: z.number().int().positive().optional(),
|
|
1611
|
+
/**
|
|
1612
|
+
* What happens once `maxTokens` is reached: `"warn"` (default) flags it and lets the run continue
|
|
1613
|
+
* unchanged; `"stop"` withholds every not-yet-attempted key for the rest of the run (the current
|
|
1614
|
+
* locale's remaining candidates and every later locale's), retried automatically next run. Present
|
|
1615
|
+
* without `maxTokens` is accepted and has no effect. Never changes the command's exit code.
|
|
1616
|
+
*/
|
|
1617
|
+
budgetBehavior: z.enum(["warn", "stop"]).optional()
|
|
1413
1618
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
1414
1619
|
message: "targetLocales must not include the source locale",
|
|
1415
1620
|
path: ["targetLocales"]
|
|
1621
|
+
}).refine((config) => findCaseInsensitiveDuplicate(config.targetLocales) === void 0, {
|
|
1622
|
+
error: (issue) => {
|
|
1623
|
+
const duplicate = findCaseInsensitiveDuplicate(
|
|
1624
|
+
issue.input.targetLocales
|
|
1625
|
+
);
|
|
1626
|
+
return `targetLocales must not contain case-insensitively duplicate locales: "${duplicate}"`;
|
|
1627
|
+
},
|
|
1628
|
+
path: ["targetLocales"]
|
|
1416
1629
|
}).refine((config) => config.files.pattern.includes(LOCALE_TOKEN), {
|
|
1417
1630
|
message: `files.pattern must contain the ${LOCALE_TOKEN} token`,
|
|
1418
1631
|
path: ["files", "pattern"]
|
|
@@ -1835,7 +2048,7 @@ var AdapterError = class extends Error {
|
|
|
1835
2048
|
var MAX_DEPTH = 100;
|
|
1836
2049
|
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1837
2050
|
var jsonTreeSchema = z.lazy(
|
|
1838
|
-
() => z.union([z.string(), z.record(z.string(), jsonTreeSchema)])
|
|
2051
|
+
() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.string(), jsonTreeSchema)])
|
|
1839
2052
|
);
|
|
1840
2053
|
var rootSchema = z.record(z.string(), jsonTreeSchema);
|
|
1841
2054
|
function assertWithinDepth(value, max) {
|
|
@@ -1863,7 +2076,7 @@ function assertJsonRecord(value) {
|
|
|
1863
2076
|
if (!result.success) {
|
|
1864
2077
|
throw new AdapterError(
|
|
1865
2078
|
"INVALID_STRUCTURE",
|
|
1866
|
-
"The file is not a valid object (expected nested objects of string
|
|
2079
|
+
"The file is not a valid object (expected nested objects of string, number, boolean, or null leaves)."
|
|
1867
2080
|
);
|
|
1868
2081
|
}
|
|
1869
2082
|
return result.data;
|
|
@@ -1881,6 +2094,9 @@ function serializeJsonTree(tree) {
|
|
|
1881
2094
|
return `${JSON.stringify(tree, null, 2)}
|
|
1882
2095
|
`;
|
|
1883
2096
|
}
|
|
2097
|
+
function isJsonNode(value) {
|
|
2098
|
+
return typeof value === "object" && value !== null;
|
|
2099
|
+
}
|
|
1884
2100
|
function namespaceOf(filePath) {
|
|
1885
2101
|
return basename(filePath, extname(filePath));
|
|
1886
2102
|
}
|
|
@@ -1908,9 +2124,25 @@ function buildCanHandle(extensions, sniff) {
|
|
|
1908
2124
|
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
1909
2125
|
};
|
|
1910
2126
|
}
|
|
2127
|
+
async function fsyncPath(path) {
|
|
2128
|
+
const handle = await open(path, "r");
|
|
2129
|
+
try {
|
|
2130
|
+
await handle.sync();
|
|
2131
|
+
} finally {
|
|
2132
|
+
await handle.close();
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
async function fsyncDirBestEffort(path) {
|
|
2136
|
+
try {
|
|
2137
|
+
await fsyncPath(path);
|
|
2138
|
+
} catch {
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
1911
2141
|
var nodeOps = {
|
|
1912
2142
|
writeFile: (path, data) => writeFile(path, data, "utf8"),
|
|
2143
|
+
fsyncFile: (path) => fsyncPath(path),
|
|
1913
2144
|
rename: (from, to) => rename(from, to),
|
|
2145
|
+
fsyncDir: (path) => fsyncDirBestEffort(path),
|
|
1914
2146
|
rm: (path) => rm(path, { force: true })
|
|
1915
2147
|
};
|
|
1916
2148
|
async function cleanup(ops, tmp) {
|
|
@@ -1926,11 +2158,16 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1926
2158
|
const tmp = tempFileName2(path);
|
|
1927
2159
|
try {
|
|
1928
2160
|
await ops.writeFile(tmp, data);
|
|
2161
|
+
await ops.fsyncFile(tmp);
|
|
1929
2162
|
await ops.rename(tmp, path);
|
|
1930
2163
|
} catch (error) {
|
|
1931
2164
|
await cleanup(ops, tmp);
|
|
1932
2165
|
throw error;
|
|
1933
2166
|
}
|
|
2167
|
+
try {
|
|
2168
|
+
await ops.fsyncDir(dirname(path));
|
|
2169
|
+
} catch {
|
|
2170
|
+
}
|
|
1934
2171
|
}
|
|
1935
2172
|
function stripLeadingBom(content) {
|
|
1936
2173
|
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
@@ -2052,20 +2289,24 @@ function addLeaf(ctx, segments, key, value) {
|
|
|
2052
2289
|
);
|
|
2053
2290
|
}
|
|
2054
2291
|
ctx.claimed.set(effectivePath, mapKey);
|
|
2292
|
+
if (typeof value !== "string") {
|
|
2293
|
+
ctx.excluded.push(mapKey);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2055
2296
|
const { placeholders, isPlural } = ctx.derive(key, value);
|
|
2056
2297
|
ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
|
|
2057
2298
|
}
|
|
2058
2299
|
function addEntries(ctx, prefix, node) {
|
|
2059
2300
|
for (const [key, value] of Object.entries(node)) {
|
|
2060
2301
|
const segments = [...prefix, key];
|
|
2061
|
-
if (
|
|
2062
|
-
addLeaf(ctx, segments, key, value);
|
|
2063
|
-
} else {
|
|
2302
|
+
if (isJsonNode(value)) {
|
|
2064
2303
|
addEntries(ctx, segments, value);
|
|
2304
|
+
} else {
|
|
2305
|
+
addLeaf(ctx, segments, key, value);
|
|
2065
2306
|
}
|
|
2066
2307
|
}
|
|
2067
2308
|
}
|
|
2068
|
-
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
2309
|
+
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths, excluded) {
|
|
2069
2310
|
for (const [key, value] of Object.entries(node)) {
|
|
2070
2311
|
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
2071
2312
|
if (claimedPaths.has(path)) {
|
|
@@ -2075,22 +2316,26 @@ function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
|
2075
2316
|
);
|
|
2076
2317
|
}
|
|
2077
2318
|
claimedPaths.add(path);
|
|
2078
|
-
if (
|
|
2319
|
+
if (isJsonNode(value)) {
|
|
2320
|
+
addPathEntries(value, path, namespace, derive, out, claimedPaths, excluded);
|
|
2321
|
+
} else if (typeof value === "string") {
|
|
2079
2322
|
const { placeholders, isPlural } = derive(key, value);
|
|
2080
2323
|
out.set(path, { key: path, namespace, value, placeholders, isPlural });
|
|
2081
2324
|
} else {
|
|
2082
|
-
|
|
2325
|
+
excluded.push(path);
|
|
2083
2326
|
}
|
|
2084
2327
|
}
|
|
2085
2328
|
}
|
|
2086
2329
|
function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
|
|
2087
2330
|
const out = /* @__PURE__ */ new Map();
|
|
2088
2331
|
if (keyMode === "path-notation") {
|
|
2089
|
-
|
|
2090
|
-
|
|
2332
|
+
const excluded = [];
|
|
2333
|
+
addPathEntries(tree, "", namespace, derive, out, /* @__PURE__ */ new Set(), excluded);
|
|
2334
|
+
return { entries: out, excludedLeafPaths: excluded };
|
|
2091
2335
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2336
|
+
const ctx = { namespace, derive, out, claimed: /* @__PURE__ */ new Map(), excluded: [] };
|
|
2337
|
+
addEntries(ctx, [], tree);
|
|
2338
|
+
return { entries: out, excludedLeafPaths: ctx.excluded };
|
|
2094
2339
|
}
|
|
2095
2340
|
function emptyNode() {
|
|
2096
2341
|
return /* @__PURE__ */ Object.create(null);
|
|
@@ -2128,11 +2373,24 @@ function unflattenEntries(entries) {
|
|
|
2128
2373
|
}
|
|
2129
2374
|
return root;
|
|
2130
2375
|
}
|
|
2131
|
-
function
|
|
2376
|
+
function mergeDescriptions(entries, content, deriveDescriptions) {
|
|
2377
|
+
if (!deriveDescriptions) {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
for (const [key, description] of deriveDescriptions(content)) {
|
|
2381
|
+
const entry = entries.get(key);
|
|
2382
|
+
if (entry !== void 0) {
|
|
2383
|
+
entries.set(key, { ...entry, description });
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree, deriveDescriptions) {
|
|
2132
2388
|
try {
|
|
2133
2389
|
const tree = parse3(content);
|
|
2134
2390
|
validateTree?.(tree);
|
|
2135
|
-
|
|
2391
|
+
const result = flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
2392
|
+
mergeDescriptions(result.entries, content, deriveDescriptions);
|
|
2393
|
+
return result;
|
|
2136
2394
|
} catch (error) {
|
|
2137
2395
|
rethrowStructured(error, "The file could not be parsed.");
|
|
2138
2396
|
}
|
|
@@ -2151,6 +2409,7 @@ function createTreeFileAdapter(options) {
|
|
|
2151
2409
|
validateTree,
|
|
2152
2410
|
buildWriteTree,
|
|
2153
2411
|
comparePlaceholders,
|
|
2412
|
+
deriveDescriptions,
|
|
2154
2413
|
keyMode = "literal-leaf"
|
|
2155
2414
|
} = options;
|
|
2156
2415
|
return {
|
|
@@ -2162,10 +2421,18 @@ function createTreeFileAdapter(options) {
|
|
|
2162
2421
|
async read(filePath, locale) {
|
|
2163
2422
|
const content = await readFileContent(filePath);
|
|
2164
2423
|
const namespace = namespaceOf(filePath);
|
|
2165
|
-
const entries = toEntries(
|
|
2424
|
+
const { entries, excludedLeafPaths } = toEntries(
|
|
2425
|
+
content,
|
|
2426
|
+
namespace,
|
|
2427
|
+
parse3,
|
|
2428
|
+
deriveEntry,
|
|
2429
|
+
keyMode,
|
|
2430
|
+
validateTree,
|
|
2431
|
+
deriveDescriptions
|
|
2432
|
+
);
|
|
2166
2433
|
const resource = { locale, namespace, format, entries };
|
|
2167
2434
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2168
|
-
return { resource, invalidIcuKeys };
|
|
2435
|
+
return { resource, invalidIcuKeys, excludedLeafPaths };
|
|
2169
2436
|
},
|
|
2170
2437
|
async write(resource, filePath) {
|
|
2171
2438
|
const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
|
|
@@ -2203,6 +2470,31 @@ function stripArbMetadata(tree) {
|
|
|
2203
2470
|
function originalKey(encoded) {
|
|
2204
2471
|
return decodeKeyToSegments(encoded).join(".");
|
|
2205
2472
|
}
|
|
2473
|
+
function messageKeyForMetadata(key) {
|
|
2474
|
+
return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
|
|
2475
|
+
}
|
|
2476
|
+
function descriptionOf(value) {
|
|
2477
|
+
if (typeof value !== "object" || value === null) {
|
|
2478
|
+
return void 0;
|
|
2479
|
+
}
|
|
2480
|
+
const description = value.description;
|
|
2481
|
+
return typeof description === "string" ? description : void 0;
|
|
2482
|
+
}
|
|
2483
|
+
function extractArbDescriptions(content) {
|
|
2484
|
+
const tree = parseArbObject(content);
|
|
2485
|
+
const out = /* @__PURE__ */ new Map();
|
|
2486
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
2487
|
+
const messageKey = messageKeyForMetadata(key);
|
|
2488
|
+
if (messageKey === null) {
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
const description = descriptionOf(value);
|
|
2492
|
+
if (description !== void 0) {
|
|
2493
|
+
out.set(encodeSegment(messageKey), description);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
return out;
|
|
2497
|
+
}
|
|
2206
2498
|
function messagesFromEntries(entries) {
|
|
2207
2499
|
const out = /* @__PURE__ */ new Map();
|
|
2208
2500
|
for (const [key, entry] of entries) {
|
|
@@ -2234,8 +2526,10 @@ async function buildArbWriteTree(entries, filePath) {
|
|
|
2234
2526
|
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
2235
2527
|
if (translated !== void 0) {
|
|
2236
2528
|
consumed.add(key);
|
|
2529
|
+
out[key] = translated;
|
|
2530
|
+
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
2531
|
+
out[key] = value;
|
|
2237
2532
|
}
|
|
2238
|
-
out[key] = translated ?? value;
|
|
2239
2533
|
}
|
|
2240
2534
|
for (const [key, value] of messages) {
|
|
2241
2535
|
if (!consumed.has(key)) {
|
|
@@ -2259,7 +2553,8 @@ function createArbAdapter() {
|
|
|
2259
2553
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
2260
2554
|
validateMessage: icuIsValid,
|
|
2261
2555
|
comparePlaceholders: compareIcuPlaceholders,
|
|
2262
|
-
buildWriteTree: buildArbWriteTree
|
|
2556
|
+
buildWriteTree: buildArbWriteTree,
|
|
2557
|
+
deriveDescriptions: extractArbDescriptions
|
|
2263
2558
|
});
|
|
2264
2559
|
}
|
|
2265
2560
|
function createJsonFileAdapter(options) {
|
|
@@ -2328,7 +2623,7 @@ function createNextIntlJsonAdapter() {
|
|
|
2328
2623
|
}
|
|
2329
2624
|
function assertNoDottedNestedKey(tree) {
|
|
2330
2625
|
for (const [key, value] of Object.entries(tree)) {
|
|
2331
|
-
if (
|
|
2626
|
+
if (!isJsonNode(value)) {
|
|
2332
2627
|
continue;
|
|
2333
2628
|
}
|
|
2334
2629
|
if (key.includes(".")) {
|
|
@@ -2344,7 +2639,7 @@ function assertNotMixed(tree) {
|
|
|
2344
2639
|
let hasNested = false;
|
|
2345
2640
|
let hasFlatDottedKey = false;
|
|
2346
2641
|
for (const [key, value] of Object.entries(tree)) {
|
|
2347
|
-
if (
|
|
2642
|
+
if (isJsonNode(value)) {
|
|
2348
2643
|
hasNested = true;
|
|
2349
2644
|
} else if (key.includes(".")) {
|
|
2350
2645
|
hasFlatDottedKey = true;
|
|
@@ -2505,7 +2800,7 @@ function createFlatFileAdapter(options) {
|
|
|
2505
2800
|
const entries = toEntries2(content, namespace, parseEntries);
|
|
2506
2801
|
const resource = { locale, namespace, format, entries };
|
|
2507
2802
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2508
|
-
return { resource, invalidIcuKeys };
|
|
2803
|
+
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
2509
2804
|
},
|
|
2510
2805
|
async write(resource, filePath) {
|
|
2511
2806
|
const data = await serializeEntries(resource.entries, filePath);
|
|
@@ -2565,16 +2860,33 @@ function parseXml(content) {
|
|
|
2565
2860
|
}
|
|
2566
2861
|
return { doc, root };
|
|
2567
2862
|
}
|
|
2863
|
+
function firstNoteText(parent) {
|
|
2864
|
+
const note = childByName(parent, "note");
|
|
2865
|
+
if (note === null) {
|
|
2866
|
+
return void 0;
|
|
2867
|
+
}
|
|
2868
|
+
const text = (note.textContent ?? "").trim();
|
|
2869
|
+
return text === "" ? void 0 : text;
|
|
2870
|
+
}
|
|
2871
|
+
function transUnitDescription(tu) {
|
|
2872
|
+
return firstNoteText(tu);
|
|
2873
|
+
}
|
|
2874
|
+
function unitDescription(unit) {
|
|
2875
|
+
const notes = childByName(unit, "notes");
|
|
2876
|
+
return notes === null ? void 0 : firstNoteText(notes);
|
|
2877
|
+
}
|
|
2568
2878
|
function walkXliff12(root) {
|
|
2569
2879
|
const units = [];
|
|
2570
2880
|
collectByTag(root, "trans-unit").forEach((tu, index) => {
|
|
2571
2881
|
const source = childByName(tu, "source");
|
|
2572
2882
|
if (source !== null) {
|
|
2883
|
+
const description = transUnitDescription(tu);
|
|
2573
2884
|
units.push({
|
|
2574
2885
|
key: unitKey(tu, index),
|
|
2575
2886
|
source,
|
|
2576
2887
|
target: childByName(tu, "target"),
|
|
2577
|
-
container: tu
|
|
2888
|
+
container: tu,
|
|
2889
|
+
...description !== void 0 ? { description } : {}
|
|
2578
2890
|
});
|
|
2579
2891
|
}
|
|
2580
2892
|
});
|
|
@@ -2584,12 +2896,19 @@ function walkXliff20(root) {
|
|
|
2584
2896
|
const units = [];
|
|
2585
2897
|
collectByTag(root, "unit").forEach((unit, index) => {
|
|
2586
2898
|
const baseKey = unitKey(unit, index);
|
|
2899
|
+
const description = unitDescription(unit);
|
|
2587
2900
|
const segments = elementChildren(unit).filter((el) => el.localName === "segment");
|
|
2588
2901
|
segments.forEach((segment, segIndex) => {
|
|
2589
2902
|
const source = childByName(segment, "source");
|
|
2590
2903
|
if (source !== null) {
|
|
2591
2904
|
const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
|
|
2592
|
-
units.push({
|
|
2905
|
+
units.push({
|
|
2906
|
+
key,
|
|
2907
|
+
source,
|
|
2908
|
+
target: childByName(segment, "target"),
|
|
2909
|
+
container: segment,
|
|
2910
|
+
...description !== void 0 ? { description } : {}
|
|
2911
|
+
});
|
|
2593
2912
|
}
|
|
2594
2913
|
});
|
|
2595
2914
|
});
|
|
@@ -2628,7 +2947,8 @@ function parseXliffEntries(content, namespace) {
|
|
|
2628
2947
|
namespace,
|
|
2629
2948
|
value,
|
|
2630
2949
|
placeholders: extractXliffPlaceholders(value),
|
|
2631
|
-
isPlural: false
|
|
2950
|
+
isPlural: false,
|
|
2951
|
+
...unit.description !== void 0 ? { description: unit.description } : {}
|
|
2632
2952
|
});
|
|
2633
2953
|
}
|
|
2634
2954
|
return out;
|
|
@@ -2934,6 +3254,46 @@ function selectProvider(config, createProvider = buildProvider) {
|
|
|
2934
3254
|
}
|
|
2935
3255
|
}
|
|
2936
3256
|
|
|
3257
|
+
// src/flow/budget.ts
|
|
3258
|
+
function createBudgetTracker(maxTokens, behavior) {
|
|
3259
|
+
return { maxTokens, behavior, tokensUsed: 0, usageSeen: false, exceeded: false, stopped: false };
|
|
3260
|
+
}
|
|
3261
|
+
function foldTrackerUsage(tracker, usage) {
|
|
3262
|
+
if (usage === void 0) {
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
tracker.usageSeen = true;
|
|
3266
|
+
tracker.tokensUsed += usage.inputTokens + usage.outputTokens;
|
|
3267
|
+
}
|
|
3268
|
+
function checkBudgetTrip(tracker) {
|
|
3269
|
+
if (tracker.maxTokens === void 0 || tracker.exceeded || tracker.tokensUsed < tracker.maxTokens) {
|
|
3270
|
+
return false;
|
|
3271
|
+
}
|
|
3272
|
+
tracker.exceeded = true;
|
|
3273
|
+
if (tracker.behavior === "stop") {
|
|
3274
|
+
tracker.stopped = true;
|
|
3275
|
+
}
|
|
3276
|
+
return true;
|
|
3277
|
+
}
|
|
3278
|
+
function toBudgetSummary(tracker) {
|
|
3279
|
+
if (tracker.maxTokens === void 0) {
|
|
3280
|
+
return void 0;
|
|
3281
|
+
}
|
|
3282
|
+
return {
|
|
3283
|
+
maxTokens: tracker.maxTokens,
|
|
3284
|
+
behavior: tracker.behavior,
|
|
3285
|
+
supported: tracker.usageSeen,
|
|
3286
|
+
tokensUsed: tracker.tokensUsed,
|
|
3287
|
+
exceeded: tracker.exceeded
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
function budgetExceededNotice(tracker) {
|
|
3291
|
+
return {
|
|
3292
|
+
code: "BUDGET_TOKENS_EXCEEDED",
|
|
3293
|
+
message: `The run's cumulative token usage (${tracker.tokensUsed}) reached the configured budget of ${tracker.maxTokens} tokens (behavior: ${tracker.behavior}).`
|
|
3294
|
+
};
|
|
3295
|
+
}
|
|
3296
|
+
|
|
2937
3297
|
// src/flow/locale-failure.ts
|
|
2938
3298
|
function describeError(error) {
|
|
2939
3299
|
if (error instanceof Error) {
|
|
@@ -2953,8 +3313,10 @@ function failureSummary(locale, error) {
|
|
|
2953
3313
|
invalidIcuSource: [],
|
|
2954
3314
|
integrityMismatches: [],
|
|
2955
3315
|
providerFailures: [],
|
|
3316
|
+
budgetWithheld: [],
|
|
2956
3317
|
generated: [],
|
|
2957
3318
|
notices: [],
|
|
3319
|
+
needsReview: [],
|
|
2958
3320
|
error: describeError(error)
|
|
2959
3321
|
};
|
|
2960
3322
|
}
|
|
@@ -3124,7 +3486,43 @@ function planPluralGeneration(source, targetLocale, format) {
|
|
|
3124
3486
|
return { items };
|
|
3125
3487
|
}
|
|
3126
3488
|
|
|
3489
|
+
// src/flow/usage.ts
|
|
3490
|
+
function createUsageAccumulator() {
|
|
3491
|
+
return { total: void 0 };
|
|
3492
|
+
}
|
|
3493
|
+
function foldUsage(accumulator, usage) {
|
|
3494
|
+
if (usage === void 0) {
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
const prior = accumulator.total ?? { inputTokens: 0, outputTokens: 0 };
|
|
3498
|
+
accumulator.total = {
|
|
3499
|
+
inputTokens: prior.inputTokens + usage.inputTokens,
|
|
3500
|
+
outputTokens: prior.outputTokens + usage.outputTokens
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
function combineUsage(a, b) {
|
|
3504
|
+
if (a === void 0) {
|
|
3505
|
+
return b;
|
|
3506
|
+
}
|
|
3507
|
+
if (b === void 0) {
|
|
3508
|
+
return a;
|
|
3509
|
+
}
|
|
3510
|
+
return {
|
|
3511
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
3512
|
+
outputTokens: a.outputTokens + b.outputTokens
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3127
3516
|
// src/flow/plural-generation.ts
|
|
3517
|
+
var EMPTY_RESULT = {
|
|
3518
|
+
accepted: [],
|
|
3519
|
+
withheld: [],
|
|
3520
|
+
providerFailures: [],
|
|
3521
|
+
budgetWithheld: [],
|
|
3522
|
+
notices: [],
|
|
3523
|
+
usage: void 0,
|
|
3524
|
+
tripped: false
|
|
3525
|
+
};
|
|
3128
3526
|
function generatedLockHash(governingEntries, category) {
|
|
3129
3527
|
const governingHashes = governingEntries.map(contentHash).sort();
|
|
3130
3528
|
return contentHash({
|
|
@@ -3163,23 +3561,45 @@ async function generatePluralForms(context) {
|
|
|
3163
3561
|
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
3164
3562
|
const stale = staleItems(plan.items, context.baseline);
|
|
3165
3563
|
if (stale.length === 0) {
|
|
3166
|
-
return
|
|
3564
|
+
return EMPTY_RESULT;
|
|
3167
3565
|
}
|
|
3168
3566
|
const accepted = [];
|
|
3169
3567
|
const withheld = [];
|
|
3170
3568
|
const providerFailures = [];
|
|
3569
|
+
const budgetWithheld = [];
|
|
3171
3570
|
const notices = [];
|
|
3571
|
+
const usage = createUsageAccumulator();
|
|
3572
|
+
let tripped = false;
|
|
3172
3573
|
for (const batch of chunk(stale, context.maxBatchSize)) {
|
|
3173
|
-
|
|
3574
|
+
if (context.budget.stopped) {
|
|
3575
|
+
for (const item of batch) {
|
|
3576
|
+
budgetWithheld.push(item.targetKey);
|
|
3577
|
+
}
|
|
3578
|
+
continue;
|
|
3579
|
+
}
|
|
3580
|
+
const subResult = await runGenerationSubBatch(
|
|
3174
3581
|
context,
|
|
3175
3582
|
batch,
|
|
3176
3583
|
accepted,
|
|
3177
3584
|
withheld,
|
|
3178
3585
|
providerFailures
|
|
3179
3586
|
);
|
|
3180
|
-
notices.push(...
|
|
3587
|
+
notices.push(...subResult.notices);
|
|
3588
|
+
foldUsage(usage, subResult.usage);
|
|
3589
|
+
foldTrackerUsage(context.budget, subResult.usage);
|
|
3590
|
+
if (checkBudgetTrip(context.budget)) {
|
|
3591
|
+
tripped = true;
|
|
3592
|
+
}
|
|
3181
3593
|
}
|
|
3182
|
-
return {
|
|
3594
|
+
return {
|
|
3595
|
+
accepted,
|
|
3596
|
+
withheld,
|
|
3597
|
+
providerFailures,
|
|
3598
|
+
budgetWithheld,
|
|
3599
|
+
notices,
|
|
3600
|
+
usage: usage.total,
|
|
3601
|
+
tripped
|
|
3602
|
+
};
|
|
3183
3603
|
}
|
|
3184
3604
|
async function runGenerationSubBatch(context, batch, accepted, withheld, providerFailures) {
|
|
3185
3605
|
let result;
|
|
@@ -3190,12 +3610,12 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3190
3610
|
for (const item of batch) {
|
|
3191
3611
|
providerFailures.push(item.targetKey);
|
|
3192
3612
|
}
|
|
3193
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
3613
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3194
3614
|
}
|
|
3195
3615
|
for (const item of batch) {
|
|
3196
3616
|
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
3197
3617
|
}
|
|
3198
|
-
return readNotices(result);
|
|
3618
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3199
3619
|
}
|
|
3200
3620
|
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
3201
3621
|
const value = result.values.get(item.targetKey);
|
|
@@ -3263,6 +3683,7 @@ async function runLocale(params) {
|
|
|
3263
3683
|
generated: [],
|
|
3264
3684
|
integrityMismatches: [],
|
|
3265
3685
|
providerFailures: [],
|
|
3686
|
+
budgetWithheld: [],
|
|
3266
3687
|
pruned,
|
|
3267
3688
|
notices: sdkNotices
|
|
3268
3689
|
}),
|
|
@@ -3270,16 +3691,21 @@ async function runLocale(params) {
|
|
|
3270
3691
|
};
|
|
3271
3692
|
}
|
|
3272
3693
|
const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
|
|
3694
|
+
const startedStopped = params.budget.stopped;
|
|
3273
3695
|
const accepted = /* @__PURE__ */ new Map();
|
|
3274
3696
|
const integrityMismatches = [];
|
|
3275
3697
|
const providerFailures = [];
|
|
3276
|
-
const
|
|
3698
|
+
const budgetWithheld = [];
|
|
3699
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
3700
|
+
const translation = await translateAndCheck(
|
|
3277
3701
|
provider,
|
|
3278
3702
|
params,
|
|
3279
3703
|
entries,
|
|
3280
3704
|
accepted,
|
|
3281
3705
|
integrityMismatches,
|
|
3282
|
-
providerFailures
|
|
3706
|
+
providerFailures,
|
|
3707
|
+
budgetWithheld,
|
|
3708
|
+
reviewFlags
|
|
3283
3709
|
);
|
|
3284
3710
|
const merged = new Map(target.entries);
|
|
3285
3711
|
for (const key of pruned) {
|
|
@@ -3305,16 +3731,20 @@ async function runLocale(params) {
|
|
|
3305
3731
|
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
3306
3732
|
const notices = [
|
|
3307
3733
|
...pluralNotices,
|
|
3308
|
-
...
|
|
3309
|
-
...generation.notices
|
|
3734
|
+
...translation.notices,
|
|
3735
|
+
...generation.notices,
|
|
3736
|
+
...budgetLocaleNotices(params.budget, startedStopped, translation.tripped, generation.tripped)
|
|
3310
3737
|
];
|
|
3311
3738
|
const withheld = /* @__PURE__ */ new Set([
|
|
3312
3739
|
...integrityMismatches,
|
|
3313
3740
|
...providerFailures,
|
|
3314
3741
|
...invalidIcuSource,
|
|
3315
3742
|
...generation.withheld,
|
|
3316
|
-
...generation.providerFailures
|
|
3743
|
+
...generation.providerFailures,
|
|
3744
|
+
// Source-present keys withheld by the budget guardrail must keep their prior lock hash too.
|
|
3745
|
+
...budgetWithheld
|
|
3317
3746
|
]);
|
|
3747
|
+
const localeUsage = combineUsage(translation.usage, generation.usage);
|
|
3318
3748
|
return {
|
|
3319
3749
|
summary: baseSummary({
|
|
3320
3750
|
locale: params.targetLocale,
|
|
@@ -3327,15 +3757,27 @@ async function runLocale(params) {
|
|
|
3327
3757
|
integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
|
|
3328
3758
|
// A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
|
|
3329
3759
|
providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
|
|
3760
|
+
budgetWithheld: [...budgetWithheld, ...generation.budgetWithheld].sort(),
|
|
3330
3761
|
pruned,
|
|
3331
|
-
notices
|
|
3762
|
+
notices,
|
|
3763
|
+
needsReview: needsReviewFor(accepted.keys(), reviewFlags),
|
|
3764
|
+
...localeUsage !== void 0 ? { usage: localeUsage } : {}
|
|
3332
3765
|
}),
|
|
3333
3766
|
lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
|
|
3334
3767
|
};
|
|
3335
3768
|
}
|
|
3769
|
+
var NO_GENERATION_RESULT = {
|
|
3770
|
+
accepted: [],
|
|
3771
|
+
withheld: [],
|
|
3772
|
+
providerFailures: [],
|
|
3773
|
+
budgetWithheld: [],
|
|
3774
|
+
notices: [],
|
|
3775
|
+
usage: void 0,
|
|
3776
|
+
tripped: false
|
|
3777
|
+
};
|
|
3336
3778
|
async function runGeneration(params, provider) {
|
|
3337
3779
|
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
3338
|
-
return
|
|
3780
|
+
return NO_GENERATION_RESULT;
|
|
3339
3781
|
}
|
|
3340
3782
|
return generatePluralForms({
|
|
3341
3783
|
source: params.source,
|
|
@@ -3347,9 +3789,13 @@ async function runGeneration(params, provider) {
|
|
|
3347
3789
|
glossary: params.glossary,
|
|
3348
3790
|
tone: params.tone,
|
|
3349
3791
|
baseline: params.baseline,
|
|
3350
|
-
maxBatchSize: params.maxBatchSize
|
|
3792
|
+
maxBatchSize: params.maxBatchSize,
|
|
3793
|
+
budget: params.budget
|
|
3351
3794
|
});
|
|
3352
3795
|
}
|
|
3796
|
+
function budgetLocaleNotices(budget, startedStopped, mainTripped, generationTripped) {
|
|
3797
|
+
return startedStopped || mainTripped || generationTripped ? [budgetExceededNotice(budget)] : [];
|
|
3798
|
+
}
|
|
3353
3799
|
function pluralNoticeFor(params, merged) {
|
|
3354
3800
|
if (params.format !== "i18next-json") {
|
|
3355
3801
|
return [];
|
|
@@ -3370,26 +3816,53 @@ function baseSummary(parts) {
|
|
|
3370
3816
|
invalidIcuSource: parts.invalidIcuSource,
|
|
3371
3817
|
integrityMismatches: parts.integrityMismatches,
|
|
3372
3818
|
providerFailures: parts.providerFailures,
|
|
3819
|
+
budgetWithheld: parts.budgetWithheld,
|
|
3373
3820
|
generated: parts.generated,
|
|
3374
|
-
notices: parts.notices
|
|
3821
|
+
notices: parts.notices,
|
|
3822
|
+
needsReview: parts.needsReview ?? [],
|
|
3823
|
+
...parts.usage !== void 0 ? { usage: parts.usage } : {}
|
|
3375
3824
|
};
|
|
3376
3825
|
}
|
|
3377
|
-
|
|
3826
|
+
function needsReviewFor(acceptedKeys, reviewFlags) {
|
|
3827
|
+
const entries = [];
|
|
3828
|
+
for (const key of acceptedKeys) {
|
|
3829
|
+
const flag = reviewFlags.get(key);
|
|
3830
|
+
if (flag !== void 0) {
|
|
3831
|
+
entries.push({ key, reasons: flag.reasons });
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
return entries.sort((a, b) => a.key < b.key ? -1 : 1);
|
|
3835
|
+
}
|
|
3836
|
+
async function translateAndCheck(provider, params, entries, accepted, integrityMismatches, providerFailures, budgetWithheld, reviewFlags) {
|
|
3378
3837
|
const notices = [];
|
|
3838
|
+
const usage = createUsageAccumulator();
|
|
3839
|
+
let tripped = false;
|
|
3379
3840
|
for (const batch of chunk(entries, params.maxBatchSize)) {
|
|
3380
|
-
|
|
3841
|
+
if (params.budget.stopped) {
|
|
3842
|
+
for (const entry of batch) {
|
|
3843
|
+
budgetWithheld.push(entry.key);
|
|
3844
|
+
}
|
|
3845
|
+
continue;
|
|
3846
|
+
}
|
|
3847
|
+
const subResult = await runSubBatch(
|
|
3381
3848
|
provider,
|
|
3382
3849
|
params,
|
|
3383
3850
|
batch,
|
|
3384
3851
|
accepted,
|
|
3385
3852
|
integrityMismatches,
|
|
3386
|
-
providerFailures
|
|
3853
|
+
providerFailures,
|
|
3854
|
+
reviewFlags
|
|
3387
3855
|
);
|
|
3388
|
-
notices.push(...
|
|
3856
|
+
notices.push(...subResult.notices);
|
|
3857
|
+
foldUsage(usage, subResult.usage);
|
|
3858
|
+
foldTrackerUsage(params.budget, subResult.usage);
|
|
3859
|
+
if (checkBudgetTrip(params.budget)) {
|
|
3860
|
+
tripped = true;
|
|
3861
|
+
}
|
|
3389
3862
|
}
|
|
3390
|
-
return notices;
|
|
3863
|
+
return { notices, tripped, usage: usage.total };
|
|
3391
3864
|
}
|
|
3392
|
-
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures) {
|
|
3865
|
+
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures, reviewFlags) {
|
|
3393
3866
|
let result;
|
|
3394
3867
|
try {
|
|
3395
3868
|
result = await provider.translateBatch(buildRequest3(params, batch));
|
|
@@ -3397,12 +3870,17 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3397
3870
|
for (const entry of batch) {
|
|
3398
3871
|
providerFailures.push(entry.key);
|
|
3399
3872
|
}
|
|
3400
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
3873
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3401
3874
|
}
|
|
3402
3875
|
for (const entry of batch) {
|
|
3403
3876
|
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
3404
3877
|
}
|
|
3405
|
-
|
|
3878
|
+
if (result.reviewFlags !== void 0) {
|
|
3879
|
+
for (const [key, flag] of result.reviewFlags) {
|
|
3880
|
+
reviewFlags.set(key, flag);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3406
3884
|
}
|
|
3407
3885
|
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
3408
3886
|
const value = result.values.get(entry.key);
|
|
@@ -3460,6 +3938,10 @@ async function translate2(input, deps = {}) {
|
|
|
3460
3938
|
const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
|
|
3461
3939
|
const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
3462
3940
|
const fs = deps.fs ?? defaultFs;
|
|
3941
|
+
const budget = createBudgetTracker(
|
|
3942
|
+
config.maxTokens,
|
|
3943
|
+
config.budgetBehavior ?? DEFAULT_BUDGET_BEHAVIOR
|
|
3944
|
+
);
|
|
3463
3945
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3464
3946
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
3465
3947
|
const source = await readSource(config, cwd, fs, adapter);
|
|
@@ -3484,7 +3966,8 @@ async function translate2(input, deps = {}) {
|
|
|
3484
3966
|
prune,
|
|
3485
3967
|
generatePlurals,
|
|
3486
3968
|
maxBatchSize,
|
|
3487
|
-
fs
|
|
3969
|
+
fs,
|
|
3970
|
+
budget
|
|
3488
3971
|
};
|
|
3489
3972
|
const { summary, lockEntries } = await runLocale(params);
|
|
3490
3973
|
if (!dryRun) {
|
|
@@ -3497,7 +3980,19 @@ async function translate2(input, deps = {}) {
|
|
|
3497
3980
|
}
|
|
3498
3981
|
}
|
|
3499
3982
|
const { succeeded, failed } = partition(summaries);
|
|
3500
|
-
|
|
3983
|
+
const usage = summaries.reduce(
|
|
3984
|
+
(total, summary) => combineUsage(total, summary.usage),
|
|
3985
|
+
void 0
|
|
3986
|
+
);
|
|
3987
|
+
const budgetSummary = toBudgetSummary(budget);
|
|
3988
|
+
return {
|
|
3989
|
+
dryRun,
|
|
3990
|
+
locales: summaries,
|
|
3991
|
+
succeeded,
|
|
3992
|
+
failed,
|
|
3993
|
+
...usage !== void 0 ? { usage } : {},
|
|
3994
|
+
...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
|
|
3995
|
+
};
|
|
3501
3996
|
}
|
|
3502
3997
|
var ExchangeError = class extends Error {
|
|
3503
3998
|
code;
|
|
@@ -3519,10 +4014,13 @@ var INSTRUCTIONS_LINES = [
|
|
|
3519
4014
|
" structure and the argument names exactly. Translate only the human-readable text.",
|
|
3520
4015
|
"6. Do not edit the 'Key' column or the hidden 'Source hash' column. They map your translation",
|
|
3521
4016
|
" back to the right string. You may sort or filter rows freely; mapping does not depend on row order.",
|
|
4017
|
+
"7. The 'Context' column, when present, is reference text from the source file explaining what the",
|
|
4018
|
+
" string means or where it is used. It is read-only; leave it as-is.",
|
|
3522
4019
|
"",
|
|
3523
4020
|
"Status values:",
|
|
3524
4021
|
" new - this string has no translation yet.",
|
|
3525
4022
|
" changed - the source string changed and the translation needs updating.",
|
|
4023
|
+
" unchanged - already up to date, included for reference. No action needed.",
|
|
3526
4024
|
"",
|
|
3527
4025
|
"When you are done, save the file and send it back. verbatra checks every value on import:",
|
|
3528
4026
|
"a value with a broken placeholder, invalid ICU, or a source that changed since export is",
|
|
@@ -3534,7 +4032,10 @@ var COLUMN = {
|
|
|
3534
4032
|
current: 3,
|
|
3535
4033
|
status: 4,
|
|
3536
4034
|
translation: 5,
|
|
3537
|
-
sourceHash: 6
|
|
4035
|
+
sourceHash: 6,
|
|
4036
|
+
context: 7,
|
|
4037
|
+
reviewStatus: 8,
|
|
4038
|
+
reviewReasons: 9
|
|
3538
4039
|
};
|
|
3539
4040
|
var HEADERS = [
|
|
3540
4041
|
"Key",
|
|
@@ -3542,7 +4043,10 @@ var HEADERS = [
|
|
|
3542
4043
|
"Current translation",
|
|
3543
4044
|
"Status",
|
|
3544
4045
|
"Translation",
|
|
3545
|
-
"Source hash"
|
|
4046
|
+
"Source hash",
|
|
4047
|
+
"Context",
|
|
4048
|
+
"Review status",
|
|
4049
|
+
"Review reasons"
|
|
3546
4050
|
];
|
|
3547
4051
|
var HEADER_ROW = 1;
|
|
3548
4052
|
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
@@ -3561,7 +4065,10 @@ var COLUMN_WIDTHS = {
|
|
|
3561
4065
|
[COLUMN.source]: 50,
|
|
3562
4066
|
[COLUMN.current]: 50,
|
|
3563
4067
|
[COLUMN.status]: 12,
|
|
3564
|
-
[COLUMN.translation]: 50
|
|
4068
|
+
[COLUMN.translation]: 50,
|
|
4069
|
+
[COLUMN.context]: 50,
|
|
4070
|
+
[COLUMN.reviewStatus]: 12,
|
|
4071
|
+
[COLUMN.reviewReasons]: 40
|
|
3565
4072
|
};
|
|
3566
4073
|
var TEXT_NUMBER_FORMAT = "@";
|
|
3567
4074
|
function styleHeader(sheet) {
|
|
@@ -3592,7 +4099,10 @@ function writeRow(sheet, sheetRow) {
|
|
|
3592
4099
|
translationCell.numFmt = TEXT_NUMBER_FORMAT;
|
|
3593
4100
|
translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
|
|
3594
4101
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
3595
|
-
|
|
4102
|
+
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
4103
|
+
row.getCell(COLUMN.reviewStatus).value = sheetRow.reviewStatus;
|
|
4104
|
+
row.getCell(COLUMN.reviewReasons).value = sheetRow.reviewReasons;
|
|
4105
|
+
for (let column = COLUMN.key; column <= COLUMN.reviewReasons; column += 1) {
|
|
3596
4106
|
const cell = row.getCell(column);
|
|
3597
4107
|
cell.protection = { locked: column !== COLUMN.translation };
|
|
3598
4108
|
if (column !== COLUMN.translation) {
|
|
@@ -3617,6 +4127,26 @@ function assertValidWorksheetName(locale) {
|
|
|
3617
4127
|
);
|
|
3618
4128
|
}
|
|
3619
4129
|
}
|
|
4130
|
+
function assertNoWorksheetNameCollisions(sheets) {
|
|
4131
|
+
const reservedKey = INSTRUCTIONS_SHEET_NAME.toLowerCase();
|
|
4132
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4133
|
+
for (const sheet of sheets) {
|
|
4134
|
+
const key = sheet.locale.toLowerCase();
|
|
4135
|
+
if (key === reservedKey) {
|
|
4136
|
+
throw new ExchangeError(
|
|
4137
|
+
"WORKBOOK_INVALID",
|
|
4138
|
+
`The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with the reserved "${INSTRUCTIONS_SHEET_NAME}" sheet.`
|
|
4139
|
+
);
|
|
4140
|
+
}
|
|
4141
|
+
if (seen.has(key)) {
|
|
4142
|
+
throw new ExchangeError(
|
|
4143
|
+
"WORKBOOK_INVALID",
|
|
4144
|
+
`The locale "${sheet.locale}" cannot be an Excel worksheet name: it collides with another target locale's sheet name.`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
seen.add(key);
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
3620
4150
|
async function buildDataSheet(workbook, sheet) {
|
|
3621
4151
|
assertValidWorksheetName(sheet.locale);
|
|
3622
4152
|
const worksheet = workbook.addWorksheet(sheet.locale);
|
|
@@ -3643,6 +4173,7 @@ function buildInstructionsSheet(workbook) {
|
|
|
3643
4173
|
sheet.getRow(1).font = { bold: true };
|
|
3644
4174
|
}
|
|
3645
4175
|
async function buildWorkbook(model) {
|
|
4176
|
+
assertNoWorksheetNameCollisions(model.sheets);
|
|
3646
4177
|
const workbook = new ExcelJS.Workbook();
|
|
3647
4178
|
buildInstructionsSheet(workbook);
|
|
3648
4179
|
for (const sheet of model.sheets) {
|
|
@@ -3743,9 +4274,14 @@ var rowSchema = z.object({
|
|
|
3743
4274
|
key: z.string().min(1),
|
|
3744
4275
|
source: z.string(),
|
|
3745
4276
|
currentTarget: z.string(),
|
|
3746
|
-
status: z.enum(["new", "changed"]),
|
|
4277
|
+
status: z.enum(["new", "changed", "unchanged"]),
|
|
3747
4278
|
sourceHash: z.string(),
|
|
3748
|
-
translation: z.string()
|
|
4279
|
+
translation: z.string(),
|
|
4280
|
+
context: z.string(),
|
|
4281
|
+
// A legacy workbook exported before these columns existed has no such cell; cellString then yields
|
|
4282
|
+
// "", and .catch maps it to "ok" / "" rather than rejecting the row.
|
|
4283
|
+
reviewStatus: z.enum(["ok", "review"]).catch("ok"),
|
|
4284
|
+
reviewReasons: z.string().catch("")
|
|
3749
4285
|
});
|
|
3750
4286
|
function cellString(cell) {
|
|
3751
4287
|
const value = cell.value;
|
|
@@ -3778,7 +4314,13 @@ function parseRow(sheet, row) {
|
|
|
3778
4314
|
currentTarget: cellString(row.getCell(COLUMN.current)),
|
|
3779
4315
|
status: cellString(row.getCell(COLUMN.status)),
|
|
3780
4316
|
sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
|
|
3781
|
-
translation: cellString(row.getCell(COLUMN.translation))
|
|
4317
|
+
translation: cellString(row.getCell(COLUMN.translation)),
|
|
4318
|
+
// A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
|
|
4319
|
+
// empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
|
|
4320
|
+
context: cellString(row.getCell(COLUMN.context)),
|
|
4321
|
+
// Same backward-compatible pattern for a workbook built before the Review columns existed.
|
|
4322
|
+
reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
|
|
4323
|
+
reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
|
|
3782
4324
|
};
|
|
3783
4325
|
const result = rowSchema.safeParse(candidate);
|
|
3784
4326
|
if (!result.success) {
|
|
@@ -3852,7 +4394,34 @@ async function readTarget3(cwd, config, adapter, fs, locale) {
|
|
|
3852
4394
|
}
|
|
3853
4395
|
return (await adapter.read(path, locale)).resource;
|
|
3854
4396
|
}
|
|
3855
|
-
function
|
|
4397
|
+
function reasonLabel(reason) {
|
|
4398
|
+
return reason.toLowerCase().replace(/_/g, "-");
|
|
4399
|
+
}
|
|
4400
|
+
function reviewColumns(flag) {
|
|
4401
|
+
if (flag === void 0) {
|
|
4402
|
+
return { reviewStatus: "ok", reviewReasons: "" };
|
|
4403
|
+
}
|
|
4404
|
+
return { reviewStatus: "review", reviewReasons: flag.reasons.map(reasonLabel).join(", ") };
|
|
4405
|
+
}
|
|
4406
|
+
function computeRowReview(adapter, sourceValue, currentTarget, sourceLocale, targetLocale, glossary) {
|
|
4407
|
+
if (currentTarget === "") {
|
|
4408
|
+
return { reviewStatus: "ok", reviewReasons: "" };
|
|
4409
|
+
}
|
|
4410
|
+
const integrity = adapter.comparePlaceholders?.(sourceValue, currentTarget) ?? checkPlaceholders(
|
|
4411
|
+
adapter.extractPlaceholders(sourceValue),
|
|
4412
|
+
adapter.extractPlaceholders(currentTarget)
|
|
4413
|
+
);
|
|
4414
|
+
const flag = computeReviewFlags({
|
|
4415
|
+
sourceValue,
|
|
4416
|
+
translatedValue: currentTarget,
|
|
4417
|
+
sourceLocale,
|
|
4418
|
+
targetLocale,
|
|
4419
|
+
integrity,
|
|
4420
|
+
glossary
|
|
4421
|
+
});
|
|
4422
|
+
return reviewColumns(flag);
|
|
4423
|
+
}
|
|
4424
|
+
function buildRows(source, target, baseline, includeUnchanged, adapter, glossary) {
|
|
3856
4425
|
const diff2 = diffResources(source, target, { baseline });
|
|
3857
4426
|
const rows = [];
|
|
3858
4427
|
const add = (keys, status) => {
|
|
@@ -3861,20 +4430,30 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
3861
4430
|
if (sourceEntry === void 0) {
|
|
3862
4431
|
continue;
|
|
3863
4432
|
}
|
|
4433
|
+
const currentTarget = target.entries.get(key)?.value ?? "";
|
|
3864
4434
|
rows.push({
|
|
3865
4435
|
key,
|
|
3866
4436
|
source: sourceEntry.value,
|
|
3867
|
-
currentTarget
|
|
4437
|
+
currentTarget,
|
|
3868
4438
|
status,
|
|
3869
4439
|
sourceHash: contentHash(sourceEntry),
|
|
3870
|
-
translation: ""
|
|
4440
|
+
translation: "",
|
|
4441
|
+
context: sourceEntry.description ?? "",
|
|
4442
|
+
...computeRowReview(
|
|
4443
|
+
adapter,
|
|
4444
|
+
sourceEntry.value,
|
|
4445
|
+
currentTarget,
|
|
4446
|
+
source.locale,
|
|
4447
|
+
target.locale,
|
|
4448
|
+
glossary
|
|
4449
|
+
)
|
|
3871
4450
|
});
|
|
3872
4451
|
}
|
|
3873
4452
|
};
|
|
3874
4453
|
add(diff2.missing, "new");
|
|
3875
4454
|
add(diff2.changed, "changed");
|
|
3876
4455
|
if (includeUnchanged) {
|
|
3877
|
-
add(diff2.unchanged, "
|
|
4456
|
+
add(diff2.unchanged, "unchanged");
|
|
3878
4457
|
}
|
|
3879
4458
|
return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
3880
4459
|
}
|
|
@@ -3893,7 +4472,9 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
3893
4472
|
source.resource,
|
|
3894
4473
|
target,
|
|
3895
4474
|
baselineFor(lock, locale),
|
|
3896
|
-
input.includeUnchanged ?? false
|
|
4475
|
+
input.includeUnchanged ?? false,
|
|
4476
|
+
adapter,
|
|
4477
|
+
config.glossary
|
|
3897
4478
|
);
|
|
3898
4479
|
return { locale, rows };
|
|
3899
4480
|
})
|
|
@@ -3994,9 +4575,14 @@ function importLocale(params) {
|
|
|
3994
4575
|
integrityMismatches: [...buckets.mismatches].sort(),
|
|
3995
4576
|
// A workbook import never calls a provider, so a provider-call failure cannot occur here.
|
|
3996
4577
|
providerFailures: [],
|
|
4578
|
+
// A workbook import never calls a provider, so the budget guardrail never withholds anything here.
|
|
4579
|
+
budgetWithheld: [],
|
|
3997
4580
|
// Plural generation is a translate-flow concern; the manual workbook import never generates forms.
|
|
3998
4581
|
generated: [],
|
|
3999
|
-
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : []
|
|
4582
|
+
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
|
|
4583
|
+
// A workbook import never calls a provider and never recomputes review flags on its own path; the
|
|
4584
|
+
// Excel workbook's Review status/reasons columns are informational only (see export-workbook.ts).
|
|
4585
|
+
needsReview: []
|
|
4000
4586
|
};
|
|
4001
4587
|
return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
|
|
4002
4588
|
}
|