@verbatra/sdk 0.5.0-next.3 → 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 +470 -63
- 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 +467 -60
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -502,6 +502,76 @@ function validateRequest(request) {
|
|
|
502
502
|
}
|
|
503
503
|
return parsed.data;
|
|
504
504
|
}
|
|
505
|
+
var LENGTH_RATIO_MIN = 0.35;
|
|
506
|
+
var LENGTH_RATIO_MAX = 3;
|
|
507
|
+
var LENGTH_RATIO_MIN_SOURCE_LENGTH = 12;
|
|
508
|
+
var UNICODE_LETTER = /\p{L}/u;
|
|
509
|
+
var DEGRADATION_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
510
|
+
"FORMALITY_DOWNGRADED",
|
|
511
|
+
"GLOSSARY_IGNORED"
|
|
512
|
+
]);
|
|
513
|
+
function isLengthRatioOutlier(sourceValue, translatedValue) {
|
|
514
|
+
const trimmedSource = sourceValue.trim();
|
|
515
|
+
if (trimmedSource.length < LENGTH_RATIO_MIN_SOURCE_LENGTH) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
const ratio = translatedValue.trim().length / trimmedSource.length;
|
|
519
|
+
return ratio < LENGTH_RATIO_MIN || ratio > LENGTH_RATIO_MAX;
|
|
520
|
+
}
|
|
521
|
+
function isEqualsSource(input) {
|
|
522
|
+
const trimmedSource = input.sourceValue.trim();
|
|
523
|
+
const trimmedTranslated = input.translatedValue.trim();
|
|
524
|
+
return trimmedTranslated === trimmedSource && input.targetLocale !== input.sourceLocale && UNICODE_LETTER.test(trimmedSource);
|
|
525
|
+
}
|
|
526
|
+
function isGlossaryTermMissed(input) {
|
|
527
|
+
const glossary = input.glossary;
|
|
528
|
+
if (glossary === void 0 || Object.keys(glossary).length === 0) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
const sourceLower = input.sourceValue.toLowerCase();
|
|
532
|
+
const translatedLower = input.translatedValue.toLowerCase();
|
|
533
|
+
for (const [sourceTerm, targetTerm] of Object.entries(glossary)) {
|
|
534
|
+
const sourceHit = sourceLower.includes(sourceTerm.toLowerCase());
|
|
535
|
+
const targetHit = translatedLower.includes(targetTerm.toLowerCase());
|
|
536
|
+
if (sourceHit && !targetHit) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
function isIntegrityReordered(integrity) {
|
|
543
|
+
return integrity.matches && integrity.reordered;
|
|
544
|
+
}
|
|
545
|
+
function computeReviewFlags(input) {
|
|
546
|
+
const reasons = [];
|
|
547
|
+
if (isLengthRatioOutlier(input.sourceValue, input.translatedValue)) {
|
|
548
|
+
reasons.push("LENGTH_RATIO_OUTLIER");
|
|
549
|
+
}
|
|
550
|
+
if (isEqualsSource(input)) {
|
|
551
|
+
reasons.push("EQUALS_SOURCE");
|
|
552
|
+
}
|
|
553
|
+
if (isGlossaryTermMissed(input)) {
|
|
554
|
+
reasons.push("GLOSSARY_TERM_MISSED");
|
|
555
|
+
}
|
|
556
|
+
if (isIntegrityReordered(input.integrity)) {
|
|
557
|
+
reasons.push("INTEGRITY_REORDERED");
|
|
558
|
+
}
|
|
559
|
+
return reasons.length > 0 ? { status: "review", reasons } : void 0;
|
|
560
|
+
}
|
|
561
|
+
function applyProviderDegraded(reviewFlags, notices, acceptedKeys) {
|
|
562
|
+
if (!notices.some((notice) => DEGRADATION_NOTICE_CODES.has(notice.code))) {
|
|
563
|
+
return reviewFlags;
|
|
564
|
+
}
|
|
565
|
+
const next = new Map(reviewFlags);
|
|
566
|
+
for (const key of acceptedKeys) {
|
|
567
|
+
const existing = next.get(key);
|
|
568
|
+
next.set(key, {
|
|
569
|
+
status: "review",
|
|
570
|
+
reasons: existing !== void 0 ? [...existing.reasons, "PROVIDER_DEGRADED"] : ["PROVIDER_DEGRADED"]
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
return next;
|
|
574
|
+
}
|
|
505
575
|
function toIntegrityInputs(entries, values) {
|
|
506
576
|
const inputs = [];
|
|
507
577
|
for (const entry of entries) {
|
|
@@ -597,7 +667,33 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
597
667
|
request.extractPlaceholders,
|
|
598
668
|
request.comparePlaceholders
|
|
599
669
|
);
|
|
600
|
-
|
|
670
|
+
const notices = [];
|
|
671
|
+
const reviewFlags = applyProviderDegraded(buildReviewFlags(data, values, integrity), notices, [
|
|
672
|
+
...values.keys()
|
|
673
|
+
]);
|
|
674
|
+
return usage === void 0 ? { values, integrity, notices, reviewFlags } : { values, integrity, usage, notices, reviewFlags };
|
|
675
|
+
}
|
|
676
|
+
function buildReviewFlags(data, values, integrity) {
|
|
677
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
678
|
+
for (const entry of data.entries) {
|
|
679
|
+
const translatedValue = values.get(entry.key);
|
|
680
|
+
const entryIntegrity = integrity.get(entry.key);
|
|
681
|
+
if (translatedValue === void 0 || entryIntegrity === void 0) {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const flag = computeReviewFlags({
|
|
685
|
+
sourceValue: entry.value,
|
|
686
|
+
translatedValue,
|
|
687
|
+
sourceLocale: data.sourceLocale,
|
|
688
|
+
targetLocale: data.targetLocale,
|
|
689
|
+
integrity: entryIntegrity,
|
|
690
|
+
glossary: data.glossary
|
|
691
|
+
});
|
|
692
|
+
if (flag !== void 0) {
|
|
693
|
+
reviewFlags.set(entry.key, flag);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return reviewFlags;
|
|
601
697
|
}
|
|
602
698
|
async function requestTranslations(mechanism, data, signal) {
|
|
603
699
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
@@ -952,7 +1048,34 @@ async function translate(bundle, config, request) {
|
|
|
952
1048
|
if (unprotectable.length > 0) {
|
|
953
1049
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
954
1050
|
}
|
|
955
|
-
|
|
1051
|
+
const reviewFlags = applyProviderDegraded(
|
|
1052
|
+
buildReviewFlags2(protectable, data, values, integrity, request.glossary),
|
|
1053
|
+
notices,
|
|
1054
|
+
[...values.keys()]
|
|
1055
|
+
);
|
|
1056
|
+
return { values, integrity, notices, reviewFlags };
|
|
1057
|
+
}
|
|
1058
|
+
function buildReviewFlags2(protectable, data, values, integrity, glossary) {
|
|
1059
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
1060
|
+
for (const entry of protectable) {
|
|
1061
|
+
const translatedValue = values.get(entry.key);
|
|
1062
|
+
const entryIntegrity = integrity.get(entry.key);
|
|
1063
|
+
if (translatedValue === void 0 || entryIntegrity === void 0) {
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const flag = computeReviewFlags({
|
|
1067
|
+
sourceValue: entry.value,
|
|
1068
|
+
translatedValue,
|
|
1069
|
+
sourceLocale: data.sourceLocale,
|
|
1070
|
+
targetLocale: data.targetLocale,
|
|
1071
|
+
integrity: entryIntegrity,
|
|
1072
|
+
glossary
|
|
1073
|
+
});
|
|
1074
|
+
if (flag !== void 0) {
|
|
1075
|
+
reviewFlags.set(entry.key, flag);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return reviewFlags;
|
|
956
1079
|
}
|
|
957
1080
|
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
958
1081
|
if (protectable.length === 0) {
|
|
@@ -1456,6 +1579,7 @@ function buildProvider(config) {
|
|
|
1456
1579
|
|
|
1457
1580
|
// src/config/schema.ts
|
|
1458
1581
|
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1582
|
+
var DEFAULT_BUDGET_BEHAVIOR = "warn";
|
|
1459
1583
|
function findCaseInsensitiveDuplicate(locales) {
|
|
1460
1584
|
const seen = /* @__PURE__ */ new Set();
|
|
1461
1585
|
for (const locale of locales) {
|
|
@@ -1504,7 +1628,23 @@ var verbatraConfigSchema = zod.z.strictObject({
|
|
|
1504
1628
|
* be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
|
|
1505
1629
|
* {@link DEFAULT_MAX_BATCH_SIZE} applies.
|
|
1506
1630
|
*/
|
|
1507
|
-
maxBatchSize: zod.z.number().int().positive().optional()
|
|
1631
|
+
maxBatchSize: zod.z.number().int().positive().optional(),
|
|
1632
|
+
/**
|
|
1633
|
+
* Optional whole-run token ceiling (input plus output tokens summed across every provider call, main
|
|
1634
|
+
* translation and plural generation alike, across all target locales). Checked after each completed
|
|
1635
|
+
* sub-batch, never mid-batch: the sub-batch whose completion crosses the ceiling is retained and
|
|
1636
|
+
* counted, since a call already in flight cannot be undone. Must be a positive integer. Config-only,
|
|
1637
|
+
* no CLI flag. Absent means no budget is enforced. Inert (never a false trip) against a token-less
|
|
1638
|
+
* provider such as DeepL, since it never reports usage to measure against the ceiling.
|
|
1639
|
+
*/
|
|
1640
|
+
maxTokens: zod.z.number().int().positive().optional(),
|
|
1641
|
+
/**
|
|
1642
|
+
* What happens once `maxTokens` is reached: `"warn"` (default) flags it and lets the run continue
|
|
1643
|
+
* unchanged; `"stop"` withholds every not-yet-attempted key for the rest of the run (the current
|
|
1644
|
+
* locale's remaining candidates and every later locale's), retried automatically next run. Present
|
|
1645
|
+
* without `maxTokens` is accepted and has no effect. Never changes the command's exit code.
|
|
1646
|
+
*/
|
|
1647
|
+
budgetBehavior: zod.z.enum(["warn", "stop"]).optional()
|
|
1508
1648
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
1509
1649
|
message: "targetLocales must not include the source locale",
|
|
1510
1650
|
path: ["targetLocales"]
|
|
@@ -1938,7 +2078,7 @@ var AdapterError = class extends Error {
|
|
|
1938
2078
|
var MAX_DEPTH = 100;
|
|
1939
2079
|
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1940
2080
|
var jsonTreeSchema = zod.z.lazy(
|
|
1941
|
-
() => zod.z.union([zod.z.string(), zod.z.record(zod.z.string(), jsonTreeSchema)])
|
|
2081
|
+
() => zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null(), zod.z.record(zod.z.string(), jsonTreeSchema)])
|
|
1942
2082
|
);
|
|
1943
2083
|
var rootSchema = zod.z.record(zod.z.string(), jsonTreeSchema);
|
|
1944
2084
|
function assertWithinDepth(value, max) {
|
|
@@ -1966,7 +2106,7 @@ function assertJsonRecord(value) {
|
|
|
1966
2106
|
if (!result.success) {
|
|
1967
2107
|
throw new AdapterError(
|
|
1968
2108
|
"INVALID_STRUCTURE",
|
|
1969
|
-
"The file is not a valid object (expected nested objects of string
|
|
2109
|
+
"The file is not a valid object (expected nested objects of string, number, boolean, or null leaves)."
|
|
1970
2110
|
);
|
|
1971
2111
|
}
|
|
1972
2112
|
return result.data;
|
|
@@ -1984,6 +2124,9 @@ function serializeJsonTree(tree) {
|
|
|
1984
2124
|
return `${JSON.stringify(tree, null, 2)}
|
|
1985
2125
|
`;
|
|
1986
2126
|
}
|
|
2127
|
+
function isJsonNode(value) {
|
|
2128
|
+
return typeof value === "object" && value !== null;
|
|
2129
|
+
}
|
|
1987
2130
|
function namespaceOf(filePath) {
|
|
1988
2131
|
return path.basename(filePath, path.extname(filePath));
|
|
1989
2132
|
}
|
|
@@ -2011,9 +2154,25 @@ function buildCanHandle(extensions, sniff) {
|
|
|
2011
2154
|
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
2012
2155
|
};
|
|
2013
2156
|
}
|
|
2157
|
+
async function fsyncPath(path) {
|
|
2158
|
+
const handle = await promises.open(path, "r");
|
|
2159
|
+
try {
|
|
2160
|
+
await handle.sync();
|
|
2161
|
+
} finally {
|
|
2162
|
+
await handle.close();
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
async function fsyncDirBestEffort(path) {
|
|
2166
|
+
try {
|
|
2167
|
+
await fsyncPath(path);
|
|
2168
|
+
} catch {
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2014
2171
|
var nodeOps = {
|
|
2015
2172
|
writeFile: (path, data) => promises.writeFile(path, data, "utf8"),
|
|
2173
|
+
fsyncFile: (path) => fsyncPath(path),
|
|
2016
2174
|
rename: (from, to) => promises.rename(from, to),
|
|
2175
|
+
fsyncDir: (path) => fsyncDirBestEffort(path),
|
|
2017
2176
|
rm: (path) => promises.rm(path, { force: true })
|
|
2018
2177
|
};
|
|
2019
2178
|
async function cleanup(ops, tmp) {
|
|
@@ -2025,15 +2184,20 @@ async function cleanup(ops, tmp) {
|
|
|
2025
2184
|
function tempFileName2(path$1) {
|
|
2026
2185
|
return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
|
|
2027
2186
|
}
|
|
2028
|
-
async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
2029
|
-
const tmp = tempFileName2(path);
|
|
2187
|
+
async function atomicWriteFile(path$1, data, ops = nodeOps) {
|
|
2188
|
+
const tmp = tempFileName2(path$1);
|
|
2030
2189
|
try {
|
|
2031
2190
|
await ops.writeFile(tmp, data);
|
|
2032
|
-
await ops.
|
|
2191
|
+
await ops.fsyncFile(tmp);
|
|
2192
|
+
await ops.rename(tmp, path$1);
|
|
2033
2193
|
} catch (error) {
|
|
2034
2194
|
await cleanup(ops, tmp);
|
|
2035
2195
|
throw error;
|
|
2036
2196
|
}
|
|
2197
|
+
try {
|
|
2198
|
+
await ops.fsyncDir(path.dirname(path$1));
|
|
2199
|
+
} catch {
|
|
2200
|
+
}
|
|
2037
2201
|
}
|
|
2038
2202
|
function stripLeadingBom(content) {
|
|
2039
2203
|
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
@@ -2155,20 +2319,24 @@ function addLeaf(ctx, segments, key, value) {
|
|
|
2155
2319
|
);
|
|
2156
2320
|
}
|
|
2157
2321
|
ctx.claimed.set(effectivePath, mapKey);
|
|
2322
|
+
if (typeof value !== "string") {
|
|
2323
|
+
ctx.excluded.push(mapKey);
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2158
2326
|
const { placeholders, isPlural } = ctx.derive(key, value);
|
|
2159
2327
|
ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
|
|
2160
2328
|
}
|
|
2161
2329
|
function addEntries(ctx, prefix, node) {
|
|
2162
2330
|
for (const [key, value] of Object.entries(node)) {
|
|
2163
2331
|
const segments = [...prefix, key];
|
|
2164
|
-
if (
|
|
2165
|
-
addLeaf(ctx, segments, key, value);
|
|
2166
|
-
} else {
|
|
2332
|
+
if (isJsonNode(value)) {
|
|
2167
2333
|
addEntries(ctx, segments, value);
|
|
2334
|
+
} else {
|
|
2335
|
+
addLeaf(ctx, segments, key, value);
|
|
2168
2336
|
}
|
|
2169
2337
|
}
|
|
2170
2338
|
}
|
|
2171
|
-
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
2339
|
+
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths, excluded) {
|
|
2172
2340
|
for (const [key, value] of Object.entries(node)) {
|
|
2173
2341
|
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
2174
2342
|
if (claimedPaths.has(path)) {
|
|
@@ -2178,22 +2346,26 @@ function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
|
2178
2346
|
);
|
|
2179
2347
|
}
|
|
2180
2348
|
claimedPaths.add(path);
|
|
2181
|
-
if (
|
|
2349
|
+
if (isJsonNode(value)) {
|
|
2350
|
+
addPathEntries(value, path, namespace, derive, out, claimedPaths, excluded);
|
|
2351
|
+
} else if (typeof value === "string") {
|
|
2182
2352
|
const { placeholders, isPlural } = derive(key, value);
|
|
2183
2353
|
out.set(path, { key: path, namespace, value, placeholders, isPlural });
|
|
2184
2354
|
} else {
|
|
2185
|
-
|
|
2355
|
+
excluded.push(path);
|
|
2186
2356
|
}
|
|
2187
2357
|
}
|
|
2188
2358
|
}
|
|
2189
2359
|
function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
|
|
2190
2360
|
const out = /* @__PURE__ */ new Map();
|
|
2191
2361
|
if (keyMode === "path-notation") {
|
|
2192
|
-
|
|
2193
|
-
|
|
2362
|
+
const excluded = [];
|
|
2363
|
+
addPathEntries(tree, "", namespace, derive, out, /* @__PURE__ */ new Set(), excluded);
|
|
2364
|
+
return { entries: out, excludedLeafPaths: excluded };
|
|
2194
2365
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2366
|
+
const ctx = { namespace, derive, out, claimed: /* @__PURE__ */ new Map(), excluded: [] };
|
|
2367
|
+
addEntries(ctx, [], tree);
|
|
2368
|
+
return { entries: out, excludedLeafPaths: ctx.excluded };
|
|
2197
2369
|
}
|
|
2198
2370
|
function emptyNode() {
|
|
2199
2371
|
return /* @__PURE__ */ Object.create(null);
|
|
@@ -2246,9 +2418,9 @@ function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTre
|
|
|
2246
2418
|
try {
|
|
2247
2419
|
const tree = parse3(content);
|
|
2248
2420
|
validateTree?.(tree);
|
|
2249
|
-
const
|
|
2250
|
-
mergeDescriptions(entries, content, deriveDescriptions);
|
|
2251
|
-
return
|
|
2421
|
+
const result = flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
2422
|
+
mergeDescriptions(result.entries, content, deriveDescriptions);
|
|
2423
|
+
return result;
|
|
2252
2424
|
} catch (error) {
|
|
2253
2425
|
rethrowStructured(error, "The file could not be parsed.");
|
|
2254
2426
|
}
|
|
@@ -2279,7 +2451,7 @@ function createTreeFileAdapter(options) {
|
|
|
2279
2451
|
async read(filePath, locale) {
|
|
2280
2452
|
const content = await readFileContent(filePath);
|
|
2281
2453
|
const namespace = namespaceOf(filePath);
|
|
2282
|
-
const entries = toEntries(
|
|
2454
|
+
const { entries, excludedLeafPaths } = toEntries(
|
|
2283
2455
|
content,
|
|
2284
2456
|
namespace,
|
|
2285
2457
|
parse3,
|
|
@@ -2290,7 +2462,7 @@ function createTreeFileAdapter(options) {
|
|
|
2290
2462
|
);
|
|
2291
2463
|
const resource = { locale, namespace, format, entries };
|
|
2292
2464
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2293
|
-
return { resource, invalidIcuKeys };
|
|
2465
|
+
return { resource, invalidIcuKeys, excludedLeafPaths };
|
|
2294
2466
|
},
|
|
2295
2467
|
async write(resource, filePath) {
|
|
2296
2468
|
const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
|
|
@@ -2384,8 +2556,10 @@ async function buildArbWriteTree(entries, filePath) {
|
|
|
2384
2556
|
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
2385
2557
|
if (translated !== void 0) {
|
|
2386
2558
|
consumed.add(key);
|
|
2559
|
+
out[key] = translated;
|
|
2560
|
+
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
2561
|
+
out[key] = value;
|
|
2387
2562
|
}
|
|
2388
|
-
out[key] = translated ?? value;
|
|
2389
2563
|
}
|
|
2390
2564
|
for (const [key, value] of messages) {
|
|
2391
2565
|
if (!consumed.has(key)) {
|
|
@@ -2479,7 +2653,7 @@ function createNextIntlJsonAdapter() {
|
|
|
2479
2653
|
}
|
|
2480
2654
|
function assertNoDottedNestedKey(tree) {
|
|
2481
2655
|
for (const [key, value] of Object.entries(tree)) {
|
|
2482
|
-
if (
|
|
2656
|
+
if (!isJsonNode(value)) {
|
|
2483
2657
|
continue;
|
|
2484
2658
|
}
|
|
2485
2659
|
if (key.includes(".")) {
|
|
@@ -2495,7 +2669,7 @@ function assertNotMixed(tree) {
|
|
|
2495
2669
|
let hasNested = false;
|
|
2496
2670
|
let hasFlatDottedKey = false;
|
|
2497
2671
|
for (const [key, value] of Object.entries(tree)) {
|
|
2498
|
-
if (
|
|
2672
|
+
if (isJsonNode(value)) {
|
|
2499
2673
|
hasNested = true;
|
|
2500
2674
|
} else if (key.includes(".")) {
|
|
2501
2675
|
hasFlatDottedKey = true;
|
|
@@ -2656,7 +2830,7 @@ function createFlatFileAdapter(options) {
|
|
|
2656
2830
|
const entries = toEntries2(content, namespace, parseEntries);
|
|
2657
2831
|
const resource = { locale, namespace, format, entries };
|
|
2658
2832
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2659
|
-
return { resource, invalidIcuKeys };
|
|
2833
|
+
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
2660
2834
|
},
|
|
2661
2835
|
async write(resource, filePath) {
|
|
2662
2836
|
const data = await serializeEntries(resource.entries, filePath);
|
|
@@ -3110,6 +3284,46 @@ function selectProvider(config, createProvider = buildProvider) {
|
|
|
3110
3284
|
}
|
|
3111
3285
|
}
|
|
3112
3286
|
|
|
3287
|
+
// src/flow/budget.ts
|
|
3288
|
+
function createBudgetTracker(maxTokens, behavior) {
|
|
3289
|
+
return { maxTokens, behavior, tokensUsed: 0, usageSeen: false, exceeded: false, stopped: false };
|
|
3290
|
+
}
|
|
3291
|
+
function foldTrackerUsage(tracker, usage) {
|
|
3292
|
+
if (usage === void 0) {
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
tracker.usageSeen = true;
|
|
3296
|
+
tracker.tokensUsed += usage.inputTokens + usage.outputTokens;
|
|
3297
|
+
}
|
|
3298
|
+
function checkBudgetTrip(tracker) {
|
|
3299
|
+
if (tracker.maxTokens === void 0 || tracker.exceeded || tracker.tokensUsed < tracker.maxTokens) {
|
|
3300
|
+
return false;
|
|
3301
|
+
}
|
|
3302
|
+
tracker.exceeded = true;
|
|
3303
|
+
if (tracker.behavior === "stop") {
|
|
3304
|
+
tracker.stopped = true;
|
|
3305
|
+
}
|
|
3306
|
+
return true;
|
|
3307
|
+
}
|
|
3308
|
+
function toBudgetSummary(tracker) {
|
|
3309
|
+
if (tracker.maxTokens === void 0) {
|
|
3310
|
+
return void 0;
|
|
3311
|
+
}
|
|
3312
|
+
return {
|
|
3313
|
+
maxTokens: tracker.maxTokens,
|
|
3314
|
+
behavior: tracker.behavior,
|
|
3315
|
+
supported: tracker.usageSeen,
|
|
3316
|
+
tokensUsed: tracker.tokensUsed,
|
|
3317
|
+
exceeded: tracker.exceeded
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
function budgetExceededNotice(tracker) {
|
|
3321
|
+
return {
|
|
3322
|
+
code: "BUDGET_TOKENS_EXCEEDED",
|
|
3323
|
+
message: `The run's cumulative token usage (${tracker.tokensUsed}) reached the configured budget of ${tracker.maxTokens} tokens (behavior: ${tracker.behavior}).`
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3113
3327
|
// src/flow/locale-failure.ts
|
|
3114
3328
|
function describeError(error) {
|
|
3115
3329
|
if (error instanceof Error) {
|
|
@@ -3129,8 +3343,10 @@ function failureSummary(locale, error) {
|
|
|
3129
3343
|
invalidIcuSource: [],
|
|
3130
3344
|
integrityMismatches: [],
|
|
3131
3345
|
providerFailures: [],
|
|
3346
|
+
budgetWithheld: [],
|
|
3132
3347
|
generated: [],
|
|
3133
3348
|
notices: [],
|
|
3349
|
+
needsReview: [],
|
|
3134
3350
|
error: describeError(error)
|
|
3135
3351
|
};
|
|
3136
3352
|
}
|
|
@@ -3300,7 +3516,43 @@ function planPluralGeneration(source, targetLocale, format) {
|
|
|
3300
3516
|
return { items };
|
|
3301
3517
|
}
|
|
3302
3518
|
|
|
3519
|
+
// src/flow/usage.ts
|
|
3520
|
+
function createUsageAccumulator() {
|
|
3521
|
+
return { total: void 0 };
|
|
3522
|
+
}
|
|
3523
|
+
function foldUsage(accumulator, usage) {
|
|
3524
|
+
if (usage === void 0) {
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
const prior = accumulator.total ?? { inputTokens: 0, outputTokens: 0 };
|
|
3528
|
+
accumulator.total = {
|
|
3529
|
+
inputTokens: prior.inputTokens + usage.inputTokens,
|
|
3530
|
+
outputTokens: prior.outputTokens + usage.outputTokens
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
function combineUsage(a, b) {
|
|
3534
|
+
if (a === void 0) {
|
|
3535
|
+
return b;
|
|
3536
|
+
}
|
|
3537
|
+
if (b === void 0) {
|
|
3538
|
+
return a;
|
|
3539
|
+
}
|
|
3540
|
+
return {
|
|
3541
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
3542
|
+
outputTokens: a.outputTokens + b.outputTokens
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3303
3546
|
// src/flow/plural-generation.ts
|
|
3547
|
+
var EMPTY_RESULT = {
|
|
3548
|
+
accepted: [],
|
|
3549
|
+
withheld: [],
|
|
3550
|
+
providerFailures: [],
|
|
3551
|
+
budgetWithheld: [],
|
|
3552
|
+
notices: [],
|
|
3553
|
+
usage: void 0,
|
|
3554
|
+
tripped: false
|
|
3555
|
+
};
|
|
3304
3556
|
function generatedLockHash(governingEntries, category) {
|
|
3305
3557
|
const governingHashes = governingEntries.map(contentHash).sort();
|
|
3306
3558
|
return contentHash({
|
|
@@ -3339,23 +3591,45 @@ async function generatePluralForms(context) {
|
|
|
3339
3591
|
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
3340
3592
|
const stale = staleItems(plan.items, context.baseline);
|
|
3341
3593
|
if (stale.length === 0) {
|
|
3342
|
-
return
|
|
3594
|
+
return EMPTY_RESULT;
|
|
3343
3595
|
}
|
|
3344
3596
|
const accepted = [];
|
|
3345
3597
|
const withheld = [];
|
|
3346
3598
|
const providerFailures = [];
|
|
3599
|
+
const budgetWithheld = [];
|
|
3347
3600
|
const notices = [];
|
|
3601
|
+
const usage = createUsageAccumulator();
|
|
3602
|
+
let tripped = false;
|
|
3348
3603
|
for (const batch of chunk(stale, context.maxBatchSize)) {
|
|
3349
|
-
|
|
3604
|
+
if (context.budget.stopped) {
|
|
3605
|
+
for (const item of batch) {
|
|
3606
|
+
budgetWithheld.push(item.targetKey);
|
|
3607
|
+
}
|
|
3608
|
+
continue;
|
|
3609
|
+
}
|
|
3610
|
+
const subResult = await runGenerationSubBatch(
|
|
3350
3611
|
context,
|
|
3351
3612
|
batch,
|
|
3352
3613
|
accepted,
|
|
3353
3614
|
withheld,
|
|
3354
3615
|
providerFailures
|
|
3355
3616
|
);
|
|
3356
|
-
notices.push(...
|
|
3617
|
+
notices.push(...subResult.notices);
|
|
3618
|
+
foldUsage(usage, subResult.usage);
|
|
3619
|
+
foldTrackerUsage(context.budget, subResult.usage);
|
|
3620
|
+
if (checkBudgetTrip(context.budget)) {
|
|
3621
|
+
tripped = true;
|
|
3622
|
+
}
|
|
3357
3623
|
}
|
|
3358
|
-
return {
|
|
3624
|
+
return {
|
|
3625
|
+
accepted,
|
|
3626
|
+
withheld,
|
|
3627
|
+
providerFailures,
|
|
3628
|
+
budgetWithheld,
|
|
3629
|
+
notices,
|
|
3630
|
+
usage: usage.total,
|
|
3631
|
+
tripped
|
|
3632
|
+
};
|
|
3359
3633
|
}
|
|
3360
3634
|
async function runGenerationSubBatch(context, batch, accepted, withheld, providerFailures) {
|
|
3361
3635
|
let result;
|
|
@@ -3366,12 +3640,12 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3366
3640
|
for (const item of batch) {
|
|
3367
3641
|
providerFailures.push(item.targetKey);
|
|
3368
3642
|
}
|
|
3369
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
3643
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3370
3644
|
}
|
|
3371
3645
|
for (const item of batch) {
|
|
3372
3646
|
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
3373
3647
|
}
|
|
3374
|
-
return readNotices(result);
|
|
3648
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3375
3649
|
}
|
|
3376
3650
|
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
3377
3651
|
const value = result.values.get(item.targetKey);
|
|
@@ -3439,6 +3713,7 @@ async function runLocale(params) {
|
|
|
3439
3713
|
generated: [],
|
|
3440
3714
|
integrityMismatches: [],
|
|
3441
3715
|
providerFailures: [],
|
|
3716
|
+
budgetWithheld: [],
|
|
3442
3717
|
pruned,
|
|
3443
3718
|
notices: sdkNotices
|
|
3444
3719
|
}),
|
|
@@ -3446,16 +3721,21 @@ async function runLocale(params) {
|
|
|
3446
3721
|
};
|
|
3447
3722
|
}
|
|
3448
3723
|
const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
|
|
3724
|
+
const startedStopped = params.budget.stopped;
|
|
3449
3725
|
const accepted = /* @__PURE__ */ new Map();
|
|
3450
3726
|
const integrityMismatches = [];
|
|
3451
3727
|
const providerFailures = [];
|
|
3452
|
-
const
|
|
3728
|
+
const budgetWithheld = [];
|
|
3729
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
3730
|
+
const translation = await translateAndCheck(
|
|
3453
3731
|
provider,
|
|
3454
3732
|
params,
|
|
3455
3733
|
entries,
|
|
3456
3734
|
accepted,
|
|
3457
3735
|
integrityMismatches,
|
|
3458
|
-
providerFailures
|
|
3736
|
+
providerFailures,
|
|
3737
|
+
budgetWithheld,
|
|
3738
|
+
reviewFlags
|
|
3459
3739
|
);
|
|
3460
3740
|
const merged = new Map(target.entries);
|
|
3461
3741
|
for (const key of pruned) {
|
|
@@ -3481,16 +3761,20 @@ async function runLocale(params) {
|
|
|
3481
3761
|
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
3482
3762
|
const notices = [
|
|
3483
3763
|
...pluralNotices,
|
|
3484
|
-
...
|
|
3485
|
-
...generation.notices
|
|
3764
|
+
...translation.notices,
|
|
3765
|
+
...generation.notices,
|
|
3766
|
+
...budgetLocaleNotices(params.budget, startedStopped, translation.tripped, generation.tripped)
|
|
3486
3767
|
];
|
|
3487
3768
|
const withheld = /* @__PURE__ */ new Set([
|
|
3488
3769
|
...integrityMismatches,
|
|
3489
3770
|
...providerFailures,
|
|
3490
3771
|
...invalidIcuSource,
|
|
3491
3772
|
...generation.withheld,
|
|
3492
|
-
...generation.providerFailures
|
|
3773
|
+
...generation.providerFailures,
|
|
3774
|
+
// Source-present keys withheld by the budget guardrail must keep their prior lock hash too.
|
|
3775
|
+
...budgetWithheld
|
|
3493
3776
|
]);
|
|
3777
|
+
const localeUsage = combineUsage(translation.usage, generation.usage);
|
|
3494
3778
|
return {
|
|
3495
3779
|
summary: baseSummary({
|
|
3496
3780
|
locale: params.targetLocale,
|
|
@@ -3503,15 +3787,27 @@ async function runLocale(params) {
|
|
|
3503
3787
|
integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
|
|
3504
3788
|
// A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
|
|
3505
3789
|
providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
|
|
3790
|
+
budgetWithheld: [...budgetWithheld, ...generation.budgetWithheld].sort(),
|
|
3506
3791
|
pruned,
|
|
3507
|
-
notices
|
|
3792
|
+
notices,
|
|
3793
|
+
needsReview: needsReviewFor(accepted.keys(), reviewFlags),
|
|
3794
|
+
...localeUsage !== void 0 ? { usage: localeUsage } : {}
|
|
3508
3795
|
}),
|
|
3509
3796
|
lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
|
|
3510
3797
|
};
|
|
3511
3798
|
}
|
|
3799
|
+
var NO_GENERATION_RESULT = {
|
|
3800
|
+
accepted: [],
|
|
3801
|
+
withheld: [],
|
|
3802
|
+
providerFailures: [],
|
|
3803
|
+
budgetWithheld: [],
|
|
3804
|
+
notices: [],
|
|
3805
|
+
usage: void 0,
|
|
3806
|
+
tripped: false
|
|
3807
|
+
};
|
|
3512
3808
|
async function runGeneration(params, provider) {
|
|
3513
3809
|
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
3514
|
-
return
|
|
3810
|
+
return NO_GENERATION_RESULT;
|
|
3515
3811
|
}
|
|
3516
3812
|
return generatePluralForms({
|
|
3517
3813
|
source: params.source,
|
|
@@ -3523,9 +3819,13 @@ async function runGeneration(params, provider) {
|
|
|
3523
3819
|
glossary: params.glossary,
|
|
3524
3820
|
tone: params.tone,
|
|
3525
3821
|
baseline: params.baseline,
|
|
3526
|
-
maxBatchSize: params.maxBatchSize
|
|
3822
|
+
maxBatchSize: params.maxBatchSize,
|
|
3823
|
+
budget: params.budget
|
|
3527
3824
|
});
|
|
3528
3825
|
}
|
|
3826
|
+
function budgetLocaleNotices(budget, startedStopped, mainTripped, generationTripped) {
|
|
3827
|
+
return startedStopped || mainTripped || generationTripped ? [budgetExceededNotice(budget)] : [];
|
|
3828
|
+
}
|
|
3529
3829
|
function pluralNoticeFor(params, merged) {
|
|
3530
3830
|
if (params.format !== "i18next-json") {
|
|
3531
3831
|
return [];
|
|
@@ -3546,26 +3846,53 @@ function baseSummary(parts) {
|
|
|
3546
3846
|
invalidIcuSource: parts.invalidIcuSource,
|
|
3547
3847
|
integrityMismatches: parts.integrityMismatches,
|
|
3548
3848
|
providerFailures: parts.providerFailures,
|
|
3849
|
+
budgetWithheld: parts.budgetWithheld,
|
|
3549
3850
|
generated: parts.generated,
|
|
3550
|
-
notices: parts.notices
|
|
3851
|
+
notices: parts.notices,
|
|
3852
|
+
needsReview: parts.needsReview ?? [],
|
|
3853
|
+
...parts.usage !== void 0 ? { usage: parts.usage } : {}
|
|
3551
3854
|
};
|
|
3552
3855
|
}
|
|
3553
|
-
|
|
3856
|
+
function needsReviewFor(acceptedKeys, reviewFlags) {
|
|
3857
|
+
const entries = [];
|
|
3858
|
+
for (const key of acceptedKeys) {
|
|
3859
|
+
const flag = reviewFlags.get(key);
|
|
3860
|
+
if (flag !== void 0) {
|
|
3861
|
+
entries.push({ key, reasons: flag.reasons });
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
return entries.sort((a, b) => a.key < b.key ? -1 : 1);
|
|
3865
|
+
}
|
|
3866
|
+
async function translateAndCheck(provider, params, entries, accepted, integrityMismatches, providerFailures, budgetWithheld, reviewFlags) {
|
|
3554
3867
|
const notices = [];
|
|
3868
|
+
const usage = createUsageAccumulator();
|
|
3869
|
+
let tripped = false;
|
|
3555
3870
|
for (const batch of chunk(entries, params.maxBatchSize)) {
|
|
3556
|
-
|
|
3871
|
+
if (params.budget.stopped) {
|
|
3872
|
+
for (const entry of batch) {
|
|
3873
|
+
budgetWithheld.push(entry.key);
|
|
3874
|
+
}
|
|
3875
|
+
continue;
|
|
3876
|
+
}
|
|
3877
|
+
const subResult = await runSubBatch(
|
|
3557
3878
|
provider,
|
|
3558
3879
|
params,
|
|
3559
3880
|
batch,
|
|
3560
3881
|
accepted,
|
|
3561
3882
|
integrityMismatches,
|
|
3562
|
-
providerFailures
|
|
3883
|
+
providerFailures,
|
|
3884
|
+
reviewFlags
|
|
3563
3885
|
);
|
|
3564
|
-
notices.push(...
|
|
3886
|
+
notices.push(...subResult.notices);
|
|
3887
|
+
foldUsage(usage, subResult.usage);
|
|
3888
|
+
foldTrackerUsage(params.budget, subResult.usage);
|
|
3889
|
+
if (checkBudgetTrip(params.budget)) {
|
|
3890
|
+
tripped = true;
|
|
3891
|
+
}
|
|
3565
3892
|
}
|
|
3566
|
-
return notices;
|
|
3893
|
+
return { notices, tripped, usage: usage.total };
|
|
3567
3894
|
}
|
|
3568
|
-
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures) {
|
|
3895
|
+
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures, reviewFlags) {
|
|
3569
3896
|
let result;
|
|
3570
3897
|
try {
|
|
3571
3898
|
result = await provider.translateBatch(buildRequest3(params, batch));
|
|
@@ -3573,12 +3900,17 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3573
3900
|
for (const entry of batch) {
|
|
3574
3901
|
providerFailures.push(entry.key);
|
|
3575
3902
|
}
|
|
3576
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
3903
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3577
3904
|
}
|
|
3578
3905
|
for (const entry of batch) {
|
|
3579
3906
|
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
3580
3907
|
}
|
|
3581
|
-
|
|
3908
|
+
if (result.reviewFlags !== void 0) {
|
|
3909
|
+
for (const [key, flag] of result.reviewFlags) {
|
|
3910
|
+
reviewFlags.set(key, flag);
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3582
3914
|
}
|
|
3583
3915
|
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
3584
3916
|
const value = result.values.get(entry.key);
|
|
@@ -3636,6 +3968,10 @@ async function translate2(input, deps = {}) {
|
|
|
3636
3968
|
const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
|
|
3637
3969
|
const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
3638
3970
|
const fs = deps.fs ?? defaultFs;
|
|
3971
|
+
const budget = createBudgetTracker(
|
|
3972
|
+
config.maxTokens,
|
|
3973
|
+
config.budgetBehavior ?? DEFAULT_BUDGET_BEHAVIOR
|
|
3974
|
+
);
|
|
3639
3975
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3640
3976
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
3641
3977
|
const source = await readSource(config, cwd, fs, adapter);
|
|
@@ -3660,7 +3996,8 @@ async function translate2(input, deps = {}) {
|
|
|
3660
3996
|
prune,
|
|
3661
3997
|
generatePlurals,
|
|
3662
3998
|
maxBatchSize,
|
|
3663
|
-
fs
|
|
3999
|
+
fs,
|
|
4000
|
+
budget
|
|
3664
4001
|
};
|
|
3665
4002
|
const { summary, lockEntries } = await runLocale(params);
|
|
3666
4003
|
if (!dryRun) {
|
|
@@ -3673,7 +4010,19 @@ async function translate2(input, deps = {}) {
|
|
|
3673
4010
|
}
|
|
3674
4011
|
}
|
|
3675
4012
|
const { succeeded, failed } = partition(summaries);
|
|
3676
|
-
|
|
4013
|
+
const usage = summaries.reduce(
|
|
4014
|
+
(total, summary) => combineUsage(total, summary.usage),
|
|
4015
|
+
void 0
|
|
4016
|
+
);
|
|
4017
|
+
const budgetSummary = toBudgetSummary(budget);
|
|
4018
|
+
return {
|
|
4019
|
+
dryRun,
|
|
4020
|
+
locales: summaries,
|
|
4021
|
+
succeeded,
|
|
4022
|
+
failed,
|
|
4023
|
+
...usage !== void 0 ? { usage } : {},
|
|
4024
|
+
...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
|
|
4025
|
+
};
|
|
3677
4026
|
}
|
|
3678
4027
|
var ExchangeError = class extends Error {
|
|
3679
4028
|
code;
|
|
@@ -3714,7 +4063,9 @@ var COLUMN = {
|
|
|
3714
4063
|
status: 4,
|
|
3715
4064
|
translation: 5,
|
|
3716
4065
|
sourceHash: 6,
|
|
3717
|
-
context: 7
|
|
4066
|
+
context: 7,
|
|
4067
|
+
reviewStatus: 8,
|
|
4068
|
+
reviewReasons: 9
|
|
3718
4069
|
};
|
|
3719
4070
|
var HEADERS = [
|
|
3720
4071
|
"Key",
|
|
@@ -3723,7 +4074,9 @@ var HEADERS = [
|
|
|
3723
4074
|
"Status",
|
|
3724
4075
|
"Translation",
|
|
3725
4076
|
"Source hash",
|
|
3726
|
-
"Context"
|
|
4077
|
+
"Context",
|
|
4078
|
+
"Review status",
|
|
4079
|
+
"Review reasons"
|
|
3727
4080
|
];
|
|
3728
4081
|
var HEADER_ROW = 1;
|
|
3729
4082
|
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
@@ -3743,7 +4096,9 @@ var COLUMN_WIDTHS = {
|
|
|
3743
4096
|
[COLUMN.current]: 50,
|
|
3744
4097
|
[COLUMN.status]: 12,
|
|
3745
4098
|
[COLUMN.translation]: 50,
|
|
3746
|
-
[COLUMN.context]: 50
|
|
4099
|
+
[COLUMN.context]: 50,
|
|
4100
|
+
[COLUMN.reviewStatus]: 12,
|
|
4101
|
+
[COLUMN.reviewReasons]: 40
|
|
3747
4102
|
};
|
|
3748
4103
|
var TEXT_NUMBER_FORMAT = "@";
|
|
3749
4104
|
function styleHeader(sheet) {
|
|
@@ -3775,7 +4130,9 @@ function writeRow(sheet, sheetRow) {
|
|
|
3775
4130
|
translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
|
|
3776
4131
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
3777
4132
|
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
3778
|
-
|
|
4133
|
+
row.getCell(COLUMN.reviewStatus).value = sheetRow.reviewStatus;
|
|
4134
|
+
row.getCell(COLUMN.reviewReasons).value = sheetRow.reviewReasons;
|
|
4135
|
+
for (let column = COLUMN.key; column <= COLUMN.reviewReasons; column += 1) {
|
|
3779
4136
|
const cell = row.getCell(column);
|
|
3780
4137
|
cell.protection = { locked: column !== COLUMN.translation };
|
|
3781
4138
|
if (column !== COLUMN.translation) {
|
|
@@ -3950,7 +4307,11 @@ var rowSchema = zod.z.object({
|
|
|
3950
4307
|
status: zod.z.enum(["new", "changed", "unchanged"]),
|
|
3951
4308
|
sourceHash: zod.z.string(),
|
|
3952
4309
|
translation: zod.z.string(),
|
|
3953
|
-
context: zod.z.string()
|
|
4310
|
+
context: zod.z.string(),
|
|
4311
|
+
// A legacy workbook exported before these columns existed has no such cell; cellString then yields
|
|
4312
|
+
// "", and .catch maps it to "ok" / "" rather than rejecting the row.
|
|
4313
|
+
reviewStatus: zod.z.enum(["ok", "review"]).catch("ok"),
|
|
4314
|
+
reviewReasons: zod.z.string().catch("")
|
|
3954
4315
|
});
|
|
3955
4316
|
function cellString(cell) {
|
|
3956
4317
|
const value = cell.value;
|
|
@@ -3986,7 +4347,10 @@ function parseRow(sheet, row) {
|
|
|
3986
4347
|
translation: cellString(row.getCell(COLUMN.translation)),
|
|
3987
4348
|
// A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
|
|
3988
4349
|
// empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
|
|
3989
|
-
context: cellString(row.getCell(COLUMN.context))
|
|
4350
|
+
context: cellString(row.getCell(COLUMN.context)),
|
|
4351
|
+
// Same backward-compatible pattern for a workbook built before the Review columns existed.
|
|
4352
|
+
reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
|
|
4353
|
+
reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
|
|
3990
4354
|
};
|
|
3991
4355
|
const result = rowSchema.safeParse(candidate);
|
|
3992
4356
|
if (!result.success) {
|
|
@@ -4060,7 +4424,34 @@ async function readTarget3(cwd, config, adapter, fs, locale) {
|
|
|
4060
4424
|
}
|
|
4061
4425
|
return (await adapter.read(path, locale)).resource;
|
|
4062
4426
|
}
|
|
4063
|
-
function
|
|
4427
|
+
function reasonLabel(reason) {
|
|
4428
|
+
return reason.toLowerCase().replace(/_/g, "-");
|
|
4429
|
+
}
|
|
4430
|
+
function reviewColumns(flag) {
|
|
4431
|
+
if (flag === void 0) {
|
|
4432
|
+
return { reviewStatus: "ok", reviewReasons: "" };
|
|
4433
|
+
}
|
|
4434
|
+
return { reviewStatus: "review", reviewReasons: flag.reasons.map(reasonLabel).join(", ") };
|
|
4435
|
+
}
|
|
4436
|
+
function computeRowReview(adapter, sourceValue, currentTarget, sourceLocale, targetLocale, glossary) {
|
|
4437
|
+
if (currentTarget === "") {
|
|
4438
|
+
return { reviewStatus: "ok", reviewReasons: "" };
|
|
4439
|
+
}
|
|
4440
|
+
const integrity = adapter.comparePlaceholders?.(sourceValue, currentTarget) ?? checkPlaceholders(
|
|
4441
|
+
adapter.extractPlaceholders(sourceValue),
|
|
4442
|
+
adapter.extractPlaceholders(currentTarget)
|
|
4443
|
+
);
|
|
4444
|
+
const flag = computeReviewFlags({
|
|
4445
|
+
sourceValue,
|
|
4446
|
+
translatedValue: currentTarget,
|
|
4447
|
+
sourceLocale,
|
|
4448
|
+
targetLocale,
|
|
4449
|
+
integrity,
|
|
4450
|
+
glossary
|
|
4451
|
+
});
|
|
4452
|
+
return reviewColumns(flag);
|
|
4453
|
+
}
|
|
4454
|
+
function buildRows(source, target, baseline, includeUnchanged, adapter, glossary) {
|
|
4064
4455
|
const diff2 = diffResources(source, target, { baseline });
|
|
4065
4456
|
const rows = [];
|
|
4066
4457
|
const add = (keys, status) => {
|
|
@@ -4069,14 +4460,23 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
4069
4460
|
if (sourceEntry === void 0) {
|
|
4070
4461
|
continue;
|
|
4071
4462
|
}
|
|
4463
|
+
const currentTarget = target.entries.get(key)?.value ?? "";
|
|
4072
4464
|
rows.push({
|
|
4073
4465
|
key,
|
|
4074
4466
|
source: sourceEntry.value,
|
|
4075
|
-
currentTarget
|
|
4467
|
+
currentTarget,
|
|
4076
4468
|
status,
|
|
4077
4469
|
sourceHash: contentHash(sourceEntry),
|
|
4078
4470
|
translation: "",
|
|
4079
|
-
context: sourceEntry.description ?? ""
|
|
4471
|
+
context: sourceEntry.description ?? "",
|
|
4472
|
+
...computeRowReview(
|
|
4473
|
+
adapter,
|
|
4474
|
+
sourceEntry.value,
|
|
4475
|
+
currentTarget,
|
|
4476
|
+
source.locale,
|
|
4477
|
+
target.locale,
|
|
4478
|
+
glossary
|
|
4479
|
+
)
|
|
4080
4480
|
});
|
|
4081
4481
|
}
|
|
4082
4482
|
};
|
|
@@ -4102,7 +4502,9 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
4102
4502
|
source.resource,
|
|
4103
4503
|
target,
|
|
4104
4504
|
baselineFor(lock, locale),
|
|
4105
|
-
input.includeUnchanged ?? false
|
|
4505
|
+
input.includeUnchanged ?? false,
|
|
4506
|
+
adapter,
|
|
4507
|
+
config.glossary
|
|
4106
4508
|
);
|
|
4107
4509
|
return { locale, rows };
|
|
4108
4510
|
})
|
|
@@ -4203,9 +4605,14 @@ function importLocale(params) {
|
|
|
4203
4605
|
integrityMismatches: [...buckets.mismatches].sort(),
|
|
4204
4606
|
// A workbook import never calls a provider, so a provider-call failure cannot occur here.
|
|
4205
4607
|
providerFailures: [],
|
|
4608
|
+
// A workbook import never calls a provider, so the budget guardrail never withholds anything here.
|
|
4609
|
+
budgetWithheld: [],
|
|
4206
4610
|
// Plural generation is a translate-flow concern; the manual workbook import never generates forms.
|
|
4207
4611
|
generated: [],
|
|
4208
|
-
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : []
|
|
4612
|
+
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
|
|
4613
|
+
// A workbook import never calls a provider and never recomputes review flags on its own path; the
|
|
4614
|
+
// Excel workbook's Review status/reasons columns are informational only (see export-workbook.ts).
|
|
4615
|
+
needsReview: []
|
|
4209
4616
|
};
|
|
4210
4617
|
return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
|
|
4211
4618
|
}
|