@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.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));
|
|
@@ -922,7 +1018,34 @@ async function translate(bundle, config, request) {
|
|
|
922
1018
|
if (unprotectable.length > 0) {
|
|
923
1019
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
924
1020
|
}
|
|
925
|
-
|
|
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;
|
|
926
1049
|
}
|
|
927
1050
|
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
928
1051
|
if (protectable.length === 0) {
|
|
@@ -1426,6 +1549,7 @@ function buildProvider(config) {
|
|
|
1426
1549
|
|
|
1427
1550
|
// src/config/schema.ts
|
|
1428
1551
|
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1552
|
+
var DEFAULT_BUDGET_BEHAVIOR = "warn";
|
|
1429
1553
|
function findCaseInsensitiveDuplicate(locales) {
|
|
1430
1554
|
const seen = /* @__PURE__ */ new Set();
|
|
1431
1555
|
for (const locale of locales) {
|
|
@@ -1474,7 +1598,23 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
1474
1598
|
* be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
|
|
1475
1599
|
* {@link DEFAULT_MAX_BATCH_SIZE} applies.
|
|
1476
1600
|
*/
|
|
1477
|
-
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()
|
|
1478
1618
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
1479
1619
|
message: "targetLocales must not include the source locale",
|
|
1480
1620
|
path: ["targetLocales"]
|
|
@@ -1908,7 +2048,7 @@ var AdapterError = class extends Error {
|
|
|
1908
2048
|
var MAX_DEPTH = 100;
|
|
1909
2049
|
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1910
2050
|
var jsonTreeSchema = z.lazy(
|
|
1911
|
-
() => 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)])
|
|
1912
2052
|
);
|
|
1913
2053
|
var rootSchema = z.record(z.string(), jsonTreeSchema);
|
|
1914
2054
|
function assertWithinDepth(value, max) {
|
|
@@ -1936,7 +2076,7 @@ function assertJsonRecord(value) {
|
|
|
1936
2076
|
if (!result.success) {
|
|
1937
2077
|
throw new AdapterError(
|
|
1938
2078
|
"INVALID_STRUCTURE",
|
|
1939
|
-
"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)."
|
|
1940
2080
|
);
|
|
1941
2081
|
}
|
|
1942
2082
|
return result.data;
|
|
@@ -1954,6 +2094,9 @@ function serializeJsonTree(tree) {
|
|
|
1954
2094
|
return `${JSON.stringify(tree, null, 2)}
|
|
1955
2095
|
`;
|
|
1956
2096
|
}
|
|
2097
|
+
function isJsonNode(value) {
|
|
2098
|
+
return typeof value === "object" && value !== null;
|
|
2099
|
+
}
|
|
1957
2100
|
function namespaceOf(filePath) {
|
|
1958
2101
|
return basename(filePath, extname(filePath));
|
|
1959
2102
|
}
|
|
@@ -1981,9 +2124,25 @@ function buildCanHandle(extensions, sniff) {
|
|
|
1981
2124
|
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
1982
2125
|
};
|
|
1983
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
|
+
}
|
|
1984
2141
|
var nodeOps = {
|
|
1985
2142
|
writeFile: (path, data) => writeFile(path, data, "utf8"),
|
|
2143
|
+
fsyncFile: (path) => fsyncPath(path),
|
|
1986
2144
|
rename: (from, to) => rename(from, to),
|
|
2145
|
+
fsyncDir: (path) => fsyncDirBestEffort(path),
|
|
1987
2146
|
rm: (path) => rm(path, { force: true })
|
|
1988
2147
|
};
|
|
1989
2148
|
async function cleanup(ops, tmp) {
|
|
@@ -1999,11 +2158,16 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1999
2158
|
const tmp = tempFileName2(path);
|
|
2000
2159
|
try {
|
|
2001
2160
|
await ops.writeFile(tmp, data);
|
|
2161
|
+
await ops.fsyncFile(tmp);
|
|
2002
2162
|
await ops.rename(tmp, path);
|
|
2003
2163
|
} catch (error) {
|
|
2004
2164
|
await cleanup(ops, tmp);
|
|
2005
2165
|
throw error;
|
|
2006
2166
|
}
|
|
2167
|
+
try {
|
|
2168
|
+
await ops.fsyncDir(dirname(path));
|
|
2169
|
+
} catch {
|
|
2170
|
+
}
|
|
2007
2171
|
}
|
|
2008
2172
|
function stripLeadingBom(content) {
|
|
2009
2173
|
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
@@ -2125,20 +2289,24 @@ function addLeaf(ctx, segments, key, value) {
|
|
|
2125
2289
|
);
|
|
2126
2290
|
}
|
|
2127
2291
|
ctx.claimed.set(effectivePath, mapKey);
|
|
2292
|
+
if (typeof value !== "string") {
|
|
2293
|
+
ctx.excluded.push(mapKey);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2128
2296
|
const { placeholders, isPlural } = ctx.derive(key, value);
|
|
2129
2297
|
ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
|
|
2130
2298
|
}
|
|
2131
2299
|
function addEntries(ctx, prefix, node) {
|
|
2132
2300
|
for (const [key, value] of Object.entries(node)) {
|
|
2133
2301
|
const segments = [...prefix, key];
|
|
2134
|
-
if (
|
|
2135
|
-
addLeaf(ctx, segments, key, value);
|
|
2136
|
-
} else {
|
|
2302
|
+
if (isJsonNode(value)) {
|
|
2137
2303
|
addEntries(ctx, segments, value);
|
|
2304
|
+
} else {
|
|
2305
|
+
addLeaf(ctx, segments, key, value);
|
|
2138
2306
|
}
|
|
2139
2307
|
}
|
|
2140
2308
|
}
|
|
2141
|
-
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
2309
|
+
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths, excluded) {
|
|
2142
2310
|
for (const [key, value] of Object.entries(node)) {
|
|
2143
2311
|
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
2144
2312
|
if (claimedPaths.has(path)) {
|
|
@@ -2148,22 +2316,26 @@ function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
|
2148
2316
|
);
|
|
2149
2317
|
}
|
|
2150
2318
|
claimedPaths.add(path);
|
|
2151
|
-
if (
|
|
2319
|
+
if (isJsonNode(value)) {
|
|
2320
|
+
addPathEntries(value, path, namespace, derive, out, claimedPaths, excluded);
|
|
2321
|
+
} else if (typeof value === "string") {
|
|
2152
2322
|
const { placeholders, isPlural } = derive(key, value);
|
|
2153
2323
|
out.set(path, { key: path, namespace, value, placeholders, isPlural });
|
|
2154
2324
|
} else {
|
|
2155
|
-
|
|
2325
|
+
excluded.push(path);
|
|
2156
2326
|
}
|
|
2157
2327
|
}
|
|
2158
2328
|
}
|
|
2159
2329
|
function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
|
|
2160
2330
|
const out = /* @__PURE__ */ new Map();
|
|
2161
2331
|
if (keyMode === "path-notation") {
|
|
2162
|
-
|
|
2163
|
-
|
|
2332
|
+
const excluded = [];
|
|
2333
|
+
addPathEntries(tree, "", namespace, derive, out, /* @__PURE__ */ new Set(), excluded);
|
|
2334
|
+
return { entries: out, excludedLeafPaths: excluded };
|
|
2164
2335
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2336
|
+
const ctx = { namespace, derive, out, claimed: /* @__PURE__ */ new Map(), excluded: [] };
|
|
2337
|
+
addEntries(ctx, [], tree);
|
|
2338
|
+
return { entries: out, excludedLeafPaths: ctx.excluded };
|
|
2167
2339
|
}
|
|
2168
2340
|
function emptyNode() {
|
|
2169
2341
|
return /* @__PURE__ */ Object.create(null);
|
|
@@ -2216,9 +2388,9 @@ function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTre
|
|
|
2216
2388
|
try {
|
|
2217
2389
|
const tree = parse3(content);
|
|
2218
2390
|
validateTree?.(tree);
|
|
2219
|
-
const
|
|
2220
|
-
mergeDescriptions(entries, content, deriveDescriptions);
|
|
2221
|
-
return
|
|
2391
|
+
const result = flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
2392
|
+
mergeDescriptions(result.entries, content, deriveDescriptions);
|
|
2393
|
+
return result;
|
|
2222
2394
|
} catch (error) {
|
|
2223
2395
|
rethrowStructured(error, "The file could not be parsed.");
|
|
2224
2396
|
}
|
|
@@ -2249,7 +2421,7 @@ function createTreeFileAdapter(options) {
|
|
|
2249
2421
|
async read(filePath, locale) {
|
|
2250
2422
|
const content = await readFileContent(filePath);
|
|
2251
2423
|
const namespace = namespaceOf(filePath);
|
|
2252
|
-
const entries = toEntries(
|
|
2424
|
+
const { entries, excludedLeafPaths } = toEntries(
|
|
2253
2425
|
content,
|
|
2254
2426
|
namespace,
|
|
2255
2427
|
parse3,
|
|
@@ -2260,7 +2432,7 @@ function createTreeFileAdapter(options) {
|
|
|
2260
2432
|
);
|
|
2261
2433
|
const resource = { locale, namespace, format, entries };
|
|
2262
2434
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2263
|
-
return { resource, invalidIcuKeys };
|
|
2435
|
+
return { resource, invalidIcuKeys, excludedLeafPaths };
|
|
2264
2436
|
},
|
|
2265
2437
|
async write(resource, filePath) {
|
|
2266
2438
|
const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
|
|
@@ -2354,8 +2526,10 @@ async function buildArbWriteTree(entries, filePath) {
|
|
|
2354
2526
|
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
2355
2527
|
if (translated !== void 0) {
|
|
2356
2528
|
consumed.add(key);
|
|
2529
|
+
out[key] = translated;
|
|
2530
|
+
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
2531
|
+
out[key] = value;
|
|
2357
2532
|
}
|
|
2358
|
-
out[key] = translated ?? value;
|
|
2359
2533
|
}
|
|
2360
2534
|
for (const [key, value] of messages) {
|
|
2361
2535
|
if (!consumed.has(key)) {
|
|
@@ -2449,7 +2623,7 @@ function createNextIntlJsonAdapter() {
|
|
|
2449
2623
|
}
|
|
2450
2624
|
function assertNoDottedNestedKey(tree) {
|
|
2451
2625
|
for (const [key, value] of Object.entries(tree)) {
|
|
2452
|
-
if (
|
|
2626
|
+
if (!isJsonNode(value)) {
|
|
2453
2627
|
continue;
|
|
2454
2628
|
}
|
|
2455
2629
|
if (key.includes(".")) {
|
|
@@ -2465,7 +2639,7 @@ function assertNotMixed(tree) {
|
|
|
2465
2639
|
let hasNested = false;
|
|
2466
2640
|
let hasFlatDottedKey = false;
|
|
2467
2641
|
for (const [key, value] of Object.entries(tree)) {
|
|
2468
|
-
if (
|
|
2642
|
+
if (isJsonNode(value)) {
|
|
2469
2643
|
hasNested = true;
|
|
2470
2644
|
} else if (key.includes(".")) {
|
|
2471
2645
|
hasFlatDottedKey = true;
|
|
@@ -2626,7 +2800,7 @@ function createFlatFileAdapter(options) {
|
|
|
2626
2800
|
const entries = toEntries2(content, namespace, parseEntries);
|
|
2627
2801
|
const resource = { locale, namespace, format, entries };
|
|
2628
2802
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2629
|
-
return { resource, invalidIcuKeys };
|
|
2803
|
+
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
2630
2804
|
},
|
|
2631
2805
|
async write(resource, filePath) {
|
|
2632
2806
|
const data = await serializeEntries(resource.entries, filePath);
|
|
@@ -3080,6 +3254,46 @@ function selectProvider(config, createProvider = buildProvider) {
|
|
|
3080
3254
|
}
|
|
3081
3255
|
}
|
|
3082
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
|
+
|
|
3083
3297
|
// src/flow/locale-failure.ts
|
|
3084
3298
|
function describeError(error) {
|
|
3085
3299
|
if (error instanceof Error) {
|
|
@@ -3099,8 +3313,10 @@ function failureSummary(locale, error) {
|
|
|
3099
3313
|
invalidIcuSource: [],
|
|
3100
3314
|
integrityMismatches: [],
|
|
3101
3315
|
providerFailures: [],
|
|
3316
|
+
budgetWithheld: [],
|
|
3102
3317
|
generated: [],
|
|
3103
3318
|
notices: [],
|
|
3319
|
+
needsReview: [],
|
|
3104
3320
|
error: describeError(error)
|
|
3105
3321
|
};
|
|
3106
3322
|
}
|
|
@@ -3270,7 +3486,43 @@ function planPluralGeneration(source, targetLocale, format) {
|
|
|
3270
3486
|
return { items };
|
|
3271
3487
|
}
|
|
3272
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
|
+
|
|
3273
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
|
+
};
|
|
3274
3526
|
function generatedLockHash(governingEntries, category) {
|
|
3275
3527
|
const governingHashes = governingEntries.map(contentHash).sort();
|
|
3276
3528
|
return contentHash({
|
|
@@ -3309,23 +3561,45 @@ async function generatePluralForms(context) {
|
|
|
3309
3561
|
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
3310
3562
|
const stale = staleItems(plan.items, context.baseline);
|
|
3311
3563
|
if (stale.length === 0) {
|
|
3312
|
-
return
|
|
3564
|
+
return EMPTY_RESULT;
|
|
3313
3565
|
}
|
|
3314
3566
|
const accepted = [];
|
|
3315
3567
|
const withheld = [];
|
|
3316
3568
|
const providerFailures = [];
|
|
3569
|
+
const budgetWithheld = [];
|
|
3317
3570
|
const notices = [];
|
|
3571
|
+
const usage = createUsageAccumulator();
|
|
3572
|
+
let tripped = false;
|
|
3318
3573
|
for (const batch of chunk(stale, context.maxBatchSize)) {
|
|
3319
|
-
|
|
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(
|
|
3320
3581
|
context,
|
|
3321
3582
|
batch,
|
|
3322
3583
|
accepted,
|
|
3323
3584
|
withheld,
|
|
3324
3585
|
providerFailures
|
|
3325
3586
|
);
|
|
3326
|
-
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
|
+
}
|
|
3327
3593
|
}
|
|
3328
|
-
return {
|
|
3594
|
+
return {
|
|
3595
|
+
accepted,
|
|
3596
|
+
withheld,
|
|
3597
|
+
providerFailures,
|
|
3598
|
+
budgetWithheld,
|
|
3599
|
+
notices,
|
|
3600
|
+
usage: usage.total,
|
|
3601
|
+
tripped
|
|
3602
|
+
};
|
|
3329
3603
|
}
|
|
3330
3604
|
async function runGenerationSubBatch(context, batch, accepted, withheld, providerFailures) {
|
|
3331
3605
|
let result;
|
|
@@ -3336,12 +3610,12 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3336
3610
|
for (const item of batch) {
|
|
3337
3611
|
providerFailures.push(item.targetKey);
|
|
3338
3612
|
}
|
|
3339
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
3613
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3340
3614
|
}
|
|
3341
3615
|
for (const item of batch) {
|
|
3342
3616
|
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
3343
3617
|
}
|
|
3344
|
-
return readNotices(result);
|
|
3618
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3345
3619
|
}
|
|
3346
3620
|
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
3347
3621
|
const value = result.values.get(item.targetKey);
|
|
@@ -3409,6 +3683,7 @@ async function runLocale(params) {
|
|
|
3409
3683
|
generated: [],
|
|
3410
3684
|
integrityMismatches: [],
|
|
3411
3685
|
providerFailures: [],
|
|
3686
|
+
budgetWithheld: [],
|
|
3412
3687
|
pruned,
|
|
3413
3688
|
notices: sdkNotices
|
|
3414
3689
|
}),
|
|
@@ -3416,16 +3691,21 @@ async function runLocale(params) {
|
|
|
3416
3691
|
};
|
|
3417
3692
|
}
|
|
3418
3693
|
const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
|
|
3694
|
+
const startedStopped = params.budget.stopped;
|
|
3419
3695
|
const accepted = /* @__PURE__ */ new Map();
|
|
3420
3696
|
const integrityMismatches = [];
|
|
3421
3697
|
const providerFailures = [];
|
|
3422
|
-
const
|
|
3698
|
+
const budgetWithheld = [];
|
|
3699
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
3700
|
+
const translation = await translateAndCheck(
|
|
3423
3701
|
provider,
|
|
3424
3702
|
params,
|
|
3425
3703
|
entries,
|
|
3426
3704
|
accepted,
|
|
3427
3705
|
integrityMismatches,
|
|
3428
|
-
providerFailures
|
|
3706
|
+
providerFailures,
|
|
3707
|
+
budgetWithheld,
|
|
3708
|
+
reviewFlags
|
|
3429
3709
|
);
|
|
3430
3710
|
const merged = new Map(target.entries);
|
|
3431
3711
|
for (const key of pruned) {
|
|
@@ -3451,16 +3731,20 @@ async function runLocale(params) {
|
|
|
3451
3731
|
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
3452
3732
|
const notices = [
|
|
3453
3733
|
...pluralNotices,
|
|
3454
|
-
...
|
|
3455
|
-
...generation.notices
|
|
3734
|
+
...translation.notices,
|
|
3735
|
+
...generation.notices,
|
|
3736
|
+
...budgetLocaleNotices(params.budget, startedStopped, translation.tripped, generation.tripped)
|
|
3456
3737
|
];
|
|
3457
3738
|
const withheld = /* @__PURE__ */ new Set([
|
|
3458
3739
|
...integrityMismatches,
|
|
3459
3740
|
...providerFailures,
|
|
3460
3741
|
...invalidIcuSource,
|
|
3461
3742
|
...generation.withheld,
|
|
3462
|
-
...generation.providerFailures
|
|
3743
|
+
...generation.providerFailures,
|
|
3744
|
+
// Source-present keys withheld by the budget guardrail must keep their prior lock hash too.
|
|
3745
|
+
...budgetWithheld
|
|
3463
3746
|
]);
|
|
3747
|
+
const localeUsage = combineUsage(translation.usage, generation.usage);
|
|
3464
3748
|
return {
|
|
3465
3749
|
summary: baseSummary({
|
|
3466
3750
|
locale: params.targetLocale,
|
|
@@ -3473,15 +3757,27 @@ async function runLocale(params) {
|
|
|
3473
3757
|
integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
|
|
3474
3758
|
// A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
|
|
3475
3759
|
providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
|
|
3760
|
+
budgetWithheld: [...budgetWithheld, ...generation.budgetWithheld].sort(),
|
|
3476
3761
|
pruned,
|
|
3477
|
-
notices
|
|
3762
|
+
notices,
|
|
3763
|
+
needsReview: needsReviewFor(accepted.keys(), reviewFlags),
|
|
3764
|
+
...localeUsage !== void 0 ? { usage: localeUsage } : {}
|
|
3478
3765
|
}),
|
|
3479
3766
|
lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
|
|
3480
3767
|
};
|
|
3481
3768
|
}
|
|
3769
|
+
var NO_GENERATION_RESULT = {
|
|
3770
|
+
accepted: [],
|
|
3771
|
+
withheld: [],
|
|
3772
|
+
providerFailures: [],
|
|
3773
|
+
budgetWithheld: [],
|
|
3774
|
+
notices: [],
|
|
3775
|
+
usage: void 0,
|
|
3776
|
+
tripped: false
|
|
3777
|
+
};
|
|
3482
3778
|
async function runGeneration(params, provider) {
|
|
3483
3779
|
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
3484
|
-
return
|
|
3780
|
+
return NO_GENERATION_RESULT;
|
|
3485
3781
|
}
|
|
3486
3782
|
return generatePluralForms({
|
|
3487
3783
|
source: params.source,
|
|
@@ -3493,9 +3789,13 @@ async function runGeneration(params, provider) {
|
|
|
3493
3789
|
glossary: params.glossary,
|
|
3494
3790
|
tone: params.tone,
|
|
3495
3791
|
baseline: params.baseline,
|
|
3496
|
-
maxBatchSize: params.maxBatchSize
|
|
3792
|
+
maxBatchSize: params.maxBatchSize,
|
|
3793
|
+
budget: params.budget
|
|
3497
3794
|
});
|
|
3498
3795
|
}
|
|
3796
|
+
function budgetLocaleNotices(budget, startedStopped, mainTripped, generationTripped) {
|
|
3797
|
+
return startedStopped || mainTripped || generationTripped ? [budgetExceededNotice(budget)] : [];
|
|
3798
|
+
}
|
|
3499
3799
|
function pluralNoticeFor(params, merged) {
|
|
3500
3800
|
if (params.format !== "i18next-json") {
|
|
3501
3801
|
return [];
|
|
@@ -3516,26 +3816,53 @@ function baseSummary(parts) {
|
|
|
3516
3816
|
invalidIcuSource: parts.invalidIcuSource,
|
|
3517
3817
|
integrityMismatches: parts.integrityMismatches,
|
|
3518
3818
|
providerFailures: parts.providerFailures,
|
|
3819
|
+
budgetWithheld: parts.budgetWithheld,
|
|
3519
3820
|
generated: parts.generated,
|
|
3520
|
-
notices: parts.notices
|
|
3821
|
+
notices: parts.notices,
|
|
3822
|
+
needsReview: parts.needsReview ?? [],
|
|
3823
|
+
...parts.usage !== void 0 ? { usage: parts.usage } : {}
|
|
3521
3824
|
};
|
|
3522
3825
|
}
|
|
3523
|
-
|
|
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) {
|
|
3524
3837
|
const notices = [];
|
|
3838
|
+
const usage = createUsageAccumulator();
|
|
3839
|
+
let tripped = false;
|
|
3525
3840
|
for (const batch of chunk(entries, params.maxBatchSize)) {
|
|
3526
|
-
|
|
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(
|
|
3527
3848
|
provider,
|
|
3528
3849
|
params,
|
|
3529
3850
|
batch,
|
|
3530
3851
|
accepted,
|
|
3531
3852
|
integrityMismatches,
|
|
3532
|
-
providerFailures
|
|
3853
|
+
providerFailures,
|
|
3854
|
+
reviewFlags
|
|
3533
3855
|
);
|
|
3534
|
-
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
|
+
}
|
|
3535
3862
|
}
|
|
3536
|
-
return notices;
|
|
3863
|
+
return { notices, tripped, usage: usage.total };
|
|
3537
3864
|
}
|
|
3538
|
-
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures) {
|
|
3865
|
+
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures, reviewFlags) {
|
|
3539
3866
|
let result;
|
|
3540
3867
|
try {
|
|
3541
3868
|
result = await provider.translateBatch(buildRequest3(params, batch));
|
|
@@ -3543,12 +3870,17 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3543
3870
|
for (const entry of batch) {
|
|
3544
3871
|
providerFailures.push(entry.key);
|
|
3545
3872
|
}
|
|
3546
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
3873
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3547
3874
|
}
|
|
3548
3875
|
for (const entry of batch) {
|
|
3549
3876
|
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
3550
3877
|
}
|
|
3551
|
-
|
|
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 };
|
|
3552
3884
|
}
|
|
3553
3885
|
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
3554
3886
|
const value = result.values.get(entry.key);
|
|
@@ -3606,6 +3938,10 @@ async function translate2(input, deps = {}) {
|
|
|
3606
3938
|
const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
|
|
3607
3939
|
const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
3608
3940
|
const fs = deps.fs ?? defaultFs;
|
|
3941
|
+
const budget = createBudgetTracker(
|
|
3942
|
+
config.maxTokens,
|
|
3943
|
+
config.budgetBehavior ?? DEFAULT_BUDGET_BEHAVIOR
|
|
3944
|
+
);
|
|
3609
3945
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3610
3946
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
3611
3947
|
const source = await readSource(config, cwd, fs, adapter);
|
|
@@ -3630,7 +3966,8 @@ async function translate2(input, deps = {}) {
|
|
|
3630
3966
|
prune,
|
|
3631
3967
|
generatePlurals,
|
|
3632
3968
|
maxBatchSize,
|
|
3633
|
-
fs
|
|
3969
|
+
fs,
|
|
3970
|
+
budget
|
|
3634
3971
|
};
|
|
3635
3972
|
const { summary, lockEntries } = await runLocale(params);
|
|
3636
3973
|
if (!dryRun) {
|
|
@@ -3643,7 +3980,19 @@ async function translate2(input, deps = {}) {
|
|
|
3643
3980
|
}
|
|
3644
3981
|
}
|
|
3645
3982
|
const { succeeded, failed } = partition(summaries);
|
|
3646
|
-
|
|
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
|
+
};
|
|
3647
3996
|
}
|
|
3648
3997
|
var ExchangeError = class extends Error {
|
|
3649
3998
|
code;
|
|
@@ -3684,7 +4033,9 @@ var COLUMN = {
|
|
|
3684
4033
|
status: 4,
|
|
3685
4034
|
translation: 5,
|
|
3686
4035
|
sourceHash: 6,
|
|
3687
|
-
context: 7
|
|
4036
|
+
context: 7,
|
|
4037
|
+
reviewStatus: 8,
|
|
4038
|
+
reviewReasons: 9
|
|
3688
4039
|
};
|
|
3689
4040
|
var HEADERS = [
|
|
3690
4041
|
"Key",
|
|
@@ -3693,7 +4044,9 @@ var HEADERS = [
|
|
|
3693
4044
|
"Status",
|
|
3694
4045
|
"Translation",
|
|
3695
4046
|
"Source hash",
|
|
3696
|
-
"Context"
|
|
4047
|
+
"Context",
|
|
4048
|
+
"Review status",
|
|
4049
|
+
"Review reasons"
|
|
3697
4050
|
];
|
|
3698
4051
|
var HEADER_ROW = 1;
|
|
3699
4052
|
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
@@ -3713,7 +4066,9 @@ var COLUMN_WIDTHS = {
|
|
|
3713
4066
|
[COLUMN.current]: 50,
|
|
3714
4067
|
[COLUMN.status]: 12,
|
|
3715
4068
|
[COLUMN.translation]: 50,
|
|
3716
|
-
[COLUMN.context]: 50
|
|
4069
|
+
[COLUMN.context]: 50,
|
|
4070
|
+
[COLUMN.reviewStatus]: 12,
|
|
4071
|
+
[COLUMN.reviewReasons]: 40
|
|
3717
4072
|
};
|
|
3718
4073
|
var TEXT_NUMBER_FORMAT = "@";
|
|
3719
4074
|
function styleHeader(sheet) {
|
|
@@ -3745,7 +4100,9 @@ function writeRow(sheet, sheetRow) {
|
|
|
3745
4100
|
translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
|
|
3746
4101
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
3747
4102
|
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
3748
|
-
|
|
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) {
|
|
3749
4106
|
const cell = row.getCell(column);
|
|
3750
4107
|
cell.protection = { locked: column !== COLUMN.translation };
|
|
3751
4108
|
if (column !== COLUMN.translation) {
|
|
@@ -3920,7 +4277,11 @@ var rowSchema = z.object({
|
|
|
3920
4277
|
status: z.enum(["new", "changed", "unchanged"]),
|
|
3921
4278
|
sourceHash: z.string(),
|
|
3922
4279
|
translation: z.string(),
|
|
3923
|
-
context: 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("")
|
|
3924
4285
|
});
|
|
3925
4286
|
function cellString(cell) {
|
|
3926
4287
|
const value = cell.value;
|
|
@@ -3956,7 +4317,10 @@ function parseRow(sheet, row) {
|
|
|
3956
4317
|
translation: cellString(row.getCell(COLUMN.translation)),
|
|
3957
4318
|
// A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
|
|
3958
4319
|
// empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
|
|
3959
|
-
context: cellString(row.getCell(COLUMN.context))
|
|
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))
|
|
3960
4324
|
};
|
|
3961
4325
|
const result = rowSchema.safeParse(candidate);
|
|
3962
4326
|
if (!result.success) {
|
|
@@ -4030,7 +4394,34 @@ async function readTarget3(cwd, config, adapter, fs, locale) {
|
|
|
4030
4394
|
}
|
|
4031
4395
|
return (await adapter.read(path, locale)).resource;
|
|
4032
4396
|
}
|
|
4033
|
-
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) {
|
|
4034
4425
|
const diff2 = diffResources(source, target, { baseline });
|
|
4035
4426
|
const rows = [];
|
|
4036
4427
|
const add = (keys, status) => {
|
|
@@ -4039,14 +4430,23 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
4039
4430
|
if (sourceEntry === void 0) {
|
|
4040
4431
|
continue;
|
|
4041
4432
|
}
|
|
4433
|
+
const currentTarget = target.entries.get(key)?.value ?? "";
|
|
4042
4434
|
rows.push({
|
|
4043
4435
|
key,
|
|
4044
4436
|
source: sourceEntry.value,
|
|
4045
|
-
currentTarget
|
|
4437
|
+
currentTarget,
|
|
4046
4438
|
status,
|
|
4047
4439
|
sourceHash: contentHash(sourceEntry),
|
|
4048
4440
|
translation: "",
|
|
4049
|
-
context: sourceEntry.description ?? ""
|
|
4441
|
+
context: sourceEntry.description ?? "",
|
|
4442
|
+
...computeRowReview(
|
|
4443
|
+
adapter,
|
|
4444
|
+
sourceEntry.value,
|
|
4445
|
+
currentTarget,
|
|
4446
|
+
source.locale,
|
|
4447
|
+
target.locale,
|
|
4448
|
+
glossary
|
|
4449
|
+
)
|
|
4050
4450
|
});
|
|
4051
4451
|
}
|
|
4052
4452
|
};
|
|
@@ -4072,7 +4472,9 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
4072
4472
|
source.resource,
|
|
4073
4473
|
target,
|
|
4074
4474
|
baselineFor(lock, locale),
|
|
4075
|
-
input.includeUnchanged ?? false
|
|
4475
|
+
input.includeUnchanged ?? false,
|
|
4476
|
+
adapter,
|
|
4477
|
+
config.glossary
|
|
4076
4478
|
);
|
|
4077
4479
|
return { locale, rows };
|
|
4078
4480
|
})
|
|
@@ -4173,9 +4575,14 @@ function importLocale(params) {
|
|
|
4173
4575
|
integrityMismatches: [...buckets.mismatches].sort(),
|
|
4174
4576
|
// A workbook import never calls a provider, so a provider-call failure cannot occur here.
|
|
4175
4577
|
providerFailures: [],
|
|
4578
|
+
// A workbook import never calls a provider, so the budget guardrail never withholds anything here.
|
|
4579
|
+
budgetWithheld: [],
|
|
4176
4580
|
// Plural generation is a translate-flow concern; the manual workbook import never generates forms.
|
|
4177
4581
|
generated: [],
|
|
4178
|
-
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: []
|
|
4179
4586
|
};
|
|
4180
4587
|
return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
|
|
4181
4588
|
}
|