@verbatra/sdk 0.5.0-next.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/index.cjs +1246 -238
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +573 -45
- package/dist/index.d.ts +573 -45
- package/dist/index.js +1238 -237
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -145,6 +145,24 @@ async function atomicWrite(path, data) {
|
|
|
145
145
|
throw error;
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
async function createExclusive(path$1, data) {
|
|
149
|
+
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
150
|
+
let handle;
|
|
151
|
+
try {
|
|
152
|
+
handle = await promises.open(path$1, "wx");
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.code === "EEXIST") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
await handle.writeFile(data, "utf8");
|
|
161
|
+
} finally {
|
|
162
|
+
await handle.close();
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
148
166
|
var defaultFs = {
|
|
149
167
|
async fileExists(path) {
|
|
150
168
|
try {
|
|
@@ -157,7 +175,11 @@ var defaultFs = {
|
|
|
157
175
|
readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
|
|
158
176
|
readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
|
|
159
177
|
writeFile: (path, data) => atomicWrite(path, data),
|
|
160
|
-
writeBytes: (path, data) => atomicWrite(path, data)
|
|
178
|
+
writeBytes: (path, data) => atomicWrite(path, data),
|
|
179
|
+
createExclusive: (path, data) => createExclusive(path, data),
|
|
180
|
+
deleteFile: async (path) => {
|
|
181
|
+
await promises.rm(path, { force: true });
|
|
182
|
+
}
|
|
161
183
|
};
|
|
162
184
|
var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
|
|
163
185
|
var glossaryRecordSchema = zod.z.record(zod.z.string(), zod.z.string());
|
|
@@ -346,29 +368,18 @@ var TIMEOUT_STATUS = 408;
|
|
|
346
368
|
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
347
369
|
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
348
370
|
"RateLimitError",
|
|
349
|
-
// openai, @anthropic-ai/sdk
|
|
350
371
|
"TooManyRequestsError"
|
|
351
|
-
// deepl-node
|
|
352
372
|
]);
|
|
353
373
|
var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
354
374
|
"AuthenticationError",
|
|
355
|
-
// openai, @anthropic-ai/sdk (401)
|
|
356
375
|
"PermissionDeniedError",
|
|
357
|
-
// openai, @anthropic-ai/sdk (403)
|
|
358
376
|
"AuthorizationError"
|
|
359
|
-
// deepl-node
|
|
360
377
|
]);
|
|
361
378
|
var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
362
379
|
"APIConnectionTimeoutError",
|
|
363
|
-
// openai, @anthropic-ai/sdk
|
|
364
|
-
// deepl-node has no distinct timeout class; ConnectionError is its only network-failure
|
|
365
|
-
// class, so it is folded into TIMEOUT rather than the PROVIDER_ERROR fallback.
|
|
366
380
|
"ConnectionError"
|
|
367
381
|
]);
|
|
368
|
-
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
369
|
-
"APIUserAbortError"
|
|
370
|
-
// openai, @anthropic-ai/sdk
|
|
371
|
-
]);
|
|
382
|
+
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set(["APIUserAbortError"]);
|
|
372
383
|
function getErrorStatus(error) {
|
|
373
384
|
if (typeof error !== "object" || error === null || !("status" in error)) {
|
|
374
385
|
return void 0;
|
|
@@ -422,7 +433,6 @@ function isAbortError(error, signal) {
|
|
|
422
433
|
}
|
|
423
434
|
var REDACTED = "[REDACTED]";
|
|
424
435
|
var KEY_PATTERNS = [
|
|
425
|
-
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
426
436
|
/\bsk-[A-Za-z0-9_-]{8,}/g,
|
|
427
437
|
/AIza[0-9A-Za-z_-]{35}/g,
|
|
428
438
|
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(?::fx)?/g
|
|
@@ -444,6 +454,10 @@ var ProviderError = class extends Error {
|
|
|
444
454
|
/** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
|
|
445
455
|
code;
|
|
446
456
|
/**
|
|
457
|
+
* Redacts the message as a defense-in-depth backstop before storing it. The redaction runs with an
|
|
458
|
+
* empty secret argument so the ANTHROPIC_API_KEY environment default is not re-applied, keeping
|
|
459
|
+
* this generic error decoupled from any one provider's environment variable.
|
|
460
|
+
*
|
|
447
461
|
* @param code - The stable failure code.
|
|
448
462
|
* @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
|
|
449
463
|
*/
|
|
@@ -502,6 +516,76 @@ function validateRequest(request) {
|
|
|
502
516
|
}
|
|
503
517
|
return parsed.data;
|
|
504
518
|
}
|
|
519
|
+
var LENGTH_RATIO_MIN = 0.35;
|
|
520
|
+
var LENGTH_RATIO_MAX = 3;
|
|
521
|
+
var LENGTH_RATIO_MIN_SOURCE_LENGTH = 12;
|
|
522
|
+
var UNICODE_LETTER = /\p{L}/u;
|
|
523
|
+
var DEGRADATION_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
524
|
+
"FORMALITY_DOWNGRADED",
|
|
525
|
+
"GLOSSARY_IGNORED"
|
|
526
|
+
]);
|
|
527
|
+
function isLengthRatioOutlier(sourceValue, translatedValue) {
|
|
528
|
+
const trimmedSource = sourceValue.trim();
|
|
529
|
+
if (trimmedSource.length < LENGTH_RATIO_MIN_SOURCE_LENGTH) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
const ratio = translatedValue.trim().length / trimmedSource.length;
|
|
533
|
+
return ratio < LENGTH_RATIO_MIN || ratio > LENGTH_RATIO_MAX;
|
|
534
|
+
}
|
|
535
|
+
function isEqualsSource(input) {
|
|
536
|
+
const trimmedSource = input.sourceValue.trim();
|
|
537
|
+
const trimmedTranslated = input.translatedValue.trim();
|
|
538
|
+
return trimmedTranslated === trimmedSource && input.targetLocale !== input.sourceLocale && UNICODE_LETTER.test(trimmedSource);
|
|
539
|
+
}
|
|
540
|
+
function isGlossaryTermMissed(input) {
|
|
541
|
+
const glossary = input.glossary;
|
|
542
|
+
if (glossary === void 0 || Object.keys(glossary).length === 0) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const sourceLower = input.sourceValue.toLowerCase();
|
|
546
|
+
const translatedLower = input.translatedValue.toLowerCase();
|
|
547
|
+
for (const [sourceTerm, targetTerm] of Object.entries(glossary)) {
|
|
548
|
+
const sourceHit = sourceLower.includes(sourceTerm.toLowerCase());
|
|
549
|
+
const targetHit = translatedLower.includes(targetTerm.toLowerCase());
|
|
550
|
+
if (sourceHit && !targetHit) {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
function isIntegrityReordered(integrity) {
|
|
557
|
+
return integrity.matches && integrity.reordered;
|
|
558
|
+
}
|
|
559
|
+
function computeReviewFlags(input) {
|
|
560
|
+
const reasons = [];
|
|
561
|
+
if (isLengthRatioOutlier(input.sourceValue, input.translatedValue)) {
|
|
562
|
+
reasons.push("LENGTH_RATIO_OUTLIER");
|
|
563
|
+
}
|
|
564
|
+
if (isEqualsSource(input)) {
|
|
565
|
+
reasons.push("EQUALS_SOURCE");
|
|
566
|
+
}
|
|
567
|
+
if (isGlossaryTermMissed(input)) {
|
|
568
|
+
reasons.push("GLOSSARY_TERM_MISSED");
|
|
569
|
+
}
|
|
570
|
+
if (isIntegrityReordered(input.integrity)) {
|
|
571
|
+
reasons.push("INTEGRITY_REORDERED");
|
|
572
|
+
}
|
|
573
|
+
return reasons.length > 0 ? { status: "review", reasons } : void 0;
|
|
574
|
+
}
|
|
575
|
+
function applyProviderDegraded(reviewFlags, notices, acceptedKeys) {
|
|
576
|
+
if (!notices.some((notice) => DEGRADATION_NOTICE_CODES.has(notice.code))) {
|
|
577
|
+
return reviewFlags;
|
|
578
|
+
}
|
|
579
|
+
const next = new Map(reviewFlags);
|
|
580
|
+
for (const key of acceptedKeys) {
|
|
581
|
+
const existing = next.get(key);
|
|
582
|
+
next.set(key, {
|
|
583
|
+
status: "review",
|
|
584
|
+
reasons: existing !== void 0 ? [...existing.reasons, "PROVIDER_DEGRADED"] : ["PROVIDER_DEGRADED"]
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return next;
|
|
588
|
+
}
|
|
505
589
|
function toIntegrityInputs(entries, values) {
|
|
506
590
|
const inputs = [];
|
|
507
591
|
for (const entry of entries) {
|
|
@@ -597,7 +681,33 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
597
681
|
request.extractPlaceholders,
|
|
598
682
|
request.comparePlaceholders
|
|
599
683
|
);
|
|
600
|
-
|
|
684
|
+
const notices = [];
|
|
685
|
+
const reviewFlags = applyProviderDegraded(buildReviewFlags(data, values, integrity), notices, [
|
|
686
|
+
...values.keys()
|
|
687
|
+
]);
|
|
688
|
+
return usage === void 0 ? { values, integrity, notices, reviewFlags } : { values, integrity, usage, notices, reviewFlags };
|
|
689
|
+
}
|
|
690
|
+
function buildReviewFlags(data, values, integrity) {
|
|
691
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
692
|
+
for (const entry of data.entries) {
|
|
693
|
+
const translatedValue = values.get(entry.key);
|
|
694
|
+
const entryIntegrity = integrity.get(entry.key);
|
|
695
|
+
if (translatedValue === void 0 || entryIntegrity === void 0) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const flag = computeReviewFlags({
|
|
699
|
+
sourceValue: entry.value,
|
|
700
|
+
translatedValue,
|
|
701
|
+
sourceLocale: data.sourceLocale,
|
|
702
|
+
targetLocale: data.targetLocale,
|
|
703
|
+
integrity: entryIntegrity,
|
|
704
|
+
glossary: data.glossary
|
|
705
|
+
});
|
|
706
|
+
if (flag !== void 0) {
|
|
707
|
+
reviewFlags.set(entry.key, flag);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return reviewFlags;
|
|
601
711
|
}
|
|
602
712
|
async function requestTranslations(mechanism, data, signal) {
|
|
603
713
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
@@ -915,9 +1025,6 @@ function createDeepLProvider(config, deps = {}) {
|
|
|
915
1025
|
return {
|
|
916
1026
|
id: PROVIDER_ID2,
|
|
917
1027
|
kind: "machine-translation",
|
|
918
|
-
// Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
|
|
919
|
-
// generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
|
|
920
|
-
// silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
|
|
921
1028
|
supportsGlossary: validConfig.glossaryId !== void 0,
|
|
922
1029
|
translateBatch: (request) => translate(bundle, validConfig, request)
|
|
923
1030
|
};
|
|
@@ -952,7 +1059,34 @@ async function translate(bundle, config, request) {
|
|
|
952
1059
|
if (unprotectable.length > 0) {
|
|
953
1060
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
954
1061
|
}
|
|
955
|
-
|
|
1062
|
+
const reviewFlags = applyProviderDegraded(
|
|
1063
|
+
buildReviewFlags2(protectable, data, values, integrity, request.glossary),
|
|
1064
|
+
notices,
|
|
1065
|
+
[...values.keys()]
|
|
1066
|
+
);
|
|
1067
|
+
return { values, integrity, notices, reviewFlags };
|
|
1068
|
+
}
|
|
1069
|
+
function buildReviewFlags2(protectable, data, values, integrity, glossary) {
|
|
1070
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
1071
|
+
for (const entry of protectable) {
|
|
1072
|
+
const translatedValue = values.get(entry.key);
|
|
1073
|
+
const entryIntegrity = integrity.get(entry.key);
|
|
1074
|
+
if (translatedValue === void 0 || entryIntegrity === void 0) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const flag = computeReviewFlags({
|
|
1078
|
+
sourceValue: entry.value,
|
|
1079
|
+
translatedValue,
|
|
1080
|
+
sourceLocale: data.sourceLocale,
|
|
1081
|
+
targetLocale: data.targetLocale,
|
|
1082
|
+
integrity: entryIntegrity,
|
|
1083
|
+
glossary
|
|
1084
|
+
});
|
|
1085
|
+
if (flag !== void 0) {
|
|
1086
|
+
reviewFlags.set(entry.key, flag);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return reviewFlags;
|
|
956
1090
|
}
|
|
957
1091
|
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
958
1092
|
if (protectable.length === 0) {
|
|
@@ -1002,14 +1136,14 @@ function throwAbort(signal) {
|
|
|
1002
1136
|
throw new DOMException("This operation was aborted.", "AbortError");
|
|
1003
1137
|
}
|
|
1004
1138
|
function delay(ms, signal) {
|
|
1005
|
-
return new Promise((
|
|
1139
|
+
return new Promise((resolve9) => {
|
|
1006
1140
|
const onAbort = () => {
|
|
1007
1141
|
clearTimeout(timer);
|
|
1008
|
-
|
|
1142
|
+
resolve9();
|
|
1009
1143
|
};
|
|
1010
1144
|
const timer = setTimeout(() => {
|
|
1011
1145
|
signal?.removeEventListener("abort", onAbort);
|
|
1012
|
-
|
|
1146
|
+
resolve9();
|
|
1013
1147
|
}, ms);
|
|
1014
1148
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1015
1149
|
});
|
|
@@ -1057,12 +1191,10 @@ var TYPE_MAP = {
|
|
|
1057
1191
|
object: "OBJECT"
|
|
1058
1192
|
};
|
|
1059
1193
|
var HANDLED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1060
|
-
// transformed or recursed
|
|
1061
1194
|
"type",
|
|
1062
1195
|
"required",
|
|
1063
1196
|
"properties",
|
|
1064
1197
|
"items",
|
|
1065
|
-
// deliberately dropped (not part of Google's Schema dialect)
|
|
1066
1198
|
"$schema",
|
|
1067
1199
|
"additionalProperties"
|
|
1068
1200
|
]);
|
|
@@ -1456,6 +1588,7 @@ function buildProvider(config) {
|
|
|
1456
1588
|
|
|
1457
1589
|
// src/config/schema.ts
|
|
1458
1590
|
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1591
|
+
var DEFAULT_BUDGET_BEHAVIOR = "warn";
|
|
1459
1592
|
function findCaseInsensitiveDuplicate(locales) {
|
|
1460
1593
|
const seen = /* @__PURE__ */ new Set();
|
|
1461
1594
|
for (const locale of locales) {
|
|
@@ -1504,7 +1637,23 @@ var verbatraConfigSchema = zod.z.strictObject({
|
|
|
1504
1637
|
* be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
|
|
1505
1638
|
* {@link DEFAULT_MAX_BATCH_SIZE} applies.
|
|
1506
1639
|
*/
|
|
1507
|
-
maxBatchSize: zod.z.number().int().positive().optional()
|
|
1640
|
+
maxBatchSize: zod.z.number().int().positive().optional(),
|
|
1641
|
+
/**
|
|
1642
|
+
* Optional whole-run token ceiling (input plus output tokens summed across every provider call, main
|
|
1643
|
+
* translation and plural generation alike, across all target locales). Checked after each completed
|
|
1644
|
+
* sub-batch, never mid-batch: the sub-batch whose completion crosses the ceiling is retained and
|
|
1645
|
+
* counted, since a call already in flight cannot be undone. Must be a positive integer. Config-only,
|
|
1646
|
+
* no CLI flag. Absent means no budget is enforced. Inert (never a false trip) against a token-less
|
|
1647
|
+
* provider such as DeepL, since it never reports usage to measure against the ceiling.
|
|
1648
|
+
*/
|
|
1649
|
+
maxTokens: zod.z.number().int().positive().optional(),
|
|
1650
|
+
/**
|
|
1651
|
+
* What happens once `maxTokens` is reached: `"warn"` (default) flags it and lets the run continue
|
|
1652
|
+
* unchanged; `"stop"` withholds every not-yet-attempted key for the rest of the run (the current
|
|
1653
|
+
* locale's remaining candidates and every later locale's), retried automatically next run. Present
|
|
1654
|
+
* without `maxTokens` is accepted and has no effect. Never changes the command's exit code.
|
|
1655
|
+
*/
|
|
1656
|
+
budgetBehavior: zod.z.enum(["warn", "stop"]).optional()
|
|
1508
1657
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
1509
1658
|
message: "targetLocales must not include the source locale",
|
|
1510
1659
|
path: ["targetLocales"]
|
|
@@ -1613,6 +1762,60 @@ async function loadConfig(options = {}) {
|
|
|
1613
1762
|
const { config } = await loadConfigWithMeta(options);
|
|
1614
1763
|
return config;
|
|
1615
1764
|
}
|
|
1765
|
+
var LOCAL_DIR_NAME = ".verbatra-local";
|
|
1766
|
+
var LOCK_FILE_GUARD_STEM = "_lockfile";
|
|
1767
|
+
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
1768
|
+
var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
|
|
1769
|
+
function lockPath(cwd, stem) {
|
|
1770
|
+
return path.resolve(cwd, LOCAL_DIR_NAME, "locks", `${stem}.lock`);
|
|
1771
|
+
}
|
|
1772
|
+
function localeLockPath(cwd, locale) {
|
|
1773
|
+
return lockPath(cwd, locale);
|
|
1774
|
+
}
|
|
1775
|
+
function lockFileGuardPath(cwd) {
|
|
1776
|
+
return lockPath(cwd, LOCK_FILE_GUARD_STEM);
|
|
1777
|
+
}
|
|
1778
|
+
function sleep(ms) {
|
|
1779
|
+
return new Promise((res) => {
|
|
1780
|
+
setTimeout(res, ms);
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
function lockPayload() {
|
|
1784
|
+
return JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1785
|
+
}
|
|
1786
|
+
async function acquireLock(path, fs, pollIntervalMs, deadline) {
|
|
1787
|
+
for (; ; ) {
|
|
1788
|
+
if (await fs.createExclusive(path, lockPayload())) {
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (Date.now() >= deadline) {
|
|
1792
|
+
throw new SdkError(
|
|
1793
|
+
"LOCK_CONTENDED",
|
|
1794
|
+
`Could not acquire the write lock at ${path}: another process may be holding it. If no verbatra process is currently running, this lock file was likely left behind by one that was killed; delete it and retry.`
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
const jitter = Math.random() * pollIntervalMs;
|
|
1798
|
+
await sleep(pollIntervalMs + jitter);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
async function withFileLock(path, fs, fn, options) {
|
|
1802
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1803
|
+
const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
|
|
1804
|
+
await acquireLock(path, fs, pollIntervalMs, Date.now() + acquireTimeoutMs);
|
|
1805
|
+
try {
|
|
1806
|
+
return await fn();
|
|
1807
|
+
} finally {
|
|
1808
|
+
await fs.deleteFile(path);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
async function withLocaleWriteLock(cwd, locale, fs, fn, options = {}) {
|
|
1812
|
+
return withFileLock(localeLockPath(cwd, locale), fs, fn, options);
|
|
1813
|
+
}
|
|
1814
|
+
async function withLockFileGuard(cwd, fs, fn, options = {}) {
|
|
1815
|
+
return withFileLock(lockFileGuardPath(cwd), fs, fn, options);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// src/lock/lock-file.ts
|
|
1616
1819
|
var LOCK_FILE_NAME = "verbatra.lock.json";
|
|
1617
1820
|
var CURRENT_VERSION = 1;
|
|
1618
1821
|
var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
|
|
@@ -1624,8 +1827,7 @@ var lockFileSchema = zod.z.object({
|
|
|
1624
1827
|
function lockFilePath(cwd) {
|
|
1625
1828
|
return path.resolve(cwd, LOCK_FILE_NAME);
|
|
1626
1829
|
}
|
|
1627
|
-
|
|
1628
|
-
const read = await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES);
|
|
1830
|
+
function parseLockFileRead(read, path) {
|
|
1629
1831
|
if (read.kind === "missing") {
|
|
1630
1832
|
return EMPTY_LOCK;
|
|
1631
1833
|
}
|
|
@@ -1653,6 +1855,9 @@ async function readLockFile(path, fs) {
|
|
|
1653
1855
|
}
|
|
1654
1856
|
return result.data;
|
|
1655
1857
|
}
|
|
1858
|
+
async function readLockFile(path, fs) {
|
|
1859
|
+
return parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
|
|
1860
|
+
}
|
|
1656
1861
|
function baselineFor(lock, locale) {
|
|
1657
1862
|
return new Map(Object.entries(lock.locales[locale] ?? {}));
|
|
1658
1863
|
}
|
|
@@ -1668,14 +1873,30 @@ function byKey(a, b) {
|
|
|
1668
1873
|
function sortRecord(record) {
|
|
1669
1874
|
return Object.fromEntries(Object.entries(record).sort(byKey));
|
|
1670
1875
|
}
|
|
1671
|
-
|
|
1876
|
+
function serializeLockFile(lock) {
|
|
1672
1877
|
const locales = {};
|
|
1673
1878
|
for (const [locale, entries] of Object.entries(lock.locales).sort(byKey)) {
|
|
1674
1879
|
locales[locale] = sortRecord(entries);
|
|
1675
1880
|
}
|
|
1676
1881
|
const ordered = { version: lock.version, locales };
|
|
1677
|
-
|
|
1678
|
-
|
|
1882
|
+
return `${JSON.stringify(ordered, null, 2)}
|
|
1883
|
+
`;
|
|
1884
|
+
}
|
|
1885
|
+
function applyLockLocalePatch(currentEntries, patch) {
|
|
1886
|
+
if (patch.mode === "replace") {
|
|
1887
|
+
return patch.entries;
|
|
1888
|
+
}
|
|
1889
|
+
return { ...currentEntries, ...patch.entries };
|
|
1890
|
+
}
|
|
1891
|
+
async function updateLockFileLocale(cwd, fs, locale, patch) {
|
|
1892
|
+
return withLockFileGuard(cwd, fs, async () => {
|
|
1893
|
+
const path = lockFilePath(cwd);
|
|
1894
|
+
const lock = parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
|
|
1895
|
+
const nextEntries = applyLockLocalePatch(lock.locales[locale], patch);
|
|
1896
|
+
const next = updateLockLocale(lock, locale, nextEntries);
|
|
1897
|
+
await fs.writeFile(path, serializeLockFile(next));
|
|
1898
|
+
return next;
|
|
1899
|
+
});
|
|
1679
1900
|
}
|
|
1680
1901
|
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
1681
1902
|
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
@@ -1937,10 +2158,68 @@ var AdapterError = class extends Error {
|
|
|
1937
2158
|
};
|
|
1938
2159
|
var MAX_DEPTH = 100;
|
|
1939
2160
|
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1940
|
-
var
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2161
|
+
var SENTINEL_ESCAPE = "\\u0001";
|
|
2162
|
+
function isJsonWhitespace(char) {
|
|
2163
|
+
return char === " " || char === " " || char === "\n" || char === "\r";
|
|
2164
|
+
}
|
|
2165
|
+
function endOfStringToken(content, openQuote) {
|
|
2166
|
+
let index = openQuote + 1;
|
|
2167
|
+
while (index < content.length) {
|
|
2168
|
+
const char = content[index];
|
|
2169
|
+
if (char === "\\") {
|
|
2170
|
+
index += 2;
|
|
2171
|
+
} else if (char === '"') {
|
|
2172
|
+
return index + 1;
|
|
2173
|
+
} else {
|
|
2174
|
+
index += 1;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return index;
|
|
2178
|
+
}
|
|
2179
|
+
function isKeyToken(content, end) {
|
|
2180
|
+
let index = end;
|
|
2181
|
+
while (isJsonWhitespace(content[index])) {
|
|
2182
|
+
index += 1;
|
|
2183
|
+
}
|
|
2184
|
+
return content[index] === ":";
|
|
2185
|
+
}
|
|
2186
|
+
function prefixKeyToken(token) {
|
|
2187
|
+
if (token.includes(SENTINEL_ESCAPE)) {
|
|
2188
|
+
throw new AdapterError(
|
|
2189
|
+
"INVALID_STRUCTURE",
|
|
2190
|
+
"A key contains a reserved control-character escape."
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
return `"${SENTINEL_ESCAPE}${token.slice(1)}`;
|
|
2194
|
+
}
|
|
2195
|
+
function prefixKeyTokens(content) {
|
|
2196
|
+
let out = "";
|
|
2197
|
+
let index = 0;
|
|
2198
|
+
while (index < content.length) {
|
|
2199
|
+
const openQuote = content.indexOf('"', index);
|
|
2200
|
+
if (openQuote === -1) {
|
|
2201
|
+
return out + content.slice(index);
|
|
2202
|
+
}
|
|
2203
|
+
const end = endOfStringToken(content, openQuote);
|
|
2204
|
+
const token = content.slice(openQuote, end);
|
|
2205
|
+
out += content.slice(index, openQuote);
|
|
2206
|
+
out += isKeyToken(content, end) ? prefixKeyToken(token) : token;
|
|
2207
|
+
index = end;
|
|
2208
|
+
}
|
|
2209
|
+
return out;
|
|
2210
|
+
}
|
|
2211
|
+
function childrenOf(node) {
|
|
2212
|
+
if (node instanceof Map) {
|
|
2213
|
+
return node.values();
|
|
2214
|
+
}
|
|
2215
|
+
if (Array.isArray(node)) {
|
|
2216
|
+
return node;
|
|
2217
|
+
}
|
|
2218
|
+
if (typeof node === "object" && node !== null) {
|
|
2219
|
+
return Object.values(node);
|
|
2220
|
+
}
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
1944
2223
|
function assertWithinDepth(value, max) {
|
|
1945
2224
|
const stack = [{ node: value, depth: 1 }];
|
|
1946
2225
|
while (stack.length > 0) {
|
|
@@ -1948,42 +2227,118 @@ function assertWithinDepth(value, max) {
|
|
|
1948
2227
|
if (top === void 0) {
|
|
1949
2228
|
break;
|
|
1950
2229
|
}
|
|
1951
|
-
const
|
|
1952
|
-
if (
|
|
2230
|
+
const children = childrenOf(top.node);
|
|
2231
|
+
if (children === null) {
|
|
1953
2232
|
continue;
|
|
1954
2233
|
}
|
|
1955
|
-
if (depth > max) {
|
|
2234
|
+
if (top.depth > max) {
|
|
1956
2235
|
throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
|
|
1957
2236
|
}
|
|
1958
|
-
for (const child of
|
|
1959
|
-
stack.push({ node: child, depth: depth + 1 });
|
|
2237
|
+
for (const child of children) {
|
|
2238
|
+
stack.push({ node: child, depth: top.depth + 1 });
|
|
1960
2239
|
}
|
|
1961
2240
|
}
|
|
1962
2241
|
}
|
|
1963
|
-
function
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
if (!result.success) {
|
|
1967
|
-
throw new AdapterError(
|
|
1968
|
-
"INVALID_STRUCTURE",
|
|
1969
|
-
"The file is not a valid object (expected nested objects of string values)."
|
|
1970
|
-
);
|
|
2242
|
+
function toOrdered(node) {
|
|
2243
|
+
if (Array.isArray(node)) {
|
|
2244
|
+
return node.map((child) => toOrdered(child));
|
|
1971
2245
|
}
|
|
1972
|
-
|
|
2246
|
+
if (typeof node === "object" && node !== null) {
|
|
2247
|
+
const out = /* @__PURE__ */ new Map();
|
|
2248
|
+
for (const [key, child] of Object.entries(node)) {
|
|
2249
|
+
out.set(key.slice(1), toOrdered(child));
|
|
2250
|
+
}
|
|
2251
|
+
return out;
|
|
2252
|
+
}
|
|
2253
|
+
return node;
|
|
1973
2254
|
}
|
|
1974
|
-
function
|
|
2255
|
+
function parseOrderedJson(content) {
|
|
2256
|
+
const prefixed = prefixKeyTokens(content);
|
|
1975
2257
|
let parsed;
|
|
1976
2258
|
try {
|
|
1977
|
-
parsed = JSON.parse(
|
|
2259
|
+
parsed = JSON.parse(prefixed);
|
|
1978
2260
|
} catch {
|
|
1979
2261
|
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
1980
2262
|
}
|
|
1981
|
-
|
|
2263
|
+
assertWithinDepth(parsed, MAX_DEPTH);
|
|
2264
|
+
return toOrdered(parsed);
|
|
1982
2265
|
}
|
|
1983
|
-
function
|
|
1984
|
-
|
|
2266
|
+
function printRecord(record, indent) {
|
|
2267
|
+
if (record.size === 0) {
|
|
2268
|
+
return "{}";
|
|
2269
|
+
}
|
|
2270
|
+
const childIndent = `${indent} `;
|
|
2271
|
+
const lines = [];
|
|
2272
|
+
for (const [key, child] of record) {
|
|
2273
|
+
lines.push(`${childIndent}${JSON.stringify(key)}: ${printValue(child, childIndent)}`);
|
|
2274
|
+
}
|
|
2275
|
+
return `{
|
|
2276
|
+
${lines.join(",\n")}
|
|
2277
|
+
${indent}}`;
|
|
2278
|
+
}
|
|
2279
|
+
function printArray(items, indent) {
|
|
2280
|
+
if (items.length === 0) {
|
|
2281
|
+
return "[]";
|
|
2282
|
+
}
|
|
2283
|
+
const childIndent = `${indent} `;
|
|
2284
|
+
const lines = items.map((item) => `${childIndent}${printValue(item, childIndent)}`);
|
|
2285
|
+
return `[
|
|
2286
|
+
${lines.join(",\n")}
|
|
2287
|
+
${indent}]`;
|
|
2288
|
+
}
|
|
2289
|
+
function printValue(value, indent) {
|
|
2290
|
+
if (value instanceof Map) {
|
|
2291
|
+
return printRecord(value, indent);
|
|
2292
|
+
}
|
|
2293
|
+
if (Array.isArray(value)) {
|
|
2294
|
+
return printArray(value, indent);
|
|
2295
|
+
}
|
|
2296
|
+
return JSON.stringify(value);
|
|
2297
|
+
}
|
|
2298
|
+
function serializeOrderedJson(value) {
|
|
2299
|
+
return `${printValue(value, "")}
|
|
1985
2300
|
`;
|
|
1986
2301
|
}
|
|
2302
|
+
var INVALID_STRUCTURE_MESSAGE = "The file is not a valid object (expected nested objects of string, number, boolean, or null leaves).";
|
|
2303
|
+
function isJsonLeaf(value) {
|
|
2304
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
|
|
2305
|
+
}
|
|
2306
|
+
function assertNodesValid(root) {
|
|
2307
|
+
const stack = [root];
|
|
2308
|
+
while (stack.length > 0) {
|
|
2309
|
+
const node = stack.pop();
|
|
2310
|
+
if (node === void 0) {
|
|
2311
|
+
break;
|
|
2312
|
+
}
|
|
2313
|
+
for (const [key, child] of node) {
|
|
2314
|
+
if (typeof key !== "string") {
|
|
2315
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2316
|
+
}
|
|
2317
|
+
if (child instanceof Map) {
|
|
2318
|
+
stack.push(child);
|
|
2319
|
+
} else if (!isJsonLeaf(child)) {
|
|
2320
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
function assertJsonRecord(value) {
|
|
2326
|
+
assertWithinDepth(value, MAX_DEPTH);
|
|
2327
|
+
if (!(value instanceof Map)) {
|
|
2328
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2329
|
+
}
|
|
2330
|
+
assertNodesValid(value);
|
|
2331
|
+
return value;
|
|
2332
|
+
}
|
|
2333
|
+
function parseJsonObject(content) {
|
|
2334
|
+
return assertJsonRecord(parseOrderedJson(content));
|
|
2335
|
+
}
|
|
2336
|
+
function serializeJsonTree(tree) {
|
|
2337
|
+
return serializeOrderedJson(tree);
|
|
2338
|
+
}
|
|
2339
|
+
function isJsonNode(value) {
|
|
2340
|
+
return value instanceof Map;
|
|
2341
|
+
}
|
|
1987
2342
|
function namespaceOf(filePath) {
|
|
1988
2343
|
return path.basename(filePath, path.extname(filePath));
|
|
1989
2344
|
}
|
|
@@ -2011,9 +2366,25 @@ function buildCanHandle(extensions, sniff) {
|
|
|
2011
2366
|
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
2012
2367
|
};
|
|
2013
2368
|
}
|
|
2369
|
+
async function fsyncPath(path) {
|
|
2370
|
+
const handle = await promises.open(path, "r");
|
|
2371
|
+
try {
|
|
2372
|
+
await handle.sync();
|
|
2373
|
+
} finally {
|
|
2374
|
+
await handle.close();
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
async function fsyncDirBestEffort(path) {
|
|
2378
|
+
try {
|
|
2379
|
+
await fsyncPath(path);
|
|
2380
|
+
} catch {
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2014
2383
|
var nodeOps = {
|
|
2015
2384
|
writeFile: (path, data) => promises.writeFile(path, data, "utf8"),
|
|
2385
|
+
fsyncFile: (path) => fsyncPath(path),
|
|
2016
2386
|
rename: (from, to) => promises.rename(from, to),
|
|
2387
|
+
fsyncDir: (path) => fsyncDirBestEffort(path),
|
|
2017
2388
|
rm: (path) => promises.rm(path, { force: true })
|
|
2018
2389
|
};
|
|
2019
2390
|
async function cleanup(ops, tmp) {
|
|
@@ -2025,15 +2396,20 @@ async function cleanup(ops, tmp) {
|
|
|
2025
2396
|
function tempFileName2(path$1) {
|
|
2026
2397
|
return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
|
|
2027
2398
|
}
|
|
2028
|
-
async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
2029
|
-
const tmp = tempFileName2(path);
|
|
2399
|
+
async function atomicWriteFile(path$1, data, ops = nodeOps) {
|
|
2400
|
+
const tmp = tempFileName2(path$1);
|
|
2030
2401
|
try {
|
|
2031
2402
|
await ops.writeFile(tmp, data);
|
|
2032
|
-
await ops.
|
|
2403
|
+
await ops.fsyncFile(tmp);
|
|
2404
|
+
await ops.rename(tmp, path$1);
|
|
2033
2405
|
} catch (error) {
|
|
2034
2406
|
await cleanup(ops, tmp);
|
|
2035
2407
|
throw error;
|
|
2036
2408
|
}
|
|
2409
|
+
try {
|
|
2410
|
+
await ops.fsyncDir(path.dirname(path$1));
|
|
2411
|
+
} catch {
|
|
2412
|
+
}
|
|
2037
2413
|
}
|
|
2038
2414
|
function stripLeadingBom(content) {
|
|
2039
2415
|
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
@@ -2155,21 +2531,25 @@ function addLeaf(ctx, segments, key, value) {
|
|
|
2155
2531
|
);
|
|
2156
2532
|
}
|
|
2157
2533
|
ctx.claimed.set(effectivePath, mapKey);
|
|
2534
|
+
if (typeof value !== "string") {
|
|
2535
|
+
ctx.excluded.push(mapKey);
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2158
2538
|
const { placeholders, isPlural } = ctx.derive(key, value);
|
|
2159
2539
|
ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
|
|
2160
2540
|
}
|
|
2161
2541
|
function addEntries(ctx, prefix, node) {
|
|
2162
|
-
for (const [key, value] of
|
|
2542
|
+
for (const [key, value] of node) {
|
|
2163
2543
|
const segments = [...prefix, key];
|
|
2164
|
-
if (
|
|
2165
|
-
addLeaf(ctx, segments, key, value);
|
|
2166
|
-
} else {
|
|
2544
|
+
if (isJsonNode(value)) {
|
|
2167
2545
|
addEntries(ctx, segments, value);
|
|
2546
|
+
} else {
|
|
2547
|
+
addLeaf(ctx, segments, key, value);
|
|
2168
2548
|
}
|
|
2169
2549
|
}
|
|
2170
2550
|
}
|
|
2171
|
-
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
2172
|
-
for (const [key, value] of
|
|
2551
|
+
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths, excluded) {
|
|
2552
|
+
for (const [key, value] of node) {
|
|
2173
2553
|
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
2174
2554
|
if (claimedPaths.has(path)) {
|
|
2175
2555
|
throw new AdapterError(
|
|
@@ -2178,34 +2558,35 @@ function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
|
2178
2558
|
);
|
|
2179
2559
|
}
|
|
2180
2560
|
claimedPaths.add(path);
|
|
2181
|
-
if (
|
|
2561
|
+
if (isJsonNode(value)) {
|
|
2562
|
+
addPathEntries(value, path, namespace, derive, out, claimedPaths, excluded);
|
|
2563
|
+
} else if (typeof value === "string") {
|
|
2182
2564
|
const { placeholders, isPlural } = derive(key, value);
|
|
2183
2565
|
out.set(path, { key: path, namespace, value, placeholders, isPlural });
|
|
2184
2566
|
} else {
|
|
2185
|
-
|
|
2567
|
+
excluded.push(path);
|
|
2186
2568
|
}
|
|
2187
2569
|
}
|
|
2188
2570
|
}
|
|
2189
2571
|
function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
|
|
2190
2572
|
const out = /* @__PURE__ */ new Map();
|
|
2191
2573
|
if (keyMode === "path-notation") {
|
|
2192
|
-
|
|
2193
|
-
|
|
2574
|
+
const excluded = [];
|
|
2575
|
+
addPathEntries(tree, "", namespace, derive, out, /* @__PURE__ */ new Set(), excluded);
|
|
2576
|
+
return { entries: out, excludedLeafPaths: excluded };
|
|
2194
2577
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
}
|
|
2198
|
-
function emptyNode() {
|
|
2199
|
-
return /* @__PURE__ */ Object.create(null);
|
|
2578
|
+
const ctx = { namespace, derive, out, claimed: /* @__PURE__ */ new Map(), excluded: [] };
|
|
2579
|
+
addEntries(ctx, [], tree);
|
|
2580
|
+
return { entries: out, excludedLeafPaths: ctx.excluded };
|
|
2200
2581
|
}
|
|
2201
2582
|
function descend(node, segment) {
|
|
2202
|
-
const next = node
|
|
2583
|
+
const next = node.get(segment);
|
|
2203
2584
|
if (next === void 0) {
|
|
2204
|
-
const created =
|
|
2205
|
-
node
|
|
2585
|
+
const created = /* @__PURE__ */ new Map();
|
|
2586
|
+
node.set(segment, created);
|
|
2206
2587
|
return created;
|
|
2207
2588
|
}
|
|
2208
|
-
if (
|
|
2589
|
+
if (next instanceof Map) {
|
|
2209
2590
|
return next;
|
|
2210
2591
|
}
|
|
2211
2592
|
throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
|
|
@@ -2219,13 +2600,13 @@ function setPath(root, segments, value) {
|
|
|
2219
2600
|
for (const segment of segments.slice(0, -1)) {
|
|
2220
2601
|
node = descend(node, segment);
|
|
2221
2602
|
}
|
|
2222
|
-
if (
|
|
2603
|
+
if (node.get(leaf) instanceof Map) {
|
|
2223
2604
|
throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
|
|
2224
2605
|
}
|
|
2225
|
-
node
|
|
2606
|
+
node.set(leaf, value);
|
|
2226
2607
|
}
|
|
2227
2608
|
function unflattenEntries(entries) {
|
|
2228
|
-
const root =
|
|
2609
|
+
const root = /* @__PURE__ */ new Map();
|
|
2229
2610
|
for (const [key, entry] of entries) {
|
|
2230
2611
|
setPath(root, decodeKeyToSegments(key), entry.value);
|
|
2231
2612
|
}
|
|
@@ -2246,9 +2627,9 @@ function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTre
|
|
|
2246
2627
|
try {
|
|
2247
2628
|
const tree = parse3(content);
|
|
2248
2629
|
validateTree?.(tree);
|
|
2249
|
-
const
|
|
2250
|
-
mergeDescriptions(entries, content, deriveDescriptions);
|
|
2251
|
-
return
|
|
2630
|
+
const result = flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
2631
|
+
mergeDescriptions(result.entries, content, deriveDescriptions);
|
|
2632
|
+
return result;
|
|
2252
2633
|
} catch (error) {
|
|
2253
2634
|
rethrowStructured(error, "The file could not be parsed.");
|
|
2254
2635
|
}
|
|
@@ -2279,7 +2660,7 @@ function createTreeFileAdapter(options) {
|
|
|
2279
2660
|
async read(filePath, locale) {
|
|
2280
2661
|
const content = await readFileContent(filePath);
|
|
2281
2662
|
const namespace = namespaceOf(filePath);
|
|
2282
|
-
const entries = toEntries(
|
|
2663
|
+
const { entries, excludedLeafPaths } = toEntries(
|
|
2283
2664
|
content,
|
|
2284
2665
|
namespace,
|
|
2285
2666
|
parse3,
|
|
@@ -2290,7 +2671,7 @@ function createTreeFileAdapter(options) {
|
|
|
2290
2671
|
);
|
|
2291
2672
|
const resource = { locale, namespace, format, entries };
|
|
2292
2673
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2293
|
-
return { resource, invalidIcuKeys };
|
|
2674
|
+
return { resource, invalidIcuKeys, excludedLeafPaths };
|
|
2294
2675
|
},
|
|
2295
2676
|
async write(resource, filePath) {
|
|
2296
2677
|
const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
|
|
@@ -2302,13 +2683,8 @@ function isMetadataKey(key) {
|
|
|
2302
2683
|
return key.startsWith("@");
|
|
2303
2684
|
}
|
|
2304
2685
|
function parseArbObject(content) {
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
parsed = JSON.parse(content);
|
|
2308
|
-
} catch {
|
|
2309
|
-
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
2310
|
-
}
|
|
2311
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2686
|
+
const parsed = parseOrderedJson(content);
|
|
2687
|
+
if (!(parsed instanceof Map)) {
|
|
2312
2688
|
throw new AdapterError(
|
|
2313
2689
|
"INVALID_STRUCTURE",
|
|
2314
2690
|
"The file is not a valid object (expected nested objects of string values)."
|
|
@@ -2317,10 +2693,10 @@ function parseArbObject(content) {
|
|
|
2317
2693
|
return parsed;
|
|
2318
2694
|
}
|
|
2319
2695
|
function stripArbMetadata(tree) {
|
|
2320
|
-
const out = /* @__PURE__ */
|
|
2321
|
-
for (const [key, value] of
|
|
2696
|
+
const out = /* @__PURE__ */ new Map();
|
|
2697
|
+
for (const [key, value] of tree) {
|
|
2322
2698
|
if (!isMetadataKey(key)) {
|
|
2323
|
-
out
|
|
2699
|
+
out.set(key, value);
|
|
2324
2700
|
}
|
|
2325
2701
|
}
|
|
2326
2702
|
return out;
|
|
@@ -2332,16 +2708,16 @@ function messageKeyForMetadata(key) {
|
|
|
2332
2708
|
return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
|
|
2333
2709
|
}
|
|
2334
2710
|
function descriptionOf(value) {
|
|
2335
|
-
if (
|
|
2711
|
+
if (!(value instanceof Map)) {
|
|
2336
2712
|
return void 0;
|
|
2337
2713
|
}
|
|
2338
|
-
const description = value.description;
|
|
2714
|
+
const description = value.get("description");
|
|
2339
2715
|
return typeof description === "string" ? description : void 0;
|
|
2340
2716
|
}
|
|
2341
2717
|
function extractArbDescriptions(content) {
|
|
2342
2718
|
const tree = parseArbObject(content);
|
|
2343
2719
|
const out = /* @__PURE__ */ new Map();
|
|
2344
|
-
for (const [key, value] of
|
|
2720
|
+
for (const [key, value] of tree) {
|
|
2345
2721
|
const messageKey = messageKeyForMetadata(key);
|
|
2346
2722
|
if (messageKey === null) {
|
|
2347
2723
|
continue;
|
|
@@ -2373,23 +2749,25 @@ async function readDestinationPairs(filePath) {
|
|
|
2373
2749
|
}
|
|
2374
2750
|
throw error;
|
|
2375
2751
|
}
|
|
2376
|
-
return
|
|
2752
|
+
return [...parseArbObject(content)];
|
|
2377
2753
|
}
|
|
2378
2754
|
async function buildArbWriteTree(entries, filePath) {
|
|
2379
2755
|
const messages = messagesFromEntries(entries);
|
|
2380
2756
|
const pairs = await readDestinationPairs(filePath);
|
|
2381
|
-
const out = /* @__PURE__ */
|
|
2757
|
+
const out = /* @__PURE__ */ new Map();
|
|
2382
2758
|
const consumed = /* @__PURE__ */ new Set();
|
|
2383
2759
|
for (const [key, value] of pairs ?? []) {
|
|
2384
2760
|
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
2385
2761
|
if (translated !== void 0) {
|
|
2386
2762
|
consumed.add(key);
|
|
2763
|
+
out.set(key, translated);
|
|
2764
|
+
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
2765
|
+
out.set(key, value);
|
|
2387
2766
|
}
|
|
2388
|
-
out[key] = translated ?? value;
|
|
2389
2767
|
}
|
|
2390
2768
|
for (const [key, value] of messages) {
|
|
2391
2769
|
if (!consumed.has(key)) {
|
|
2392
|
-
out
|
|
2770
|
+
out.set(key, value);
|
|
2393
2771
|
}
|
|
2394
2772
|
}
|
|
2395
2773
|
return out;
|
|
@@ -2478,8 +2856,8 @@ function createNextIntlJsonAdapter() {
|
|
|
2478
2856
|
});
|
|
2479
2857
|
}
|
|
2480
2858
|
function assertNoDottedNestedKey(tree) {
|
|
2481
|
-
for (const [key, value] of
|
|
2482
|
-
if (
|
|
2859
|
+
for (const [key, value] of tree) {
|
|
2860
|
+
if (!isJsonNode(value)) {
|
|
2483
2861
|
continue;
|
|
2484
2862
|
}
|
|
2485
2863
|
if (key.includes(".")) {
|
|
@@ -2494,8 +2872,8 @@ function assertNoDottedNestedKey(tree) {
|
|
|
2494
2872
|
function assertNotMixed(tree) {
|
|
2495
2873
|
let hasNested = false;
|
|
2496
2874
|
let hasFlatDottedKey = false;
|
|
2497
|
-
for (const [key, value] of
|
|
2498
|
-
if (
|
|
2875
|
+
for (const [key, value] of tree) {
|
|
2876
|
+
if (isJsonNode(value)) {
|
|
2499
2877
|
hasNested = true;
|
|
2500
2878
|
} else if (key.includes(".")) {
|
|
2501
2879
|
hasFlatDottedKey = true;
|
|
@@ -2531,9 +2909,9 @@ async function detectStyle(filePath) {
|
|
|
2531
2909
|
return "flat";
|
|
2532
2910
|
}
|
|
2533
2911
|
function buildFlatTree(entries) {
|
|
2534
|
-
const out = /* @__PURE__ */
|
|
2912
|
+
const out = /* @__PURE__ */ new Map();
|
|
2535
2913
|
for (const [key, entry] of entries) {
|
|
2536
|
-
out
|
|
2914
|
+
out.set(key, entry.value);
|
|
2537
2915
|
}
|
|
2538
2916
|
return out;
|
|
2539
2917
|
}
|
|
@@ -2551,7 +2929,6 @@ function createNgxTranslateJsonAdapter() {
|
|
|
2551
2929
|
}),
|
|
2552
2930
|
validateTree: assertNotMixed,
|
|
2553
2931
|
buildWriteTree: buildNgxWriteTree,
|
|
2554
|
-
// ngx-translate flat style uses dotted keys as path notation, not literal leaves.
|
|
2555
2932
|
keyMode: "path-notation"
|
|
2556
2933
|
});
|
|
2557
2934
|
}
|
|
@@ -2577,6 +2954,11 @@ var AdapterRegistry = class {
|
|
|
2577
2954
|
}
|
|
2578
2955
|
return { status: "resolved", adapter };
|
|
2579
2956
|
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Resolve by asking every registered adapter's `canHandle`. Several adapters can claim one file
|
|
2959
|
+
* (all JSON adapters claim `.json`), so more than one match is reported as `ambiguous` rather
|
|
2960
|
+
* than guessed at.
|
|
2961
|
+
*/
|
|
2580
2962
|
resolveByDetection(filePath, sample) {
|
|
2581
2963
|
const matches = this.adapters.filter((adapter) => adapter.canHandle(filePath, sample));
|
|
2582
2964
|
const first = matches[0];
|
|
@@ -2656,7 +3038,7 @@ function createFlatFileAdapter(options) {
|
|
|
2656
3038
|
const entries = toEntries2(content, namespace, parseEntries);
|
|
2657
3039
|
const resource = { locale, namespace, format, entries };
|
|
2658
3040
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2659
|
-
return { resource, invalidIcuKeys };
|
|
3041
|
+
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
2660
3042
|
},
|
|
2661
3043
|
async write(resource, filePath) {
|
|
2662
3044
|
const data = await serializeEntries(resource.entries, filePath);
|
|
@@ -2931,14 +3313,34 @@ function createXliffAdapter() {
|
|
|
2931
3313
|
extractPlaceholders: extractXliffPlaceholders
|
|
2932
3314
|
});
|
|
2933
3315
|
}
|
|
3316
|
+
function normalizeKey(key) {
|
|
3317
|
+
if (typeof key === "object" && key !== null) {
|
|
3318
|
+
throw new AdapterError(
|
|
3319
|
+
"INVALID_STRUCTURE",
|
|
3320
|
+
"A mapping key is a map or sequence (expected scalar keys)."
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
return String(key);
|
|
3324
|
+
}
|
|
3325
|
+
function normalizeYamlTree(value) {
|
|
3326
|
+
if (!(value instanceof Map)) {
|
|
3327
|
+
return value;
|
|
3328
|
+
}
|
|
3329
|
+
const out = /* @__PURE__ */ new Map();
|
|
3330
|
+
for (const [key, child] of value) {
|
|
3331
|
+
out.set(normalizeKey(key), normalizeYamlTree(child));
|
|
3332
|
+
}
|
|
3333
|
+
return out;
|
|
3334
|
+
}
|
|
2934
3335
|
function parseYamlObject(content) {
|
|
2935
3336
|
let parsed;
|
|
2936
3337
|
try {
|
|
2937
|
-
parsed = yaml.parse(content, { maxAliasCount: 100 });
|
|
3338
|
+
parsed = yaml.parse(content, { mapAsMap: true, maxAliasCount: 100 });
|
|
2938
3339
|
} catch {
|
|
2939
3340
|
throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
|
|
2940
3341
|
}
|
|
2941
|
-
|
|
3342
|
+
assertWithinDepth(parsed, MAX_DEPTH);
|
|
3343
|
+
return assertJsonRecord(normalizeYamlTree(parsed));
|
|
2942
3344
|
}
|
|
2943
3345
|
function createYamlAdapter() {
|
|
2944
3346
|
return createTreeFileAdapter({
|
|
@@ -2969,6 +3371,15 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
|
|
|
2969
3371
|
);
|
|
2970
3372
|
}
|
|
2971
3373
|
|
|
3374
|
+
// src/flow/read-target.ts
|
|
3375
|
+
async function readTargetResource(input) {
|
|
3376
|
+
const path = localeFilePath(input.cwd, input.filesPattern, input.locale);
|
|
3377
|
+
if (!await input.fs.fileExists(path)) {
|
|
3378
|
+
return { locale: input.locale, namespace: "", format: input.format, entries: /* @__PURE__ */ new Map() };
|
|
3379
|
+
}
|
|
3380
|
+
return (await input.adapter.read(path, input.locale)).resource;
|
|
3381
|
+
}
|
|
3382
|
+
|
|
2972
3383
|
// src/flow/select-locales.ts
|
|
2973
3384
|
function selectLocales(config, requested) {
|
|
2974
3385
|
if (requested === void 0) {
|
|
@@ -3009,11 +3420,14 @@ async function readSource(config, cwd, fs, adapter) {
|
|
|
3009
3420
|
|
|
3010
3421
|
// src/flow/diff-locales.ts
|
|
3011
3422
|
async function readTarget(cwd, config, adapter, fs, locale) {
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3423
|
+
return readTargetResource({
|
|
3424
|
+
cwd,
|
|
3425
|
+
filesPattern: config.files.pattern,
|
|
3426
|
+
format: config.format,
|
|
3427
|
+
locale,
|
|
3428
|
+
adapter,
|
|
3429
|
+
fs
|
|
3430
|
+
});
|
|
3017
3431
|
}
|
|
3018
3432
|
async function diffLocales(input, deps = {}) {
|
|
3019
3433
|
const config = input.config;
|
|
@@ -3063,6 +3477,166 @@ async function diff(input, deps = {}) {
|
|
|
3063
3477
|
return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
|
|
3064
3478
|
}
|
|
3065
3479
|
|
|
3480
|
+
// src/flow/integrity-gate.ts
|
|
3481
|
+
function gateCandidateValue(sourceEntry, candidateValue, adapter) {
|
|
3482
|
+
const placeholderResult = adapter.comparePlaceholders?.(sourceEntry.value, candidateValue) ?? checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(candidateValue));
|
|
3483
|
+
if (!placeholderResult.matches) {
|
|
3484
|
+
return { accepted: false, reason: "placeholder" };
|
|
3485
|
+
}
|
|
3486
|
+
if (!adapter.validateMessage(candidateValue)) {
|
|
3487
|
+
return { accepted: false, reason: "icu" };
|
|
3488
|
+
}
|
|
3489
|
+
return { accepted: true };
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// src/flow/edit-entry.ts
|
|
3493
|
+
async function editEntry(input, deps = {}) {
|
|
3494
|
+
const config = input.config;
|
|
3495
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3496
|
+
const fs = deps.fs ?? defaultFs;
|
|
3497
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3498
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3499
|
+
if (locale === void 0) {
|
|
3500
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3501
|
+
}
|
|
3502
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3503
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3504
|
+
if (sourceEntry === void 0) {
|
|
3505
|
+
throw new SdkError(
|
|
3506
|
+
"UNKNOWN_KEY",
|
|
3507
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
3511
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3512
|
+
const gate = gateCandidateValue(sourceEntry, input.value, adapter);
|
|
3513
|
+
if (!gate.accepted) {
|
|
3514
|
+
return { accepted: false, reason: gate.reason, value: input.value };
|
|
3515
|
+
}
|
|
3516
|
+
const merged = new Map(target.entries);
|
|
3517
|
+
merged.set(input.key, { ...sourceEntry, value: input.value, namespace: target.namespace });
|
|
3518
|
+
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
3519
|
+
await adapter.write(
|
|
3520
|
+
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
3521
|
+
path
|
|
3522
|
+
);
|
|
3523
|
+
await updateLockFileLocale(cwd, fs, locale, {
|
|
3524
|
+
mode: "merge",
|
|
3525
|
+
entries: { [input.key]: contentHash(sourceEntry) }
|
|
3526
|
+
});
|
|
3527
|
+
return { accepted: true, value: input.value };
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
// src/flow/key-integrity.ts
|
|
3532
|
+
function checkEntryIntegrity(adapter, sourceEntry, targetEntry) {
|
|
3533
|
+
const result = adapter.comparePlaceholders?.(sourceEntry.value, targetEntry.value) ?? checkPlaceholders(sourceEntry.placeholders, targetEntry.placeholders);
|
|
3534
|
+
return {
|
|
3535
|
+
key: sourceEntry.key,
|
|
3536
|
+
hasPlaceholders: sourceEntry.placeholders.length > 0,
|
|
3537
|
+
matches: result.matches,
|
|
3538
|
+
missing: result.missing,
|
|
3539
|
+
extra: result.extra,
|
|
3540
|
+
icuValid: adapter.validateMessage(targetEntry.value)
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
function selectChangedKeys(changed, requested) {
|
|
3544
|
+
if (requested === void 0) {
|
|
3545
|
+
return changed;
|
|
3546
|
+
}
|
|
3547
|
+
const wanted = new Set(requested);
|
|
3548
|
+
return changed.filter((key) => wanted.has(key));
|
|
3549
|
+
}
|
|
3550
|
+
function integrityEntriesFor(source, target, adapter, changedKeys) {
|
|
3551
|
+
const entries = [];
|
|
3552
|
+
for (const key of changedKeys) {
|
|
3553
|
+
const sourceEntry = source.entries.get(key);
|
|
3554
|
+
const targetEntry = target.entries.get(key);
|
|
3555
|
+
if (sourceEntry === void 0 || targetEntry === void 0) {
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
entries.push(checkEntryIntegrity(adapter, sourceEntry, targetEntry));
|
|
3559
|
+
}
|
|
3560
|
+
return entries;
|
|
3561
|
+
}
|
|
3562
|
+
async function keyIntegrity(input, deps = {}) {
|
|
3563
|
+
const config = input.config;
|
|
3564
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3565
|
+
const fs = deps.fs ?? defaultFs;
|
|
3566
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3567
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3568
|
+
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
3569
|
+
return Promise.all(
|
|
3570
|
+
selectLocales(config, input.locales).map(async (locale) => {
|
|
3571
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3572
|
+
const diffResult = diffResources(source.resource, target, {
|
|
3573
|
+
baseline: baselineFor(lock, locale)
|
|
3574
|
+
});
|
|
3575
|
+
const changedKeys = selectChangedKeys(diffResult.changed, input.keys);
|
|
3576
|
+
const entries = integrityEntriesFor(source.resource, target, adapter, changedKeys);
|
|
3577
|
+
return { locale, entries };
|
|
3578
|
+
})
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// src/flow/key-value.ts
|
|
3583
|
+
async function keyValue(input, deps = {}) {
|
|
3584
|
+
const config = input.config;
|
|
3585
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3586
|
+
const fs = deps.fs ?? defaultFs;
|
|
3587
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3588
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3589
|
+
if (locale === void 0) {
|
|
3590
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3591
|
+
}
|
|
3592
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3593
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3594
|
+
if (sourceEntry === void 0) {
|
|
3595
|
+
throw new SdkError(
|
|
3596
|
+
"UNKNOWN_KEY",
|
|
3597
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3601
|
+
const targetEntry = target.entries.get(input.key);
|
|
3602
|
+
return {
|
|
3603
|
+
source: sourceEntry.value,
|
|
3604
|
+
...targetEntry !== void 0 ? { target: targetEntry.value } : {}
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// src/flow/locale-snapshot.ts
|
|
3609
|
+
async function readLocaleFileSnapshot(input, deps = {}) {
|
|
3610
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3611
|
+
const fs = deps.fs ?? defaultFs;
|
|
3612
|
+
const adapter = selectAdapter(input.config.format, deps.adapterRegistry);
|
|
3613
|
+
const resource = await readTarget(cwd, input.config, adapter, fs, input.locale);
|
|
3614
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
3615
|
+
for (const [key, entry] of resource.entries) {
|
|
3616
|
+
hashes.set(key, contentHash(entry));
|
|
3617
|
+
}
|
|
3618
|
+
return { locale: input.locale, hashes };
|
|
3619
|
+
}
|
|
3620
|
+
function diffLocaleSnapshots(previous, current) {
|
|
3621
|
+
let added = 0;
|
|
3622
|
+
let changed = 0;
|
|
3623
|
+
for (const [key, hash] of current.hashes) {
|
|
3624
|
+
const previousHash = previous.hashes.get(key);
|
|
3625
|
+
if (previousHash === void 0) {
|
|
3626
|
+
added += 1;
|
|
3627
|
+
} else if (previousHash !== hash) {
|
|
3628
|
+
changed += 1;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
let removed = 0;
|
|
3632
|
+
for (const key of previous.hashes.keys()) {
|
|
3633
|
+
if (!current.hashes.has(key)) {
|
|
3634
|
+
removed += 1;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
return { added, changed, removed };
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3066
3640
|
// src/flow/lock-state.ts
|
|
3067
3641
|
function toLockLocaleState(locale, keyCount, diff2) {
|
|
3068
3642
|
return {
|
|
@@ -3110,6 +3684,214 @@ function selectProvider(config, createProvider = buildProvider) {
|
|
|
3110
3684
|
}
|
|
3111
3685
|
}
|
|
3112
3686
|
|
|
3687
|
+
// src/flow/retranslate-entry.ts
|
|
3688
|
+
function buildSingleEntryRequest(config, locale, sourceEntry, adapter) {
|
|
3689
|
+
return {
|
|
3690
|
+
sourceLocale: config.sourceLocale,
|
|
3691
|
+
targetLocale: locale,
|
|
3692
|
+
entries: [sourceEntry],
|
|
3693
|
+
extractPlaceholders: adapter.extractPlaceholders,
|
|
3694
|
+
...config.glossary !== void 0 ? { glossary: config.glossary } : {},
|
|
3695
|
+
...config.tone !== void 0 ? { tone: config.tone } : {},
|
|
3696
|
+
...adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: adapter.comparePlaceholders } : {}
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
async function retranslateEntry(input, deps = {}) {
|
|
3700
|
+
const config = input.config;
|
|
3701
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3702
|
+
const fs = deps.fs ?? defaultFs;
|
|
3703
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3704
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3705
|
+
if (locale === void 0) {
|
|
3706
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3707
|
+
}
|
|
3708
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3709
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3710
|
+
if (sourceEntry === void 0) {
|
|
3711
|
+
throw new SdkError(
|
|
3712
|
+
"UNKNOWN_KEY",
|
|
3713
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3714
|
+
);
|
|
3715
|
+
}
|
|
3716
|
+
const provider = selectProvider(config.provider, deps.createProvider);
|
|
3717
|
+
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
3718
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3719
|
+
const result = await provider.translateBatch(
|
|
3720
|
+
buildSingleEntryRequest(config, locale, sourceEntry, adapter)
|
|
3721
|
+
);
|
|
3722
|
+
const value = result.values.get(input.key);
|
|
3723
|
+
if (value === void 0) {
|
|
3724
|
+
throw new ProviderError(
|
|
3725
|
+
"INVALID_RESPONSE",
|
|
3726
|
+
`The provider returned no translated value for key "${input.key}".`
|
|
3727
|
+
);
|
|
3728
|
+
}
|
|
3729
|
+
const gate = gateCandidateValue(sourceEntry, value, adapter);
|
|
3730
|
+
if (!gate.accepted) {
|
|
3731
|
+
return { accepted: false, reason: gate.reason, value };
|
|
3732
|
+
}
|
|
3733
|
+
const merged = new Map(target.entries);
|
|
3734
|
+
merged.set(input.key, { ...sourceEntry, value, namespace: target.namespace });
|
|
3735
|
+
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
3736
|
+
await adapter.write(
|
|
3737
|
+
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
3738
|
+
path
|
|
3739
|
+
);
|
|
3740
|
+
await updateLockFileLocale(cwd, fs, locale, {
|
|
3741
|
+
mode: "merge",
|
|
3742
|
+
entries: { [input.key]: contentHash(sourceEntry) }
|
|
3743
|
+
});
|
|
3744
|
+
const reviewReasons = result.reviewFlags?.get(input.key)?.reasons ?? [];
|
|
3745
|
+
return { accepted: true, value, reviewReasons };
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
var RUN_STATUS_DIR_NAME = ".verbatra-local";
|
|
3749
|
+
var RUN_STATUS_FILE_NAME = "run-status.json";
|
|
3750
|
+
var CURRENT_VERSION2 = 1;
|
|
3751
|
+
var MAX_RUN_STATUS_FILE_BYTES = 16 * 1024 * 1024;
|
|
3752
|
+
var reviewReasonCodeSchema = zod.z.enum([
|
|
3753
|
+
"LENGTH_RATIO_OUTLIER",
|
|
3754
|
+
"EQUALS_SOURCE",
|
|
3755
|
+
"GLOSSARY_TERM_MISSED",
|
|
3756
|
+
"INTEGRITY_REORDERED",
|
|
3757
|
+
"PROVIDER_DEGRADED"
|
|
3758
|
+
]);
|
|
3759
|
+
var needsReviewEntrySchema = zod.z.object({
|
|
3760
|
+
key: zod.z.string(),
|
|
3761
|
+
reasons: zod.z.array(reviewReasonCodeSchema)
|
|
3762
|
+
});
|
|
3763
|
+
var usageSummarySchema = zod.z.object({
|
|
3764
|
+
inputTokens: zod.z.number().int().nonnegative(),
|
|
3765
|
+
outputTokens: zod.z.number().int().nonnegative()
|
|
3766
|
+
});
|
|
3767
|
+
var runBudgetSchema = zod.z.object({
|
|
3768
|
+
maxTokens: zod.z.number().int().nonnegative(),
|
|
3769
|
+
behavior: zod.z.enum(["warn", "stop"]),
|
|
3770
|
+
supported: zod.z.boolean(),
|
|
3771
|
+
tokensUsed: zod.z.number().int().nonnegative(),
|
|
3772
|
+
exceeded: zod.z.boolean()
|
|
3773
|
+
});
|
|
3774
|
+
var runStatusLocaleSchema = zod.z.object({
|
|
3775
|
+
locale: zod.z.string(),
|
|
3776
|
+
status: zod.z.enum(["succeeded", "failed"]),
|
|
3777
|
+
needsReview: zod.z.array(needsReviewEntrySchema),
|
|
3778
|
+
usage: usageSummarySchema.optional()
|
|
3779
|
+
});
|
|
3780
|
+
var runStatusFileSchema = zod.z.object({
|
|
3781
|
+
version: zod.z.number().int().positive(),
|
|
3782
|
+
generatedAt: zod.z.string(),
|
|
3783
|
+
usage: usageSummarySchema.optional(),
|
|
3784
|
+
budget: runBudgetSchema.optional(),
|
|
3785
|
+
locales: zod.z.array(runStatusLocaleSchema)
|
|
3786
|
+
});
|
|
3787
|
+
function runStatusFilePath(cwd) {
|
|
3788
|
+
return path.resolve(cwd, RUN_STATUS_DIR_NAME, RUN_STATUS_FILE_NAME);
|
|
3789
|
+
}
|
|
3790
|
+
function toRunStatusLocale(locale) {
|
|
3791
|
+
return {
|
|
3792
|
+
locale: locale.locale,
|
|
3793
|
+
status: locale.status,
|
|
3794
|
+
needsReview: locale.needsReview,
|
|
3795
|
+
...locale.usage !== void 0 ? { usage: locale.usage } : {}
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
function buildRunStatusFile(summary, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3799
|
+
return {
|
|
3800
|
+
version: CURRENT_VERSION2,
|
|
3801
|
+
generatedAt,
|
|
3802
|
+
...summary.usage !== void 0 ? { usage: summary.usage } : {},
|
|
3803
|
+
...summary.budget !== void 0 ? { budget: summary.budget } : {},
|
|
3804
|
+
locales: summary.locales.map(toRunStatusLocale)
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
function fromParsed(data) {
|
|
3808
|
+
return {
|
|
3809
|
+
version: data.version,
|
|
3810
|
+
generatedAt: data.generatedAt,
|
|
3811
|
+
...data.usage !== void 0 ? { usage: data.usage } : {},
|
|
3812
|
+
...data.budget !== void 0 ? { budget: data.budget } : {},
|
|
3813
|
+
locales: data.locales.map((locale) => ({
|
|
3814
|
+
locale: locale.locale,
|
|
3815
|
+
status: locale.status,
|
|
3816
|
+
needsReview: locale.needsReview,
|
|
3817
|
+
...locale.usage !== void 0 ? { usage: locale.usage } : {}
|
|
3818
|
+
}))
|
|
3819
|
+
};
|
|
3820
|
+
}
|
|
3821
|
+
async function readRunStatusFile(path, fs) {
|
|
3822
|
+
const read = await fs.readFileBounded(path, MAX_RUN_STATUS_FILE_BYTES);
|
|
3823
|
+
if (read.kind !== "ok") {
|
|
3824
|
+
return void 0;
|
|
3825
|
+
}
|
|
3826
|
+
let parsed;
|
|
3827
|
+
try {
|
|
3828
|
+
parsed = JSON.parse(read.content);
|
|
3829
|
+
} catch {
|
|
3830
|
+
return void 0;
|
|
3831
|
+
}
|
|
3832
|
+
const result = runStatusFileSchema.safeParse(parsed);
|
|
3833
|
+
if (!result.success || result.data.version !== CURRENT_VERSION2) {
|
|
3834
|
+
return void 0;
|
|
3835
|
+
}
|
|
3836
|
+
return fromParsed(result.data);
|
|
3837
|
+
}
|
|
3838
|
+
async function writeRunStatusFile(path$1, data, fs) {
|
|
3839
|
+
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
3840
|
+
await fs.writeFile(path$1, `${JSON.stringify(data, null, 2)}
|
|
3841
|
+
`);
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
// src/flow/run-status.ts
|
|
3845
|
+
async function runStatus(input = {}, deps = {}) {
|
|
3846
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3847
|
+
const fs = deps.fs ?? defaultFs;
|
|
3848
|
+
const file = await readRunStatusFile(runStatusFilePath(cwd), fs);
|
|
3849
|
+
if (file === void 0) {
|
|
3850
|
+
return { available: false };
|
|
3851
|
+
}
|
|
3852
|
+
return { available: true, ...file };
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// src/flow/budget.ts
|
|
3856
|
+
function createBudgetTracker(maxTokens, behavior) {
|
|
3857
|
+
return { maxTokens, behavior, tokensUsed: 0, usageSeen: false, exceeded: false, stopped: false };
|
|
3858
|
+
}
|
|
3859
|
+
function foldTrackerUsage(tracker, usage) {
|
|
3860
|
+
if (usage === void 0) {
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
tracker.usageSeen = true;
|
|
3864
|
+
tracker.tokensUsed += usage.inputTokens + usage.outputTokens;
|
|
3865
|
+
}
|
|
3866
|
+
function checkBudgetTrip(tracker) {
|
|
3867
|
+
if (tracker.maxTokens === void 0 || tracker.exceeded || tracker.tokensUsed < tracker.maxTokens) {
|
|
3868
|
+
return false;
|
|
3869
|
+
}
|
|
3870
|
+
tracker.exceeded = true;
|
|
3871
|
+
if (tracker.behavior === "stop") {
|
|
3872
|
+
tracker.stopped = true;
|
|
3873
|
+
}
|
|
3874
|
+
return true;
|
|
3875
|
+
}
|
|
3876
|
+
function toBudgetSummary(tracker) {
|
|
3877
|
+
if (tracker.maxTokens === void 0) {
|
|
3878
|
+
return void 0;
|
|
3879
|
+
}
|
|
3880
|
+
return {
|
|
3881
|
+
maxTokens: tracker.maxTokens,
|
|
3882
|
+
behavior: tracker.behavior,
|
|
3883
|
+
supported: tracker.usageSeen,
|
|
3884
|
+
tokensUsed: tracker.tokensUsed,
|
|
3885
|
+
exceeded: tracker.exceeded
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
function budgetExceededNotice(tracker) {
|
|
3889
|
+
return {
|
|
3890
|
+
code: "BUDGET_TOKENS_EXCEEDED",
|
|
3891
|
+
message: `The run's cumulative token usage (${tracker.tokensUsed}) reached the configured budget of ${tracker.maxTokens} tokens (behavior: ${tracker.behavior}).`
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3113
3895
|
// src/flow/locale-failure.ts
|
|
3114
3896
|
function describeError(error) {
|
|
3115
3897
|
if (error instanceof Error) {
|
|
@@ -3129,8 +3911,10 @@ function failureSummary(locale, error) {
|
|
|
3129
3911
|
invalidIcuSource: [],
|
|
3130
3912
|
integrityMismatches: [],
|
|
3131
3913
|
providerFailures: [],
|
|
3914
|
+
budgetWithheld: [],
|
|
3132
3915
|
generated: [],
|
|
3133
3916
|
notices: [],
|
|
3917
|
+
needsReview: [],
|
|
3134
3918
|
error: describeError(error)
|
|
3135
3919
|
};
|
|
3136
3920
|
}
|
|
@@ -3300,7 +4084,43 @@ function planPluralGeneration(source, targetLocale, format) {
|
|
|
3300
4084
|
return { items };
|
|
3301
4085
|
}
|
|
3302
4086
|
|
|
4087
|
+
// src/flow/usage.ts
|
|
4088
|
+
function createUsageAccumulator() {
|
|
4089
|
+
return { total: void 0 };
|
|
4090
|
+
}
|
|
4091
|
+
function foldUsage(accumulator, usage) {
|
|
4092
|
+
if (usage === void 0) {
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
const prior = accumulator.total ?? { inputTokens: 0, outputTokens: 0 };
|
|
4096
|
+
accumulator.total = {
|
|
4097
|
+
inputTokens: prior.inputTokens + usage.inputTokens,
|
|
4098
|
+
outputTokens: prior.outputTokens + usage.outputTokens
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
function combineUsage(a, b) {
|
|
4102
|
+
if (a === void 0) {
|
|
4103
|
+
return b;
|
|
4104
|
+
}
|
|
4105
|
+
if (b === void 0) {
|
|
4106
|
+
return a;
|
|
4107
|
+
}
|
|
4108
|
+
return {
|
|
4109
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
4110
|
+
outputTokens: a.outputTokens + b.outputTokens
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
4113
|
+
|
|
3303
4114
|
// src/flow/plural-generation.ts
|
|
4115
|
+
var EMPTY_RESULT = {
|
|
4116
|
+
accepted: [],
|
|
4117
|
+
withheld: [],
|
|
4118
|
+
providerFailures: [],
|
|
4119
|
+
budgetWithheld: [],
|
|
4120
|
+
notices: [],
|
|
4121
|
+
usage: void 0,
|
|
4122
|
+
tripped: false
|
|
4123
|
+
};
|
|
3304
4124
|
function generatedLockHash(governingEntries, category) {
|
|
3305
4125
|
const governingHashes = governingEntries.map(contentHash).sort();
|
|
3306
4126
|
return contentHash({
|
|
@@ -3314,7 +4134,6 @@ function syntheticEntry(item) {
|
|
|
3314
4134
|
...item.sourceEntry,
|
|
3315
4135
|
key: item.targetKey,
|
|
3316
4136
|
isPlural: true,
|
|
3317
|
-
// The CLDR category travels as data context (the meaning field), never the instruction channel.
|
|
3318
4137
|
meaning: `CLDR plural category "${item.category}"`
|
|
3319
4138
|
};
|
|
3320
4139
|
}
|
|
@@ -3339,23 +4158,45 @@ async function generatePluralForms(context) {
|
|
|
3339
4158
|
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
3340
4159
|
const stale = staleItems(plan.items, context.baseline);
|
|
3341
4160
|
if (stale.length === 0) {
|
|
3342
|
-
return
|
|
4161
|
+
return EMPTY_RESULT;
|
|
3343
4162
|
}
|
|
3344
4163
|
const accepted = [];
|
|
3345
4164
|
const withheld = [];
|
|
3346
4165
|
const providerFailures = [];
|
|
4166
|
+
const budgetWithheld = [];
|
|
3347
4167
|
const notices = [];
|
|
4168
|
+
const usage = createUsageAccumulator();
|
|
4169
|
+
let tripped = false;
|
|
3348
4170
|
for (const batch of chunk(stale, context.maxBatchSize)) {
|
|
3349
|
-
|
|
4171
|
+
if (context.budget.stopped) {
|
|
4172
|
+
for (const item of batch) {
|
|
4173
|
+
budgetWithheld.push(item.targetKey);
|
|
4174
|
+
}
|
|
4175
|
+
continue;
|
|
4176
|
+
}
|
|
4177
|
+
const subResult = await runGenerationSubBatch(
|
|
3350
4178
|
context,
|
|
3351
4179
|
batch,
|
|
3352
4180
|
accepted,
|
|
3353
4181
|
withheld,
|
|
3354
4182
|
providerFailures
|
|
3355
4183
|
);
|
|
3356
|
-
notices.push(...
|
|
4184
|
+
notices.push(...subResult.notices);
|
|
4185
|
+
foldUsage(usage, subResult.usage);
|
|
4186
|
+
foldTrackerUsage(context.budget, subResult.usage);
|
|
4187
|
+
if (checkBudgetTrip(context.budget)) {
|
|
4188
|
+
tripped = true;
|
|
4189
|
+
}
|
|
3357
4190
|
}
|
|
3358
|
-
return {
|
|
4191
|
+
return {
|
|
4192
|
+
accepted,
|
|
4193
|
+
withheld,
|
|
4194
|
+
providerFailures,
|
|
4195
|
+
budgetWithheld,
|
|
4196
|
+
notices,
|
|
4197
|
+
usage: usage.total,
|
|
4198
|
+
tripped
|
|
4199
|
+
};
|
|
3359
4200
|
}
|
|
3360
4201
|
async function runGenerationSubBatch(context, batch, accepted, withheld, providerFailures) {
|
|
3361
4202
|
let result;
|
|
@@ -3366,20 +4207,20 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3366
4207
|
for (const item of batch) {
|
|
3367
4208
|
providerFailures.push(item.targetKey);
|
|
3368
4209
|
}
|
|
3369
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
4210
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3370
4211
|
}
|
|
3371
4212
|
for (const item of batch) {
|
|
3372
|
-
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
4213
|
+
foldGenerationItem(item, result, context.adapter, accepted, withheld, providerFailures);
|
|
3373
4214
|
}
|
|
3374
|
-
return readNotices(result);
|
|
4215
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3375
4216
|
}
|
|
3376
|
-
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
4217
|
+
function foldGenerationItem(item, result, adapter, accepted, withheld, providerFailures) {
|
|
3377
4218
|
const value = result.values.get(item.targetKey);
|
|
3378
4219
|
if (value === void 0) {
|
|
3379
4220
|
providerFailures.push(item.targetKey);
|
|
3380
4221
|
return;
|
|
3381
4222
|
}
|
|
3382
|
-
if (
|
|
4223
|
+
if (gateCandidateValue(item.sourceEntry, value, adapter).accepted) {
|
|
3383
4224
|
accepted.push({
|
|
3384
4225
|
targetKey: item.targetKey,
|
|
3385
4226
|
entry: { ...syntheticEntry(item), value },
|
|
@@ -3391,16 +4232,6 @@ function foldGenerationItem(item, result, accepted, withheld, providerFailures)
|
|
|
3391
4232
|
}
|
|
3392
4233
|
|
|
3393
4234
|
// src/flow/locale-run.ts
|
|
3394
|
-
function emptyResource(locale, format) {
|
|
3395
|
-
return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
|
|
3396
|
-
}
|
|
3397
|
-
async function readTarget2(params) {
|
|
3398
|
-
const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
|
|
3399
|
-
if (!await params.fs.fileExists(path)) {
|
|
3400
|
-
return emptyResource(params.targetLocale, params.format);
|
|
3401
|
-
}
|
|
3402
|
-
return (await params.adapter.read(path, params.targetLocale)).resource;
|
|
3403
|
-
}
|
|
3404
4235
|
function buildRequest3(params, entries) {
|
|
3405
4236
|
return {
|
|
3406
4237
|
sourceLocale: params.sourceLocale,
|
|
@@ -3413,7 +4244,14 @@ function buildRequest3(params, entries) {
|
|
|
3413
4244
|
};
|
|
3414
4245
|
}
|
|
3415
4246
|
async function runLocale(params) {
|
|
3416
|
-
const target = await
|
|
4247
|
+
const target = await readTargetResource({
|
|
4248
|
+
cwd: params.cwd,
|
|
4249
|
+
filesPattern: params.filesPattern,
|
|
4250
|
+
format: params.format,
|
|
4251
|
+
locale: params.targetLocale,
|
|
4252
|
+
adapter: params.adapter,
|
|
4253
|
+
fs: params.fs
|
|
4254
|
+
});
|
|
3417
4255
|
const diff2 = diffResources(params.source, target, { baseline: params.baseline });
|
|
3418
4256
|
const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
|
|
3419
4257
|
const pruned = params.prune ? orphaned : [];
|
|
@@ -3439,6 +4277,7 @@ async function runLocale(params) {
|
|
|
3439
4277
|
generated: [],
|
|
3440
4278
|
integrityMismatches: [],
|
|
3441
4279
|
providerFailures: [],
|
|
4280
|
+
budgetWithheld: [],
|
|
3442
4281
|
pruned,
|
|
3443
4282
|
notices: sdkNotices
|
|
3444
4283
|
}),
|
|
@@ -3446,23 +4285,31 @@ async function runLocale(params) {
|
|
|
3446
4285
|
};
|
|
3447
4286
|
}
|
|
3448
4287
|
const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
|
|
4288
|
+
const startedStopped = params.budget.stopped;
|
|
3449
4289
|
const accepted = /* @__PURE__ */ new Map();
|
|
3450
4290
|
const integrityMismatches = [];
|
|
3451
4291
|
const providerFailures = [];
|
|
3452
|
-
const
|
|
4292
|
+
const budgetWithheld = [];
|
|
4293
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
4294
|
+
const translation = await translateAndCheck(
|
|
3453
4295
|
provider,
|
|
3454
4296
|
params,
|
|
3455
4297
|
entries,
|
|
3456
4298
|
accepted,
|
|
3457
4299
|
integrityMismatches,
|
|
3458
|
-
providerFailures
|
|
4300
|
+
providerFailures,
|
|
4301
|
+
budgetWithheld,
|
|
4302
|
+
reviewFlags
|
|
3459
4303
|
);
|
|
3460
4304
|
const merged = new Map(target.entries);
|
|
3461
4305
|
for (const key of pruned) {
|
|
3462
4306
|
merged.delete(key);
|
|
3463
4307
|
}
|
|
3464
|
-
for (const
|
|
3465
|
-
|
|
4308
|
+
for (const key of params.source.entries.keys()) {
|
|
4309
|
+
const hit = accepted.get(key);
|
|
4310
|
+
if (hit !== void 0) {
|
|
4311
|
+
merged.set(key, { ...hit.source, value: hit.value, namespace: target.namespace });
|
|
4312
|
+
}
|
|
3466
4313
|
}
|
|
3467
4314
|
const generation = await runGeneration(params, provider);
|
|
3468
4315
|
for (const form of generation.accepted) {
|
|
@@ -3481,16 +4328,19 @@ async function runLocale(params) {
|
|
|
3481
4328
|
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
3482
4329
|
const notices = [
|
|
3483
4330
|
...pluralNotices,
|
|
3484
|
-
...
|
|
3485
|
-
...generation.notices
|
|
4331
|
+
...translation.notices,
|
|
4332
|
+
...generation.notices,
|
|
4333
|
+
...budgetLocaleNotices(params.budget, startedStopped, translation.tripped, generation.tripped)
|
|
3486
4334
|
];
|
|
3487
4335
|
const withheld = /* @__PURE__ */ new Set([
|
|
3488
4336
|
...integrityMismatches,
|
|
3489
4337
|
...providerFailures,
|
|
3490
4338
|
...invalidIcuSource,
|
|
3491
4339
|
...generation.withheld,
|
|
3492
|
-
...generation.providerFailures
|
|
4340
|
+
...generation.providerFailures,
|
|
4341
|
+
...budgetWithheld
|
|
3493
4342
|
]);
|
|
4343
|
+
const localeUsage = combineUsage(translation.usage, generation.usage);
|
|
3494
4344
|
return {
|
|
3495
4345
|
summary: baseSummary({
|
|
3496
4346
|
locale: params.targetLocale,
|
|
@@ -3499,19 +4349,29 @@ async function runLocale(params) {
|
|
|
3499
4349
|
invalidIcuSource,
|
|
3500
4350
|
translated: [...accepted.keys()],
|
|
3501
4351
|
generated: generation.accepted.map((form) => form.targetKey).sort(),
|
|
3502
|
-
// Withheld generated forms surface alongside withheld translations: both failed integrity.
|
|
3503
4352
|
integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
|
|
3504
|
-
// A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
|
|
3505
4353
|
providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
|
|
4354
|
+
budgetWithheld: [...budgetWithheld, ...generation.budgetWithheld].sort(),
|
|
3506
4355
|
pruned,
|
|
3507
|
-
notices
|
|
4356
|
+
notices,
|
|
4357
|
+
needsReview: needsReviewFor(accepted.keys(), reviewFlags),
|
|
4358
|
+
...localeUsage !== void 0 ? { usage: localeUsage } : {}
|
|
3508
4359
|
}),
|
|
3509
4360
|
lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
|
|
3510
4361
|
};
|
|
3511
4362
|
}
|
|
4363
|
+
var NO_GENERATION_RESULT = {
|
|
4364
|
+
accepted: [],
|
|
4365
|
+
withheld: [],
|
|
4366
|
+
providerFailures: [],
|
|
4367
|
+
budgetWithheld: [],
|
|
4368
|
+
notices: [],
|
|
4369
|
+
usage: void 0,
|
|
4370
|
+
tripped: false
|
|
4371
|
+
};
|
|
3512
4372
|
async function runGeneration(params, provider) {
|
|
3513
4373
|
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
3514
|
-
return
|
|
4374
|
+
return NO_GENERATION_RESULT;
|
|
3515
4375
|
}
|
|
3516
4376
|
return generatePluralForms({
|
|
3517
4377
|
source: params.source,
|
|
@@ -3523,9 +4383,13 @@ async function runGeneration(params, provider) {
|
|
|
3523
4383
|
glossary: params.glossary,
|
|
3524
4384
|
tone: params.tone,
|
|
3525
4385
|
baseline: params.baseline,
|
|
3526
|
-
maxBatchSize: params.maxBatchSize
|
|
4386
|
+
maxBatchSize: params.maxBatchSize,
|
|
4387
|
+
budget: params.budget
|
|
3527
4388
|
});
|
|
3528
4389
|
}
|
|
4390
|
+
function budgetLocaleNotices(budget, startedStopped, mainTripped, generationTripped) {
|
|
4391
|
+
return startedStopped || mainTripped || generationTripped ? [budgetExceededNotice(budget)] : [];
|
|
4392
|
+
}
|
|
3529
4393
|
function pluralNoticeFor(params, merged) {
|
|
3530
4394
|
if (params.format !== "i18next-json") {
|
|
3531
4395
|
return [];
|
|
@@ -3546,26 +4410,53 @@ function baseSummary(parts) {
|
|
|
3546
4410
|
invalidIcuSource: parts.invalidIcuSource,
|
|
3547
4411
|
integrityMismatches: parts.integrityMismatches,
|
|
3548
4412
|
providerFailures: parts.providerFailures,
|
|
4413
|
+
budgetWithheld: parts.budgetWithheld,
|
|
3549
4414
|
generated: parts.generated,
|
|
3550
|
-
notices: parts.notices
|
|
4415
|
+
notices: parts.notices,
|
|
4416
|
+
needsReview: parts.needsReview ?? [],
|
|
4417
|
+
...parts.usage !== void 0 ? { usage: parts.usage } : {}
|
|
3551
4418
|
};
|
|
3552
4419
|
}
|
|
3553
|
-
|
|
4420
|
+
function needsReviewFor(acceptedKeys, reviewFlags) {
|
|
4421
|
+
const entries = [];
|
|
4422
|
+
for (const key of acceptedKeys) {
|
|
4423
|
+
const flag = reviewFlags.get(key);
|
|
4424
|
+
if (flag !== void 0) {
|
|
4425
|
+
entries.push({ key, reasons: flag.reasons });
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
return entries.sort((a, b) => a.key < b.key ? -1 : 1);
|
|
4429
|
+
}
|
|
4430
|
+
async function translateAndCheck(provider, params, entries, accepted, integrityMismatches, providerFailures, budgetWithheld, reviewFlags) {
|
|
3554
4431
|
const notices = [];
|
|
4432
|
+
const usage = createUsageAccumulator();
|
|
4433
|
+
let tripped = false;
|
|
3555
4434
|
for (const batch of chunk(entries, params.maxBatchSize)) {
|
|
3556
|
-
|
|
4435
|
+
if (params.budget.stopped) {
|
|
4436
|
+
for (const entry of batch) {
|
|
4437
|
+
budgetWithheld.push(entry.key);
|
|
4438
|
+
}
|
|
4439
|
+
continue;
|
|
4440
|
+
}
|
|
4441
|
+
const subResult = await runSubBatch(
|
|
3557
4442
|
provider,
|
|
3558
4443
|
params,
|
|
3559
4444
|
batch,
|
|
3560
4445
|
accepted,
|
|
3561
4446
|
integrityMismatches,
|
|
3562
|
-
providerFailures
|
|
4447
|
+
providerFailures,
|
|
4448
|
+
reviewFlags
|
|
3563
4449
|
);
|
|
3564
|
-
notices.push(...
|
|
4450
|
+
notices.push(...subResult.notices);
|
|
4451
|
+
foldUsage(usage, subResult.usage);
|
|
4452
|
+
foldTrackerUsage(params.budget, subResult.usage);
|
|
4453
|
+
if (checkBudgetTrip(params.budget)) {
|
|
4454
|
+
tripped = true;
|
|
4455
|
+
}
|
|
3565
4456
|
}
|
|
3566
|
-
return notices;
|
|
4457
|
+
return { notices, tripped, usage: usage.total };
|
|
3567
4458
|
}
|
|
3568
|
-
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures) {
|
|
4459
|
+
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures, reviewFlags) {
|
|
3569
4460
|
let result;
|
|
3570
4461
|
try {
|
|
3571
4462
|
result = await provider.translateBatch(buildRequest3(params, batch));
|
|
@@ -3573,20 +4464,25 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3573
4464
|
for (const entry of batch) {
|
|
3574
4465
|
providerFailures.push(entry.key);
|
|
3575
4466
|
}
|
|
3576
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
4467
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3577
4468
|
}
|
|
3578
4469
|
for (const entry of batch) {
|
|
3579
|
-
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
4470
|
+
foldEntryResult(entry, result, params.adapter, accepted, integrityMismatches, providerFailures);
|
|
4471
|
+
}
|
|
4472
|
+
if (result.reviewFlags !== void 0) {
|
|
4473
|
+
for (const [key, flag] of result.reviewFlags) {
|
|
4474
|
+
reviewFlags.set(key, flag);
|
|
4475
|
+
}
|
|
3580
4476
|
}
|
|
3581
|
-
return readNotices(result);
|
|
4477
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3582
4478
|
}
|
|
3583
|
-
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
4479
|
+
function foldEntryResult(entry, result, adapter, accepted, integrityMismatches, providerFailures) {
|
|
3584
4480
|
const value = result.values.get(entry.key);
|
|
3585
4481
|
if (value === void 0) {
|
|
3586
4482
|
providerFailures.push(entry.key);
|
|
3587
4483
|
return;
|
|
3588
4484
|
}
|
|
3589
|
-
if (
|
|
4485
|
+
if (gateCandidateValue(entry, value, adapter).accepted) {
|
|
3590
4486
|
accepted.set(entry.key, { value, source: entry });
|
|
3591
4487
|
} else {
|
|
3592
4488
|
integrityMismatches.push(entry.key);
|
|
@@ -3628,6 +4524,79 @@ function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
|
|
|
3628
4524
|
}
|
|
3629
4525
|
|
|
3630
4526
|
// src/flow/translate-project.ts
|
|
4527
|
+
async function recordRunStatus(cwd, dryRun, summary, fs) {
|
|
4528
|
+
if (dryRun) {
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
try {
|
|
4532
|
+
await writeRunStatusFile(runStatusFilePath(cwd), buildRunStatusFile(summary), fs);
|
|
4533
|
+
} catch {
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
function buildLocaleRunParams(context, targetLocale, baseline) {
|
|
4537
|
+
return {
|
|
4538
|
+
source: context.source.resource,
|
|
4539
|
+
sourceInvalidIcuKeys: context.source.invalidIcuKeys,
|
|
4540
|
+
baseline,
|
|
4541
|
+
adapter: context.adapter,
|
|
4542
|
+
provider: context.provider,
|
|
4543
|
+
cwd: context.cwd,
|
|
4544
|
+
filesPattern: context.config.files.pattern,
|
|
4545
|
+
sourceLocale: context.config.sourceLocale,
|
|
4546
|
+
targetLocale,
|
|
4547
|
+
format: context.config.format,
|
|
4548
|
+
glossary: context.config.glossary,
|
|
4549
|
+
tone: context.config.tone,
|
|
4550
|
+
prune: context.prune,
|
|
4551
|
+
generatePlurals: context.generatePlurals,
|
|
4552
|
+
maxBatchSize: context.maxBatchSize,
|
|
4553
|
+
fs: context.fs,
|
|
4554
|
+
budget: context.budget
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
async function runDryLocale(context, targetLocale, lock) {
|
|
4558
|
+
const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
|
|
4559
|
+
return (await runLocale(params)).summary;
|
|
4560
|
+
}
|
|
4561
|
+
async function runLiveLocale(context, targetLocale) {
|
|
4562
|
+
return withLocaleWriteLock(context.cwd, targetLocale, context.fs, async () => {
|
|
4563
|
+
const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
|
|
4564
|
+
const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
|
|
4565
|
+
const result = await runLocale(params);
|
|
4566
|
+
await updateLockFileLocale(context.cwd, context.fs, targetLocale, {
|
|
4567
|
+
mode: "replace",
|
|
4568
|
+
entries: result.lockEntries
|
|
4569
|
+
});
|
|
4570
|
+
return result.summary;
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
async function runOneLocale(targetLocale, run) {
|
|
4574
|
+
try {
|
|
4575
|
+
return await run();
|
|
4576
|
+
} catch (error) {
|
|
4577
|
+
if (error instanceof SdkError && error.code === "LOCK_FILE_INVALID") {
|
|
4578
|
+
throw error;
|
|
4579
|
+
}
|
|
4580
|
+
return failureSummary(targetLocale, error);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
async function runAllLocalesDry(context, targetLocales) {
|
|
4584
|
+
const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
|
|
4585
|
+
const summaries = [];
|
|
4586
|
+
for (const targetLocale of targetLocales) {
|
|
4587
|
+
summaries.push(
|
|
4588
|
+
await runOneLocale(targetLocale, () => runDryLocale(context, targetLocale, lock))
|
|
4589
|
+
);
|
|
4590
|
+
}
|
|
4591
|
+
return summaries;
|
|
4592
|
+
}
|
|
4593
|
+
async function runAllLocalesLive(context, targetLocales) {
|
|
4594
|
+
const summaries = [];
|
|
4595
|
+
for (const targetLocale of targetLocales) {
|
|
4596
|
+
summaries.push(await runOneLocale(targetLocale, () => runLiveLocale(context, targetLocale)));
|
|
4597
|
+
}
|
|
4598
|
+
return summaries;
|
|
4599
|
+
}
|
|
3631
4600
|
async function translate2(input, deps = {}) {
|
|
3632
4601
|
const config = input.config;
|
|
3633
4602
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -3636,44 +4605,42 @@ async function translate2(input, deps = {}) {
|
|
|
3636
4605
|
const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
|
|
3637
4606
|
const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
3638
4607
|
const fs = deps.fs ?? defaultFs;
|
|
4608
|
+
const budget = createBudgetTracker(
|
|
4609
|
+
config.maxTokens,
|
|
4610
|
+
config.budgetBehavior ?? DEFAULT_BUDGET_BEHAVIOR
|
|
4611
|
+
);
|
|
3639
4612
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3640
4613
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
3641
4614
|
const source = await readSource(config, cwd, fs, adapter);
|
|
3642
|
-
const
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
sourceLocale: config.sourceLocale,
|
|
3656
|
-
targetLocale,
|
|
3657
|
-
format: config.format,
|
|
3658
|
-
glossary: config.glossary,
|
|
3659
|
-
tone: config.tone,
|
|
3660
|
-
prune,
|
|
3661
|
-
generatePlurals,
|
|
3662
|
-
maxBatchSize,
|
|
3663
|
-
fs
|
|
3664
|
-
};
|
|
3665
|
-
const { summary, lockEntries } = await runLocale(params);
|
|
3666
|
-
if (!dryRun) {
|
|
3667
|
-
lock = updateLockLocale(lock, targetLocale, lockEntries);
|
|
3668
|
-
await writeLockFile(lockPath, lock, fs);
|
|
3669
|
-
}
|
|
3670
|
-
summaries.push(summary);
|
|
3671
|
-
} catch (error) {
|
|
3672
|
-
summaries.push(failureSummary(targetLocale, error));
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
4615
|
+
const context = {
|
|
4616
|
+
source,
|
|
4617
|
+
adapter,
|
|
4618
|
+
provider,
|
|
4619
|
+
cwd,
|
|
4620
|
+
config,
|
|
4621
|
+
prune,
|
|
4622
|
+
generatePlurals,
|
|
4623
|
+
maxBatchSize,
|
|
4624
|
+
fs,
|
|
4625
|
+
budget
|
|
4626
|
+
};
|
|
4627
|
+
const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales) : await runAllLocalesLive(context, config.targetLocales);
|
|
3675
4628
|
const { succeeded, failed } = partition(summaries);
|
|
3676
|
-
|
|
4629
|
+
const usage = summaries.reduce(
|
|
4630
|
+
(total, summary2) => combineUsage(total, summary2.usage),
|
|
4631
|
+
void 0
|
|
4632
|
+
);
|
|
4633
|
+
const budgetSummary = toBudgetSummary(budget);
|
|
4634
|
+
const summary = {
|
|
4635
|
+
dryRun,
|
|
4636
|
+
locales: summaries,
|
|
4637
|
+
succeeded,
|
|
4638
|
+
failed,
|
|
4639
|
+
...usage !== void 0 ? { usage } : {},
|
|
4640
|
+
...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
|
|
4641
|
+
};
|
|
4642
|
+
await recordRunStatus(cwd, dryRun, summary, fs);
|
|
4643
|
+
return summary;
|
|
3677
4644
|
}
|
|
3678
4645
|
var ExchangeError = class extends Error {
|
|
3679
4646
|
code;
|
|
@@ -3714,7 +4681,9 @@ var COLUMN = {
|
|
|
3714
4681
|
status: 4,
|
|
3715
4682
|
translation: 5,
|
|
3716
4683
|
sourceHash: 6,
|
|
3717
|
-
context: 7
|
|
4684
|
+
context: 7,
|
|
4685
|
+
reviewStatus: 8,
|
|
4686
|
+
reviewReasons: 9
|
|
3718
4687
|
};
|
|
3719
4688
|
var HEADERS = [
|
|
3720
4689
|
"Key",
|
|
@@ -3723,7 +4692,9 @@ var HEADERS = [
|
|
|
3723
4692
|
"Status",
|
|
3724
4693
|
"Translation",
|
|
3725
4694
|
"Source hash",
|
|
3726
|
-
"Context"
|
|
4695
|
+
"Context",
|
|
4696
|
+
"Review status",
|
|
4697
|
+
"Review reasons"
|
|
3727
4698
|
];
|
|
3728
4699
|
var HEADER_ROW = 1;
|
|
3729
4700
|
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
@@ -3743,7 +4714,9 @@ var COLUMN_WIDTHS = {
|
|
|
3743
4714
|
[COLUMN.current]: 50,
|
|
3744
4715
|
[COLUMN.status]: 12,
|
|
3745
4716
|
[COLUMN.translation]: 50,
|
|
3746
|
-
[COLUMN.context]: 50
|
|
4717
|
+
[COLUMN.context]: 50,
|
|
4718
|
+
[COLUMN.reviewStatus]: 12,
|
|
4719
|
+
[COLUMN.reviewReasons]: 40
|
|
3747
4720
|
};
|
|
3748
4721
|
var TEXT_NUMBER_FORMAT = "@";
|
|
3749
4722
|
function styleHeader(sheet) {
|
|
@@ -3775,7 +4748,9 @@ function writeRow(sheet, sheetRow) {
|
|
|
3775
4748
|
translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
|
|
3776
4749
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
3777
4750
|
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
3778
|
-
|
|
4751
|
+
row.getCell(COLUMN.reviewStatus).value = sheetRow.reviewStatus;
|
|
4752
|
+
row.getCell(COLUMN.reviewReasons).value = sheetRow.reviewReasons;
|
|
4753
|
+
for (let column = COLUMN.key; column <= COLUMN.reviewReasons; column += 1) {
|
|
3779
4754
|
const cell = row.getCell(column);
|
|
3780
4755
|
cell.protection = { locked: column !== COLUMN.translation };
|
|
3781
4756
|
if (column !== COLUMN.translation) {
|
|
@@ -3950,7 +4925,9 @@ var rowSchema = zod.z.object({
|
|
|
3950
4925
|
status: zod.z.enum(["new", "changed", "unchanged"]),
|
|
3951
4926
|
sourceHash: zod.z.string(),
|
|
3952
4927
|
translation: zod.z.string(),
|
|
3953
|
-
context: zod.z.string()
|
|
4928
|
+
context: zod.z.string(),
|
|
4929
|
+
reviewStatus: zod.z.enum(["ok", "review"]).catch("ok"),
|
|
4930
|
+
reviewReasons: zod.z.string().catch("")
|
|
3954
4931
|
});
|
|
3955
4932
|
function cellString(cell) {
|
|
3956
4933
|
const value = cell.value;
|
|
@@ -3984,9 +4961,9 @@ function parseRow(sheet, row) {
|
|
|
3984
4961
|
status: cellString(row.getCell(COLUMN.status)),
|
|
3985
4962
|
sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
|
|
3986
4963
|
translation: cellString(row.getCell(COLUMN.translation)),
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
4964
|
+
context: cellString(row.getCell(COLUMN.context)),
|
|
4965
|
+
reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
|
|
4966
|
+
reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
|
|
3990
4967
|
};
|
|
3991
4968
|
const result = rowSchema.safeParse(candidate);
|
|
3992
4969
|
if (!result.success) {
|
|
@@ -4053,14 +5030,34 @@ async function readWorkbook(bytes, options = {}) {
|
|
|
4053
5030
|
|
|
4054
5031
|
// src/flow/workbook/export-workbook.ts
|
|
4055
5032
|
var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
5033
|
+
function reasonLabel(reason) {
|
|
5034
|
+
return reason.toLowerCase().replace(/_/g, "-");
|
|
5035
|
+
}
|
|
5036
|
+
function reviewColumns(flag) {
|
|
5037
|
+
if (flag === void 0) {
|
|
5038
|
+
return { reviewStatus: "ok", reviewReasons: "" };
|
|
4060
5039
|
}
|
|
4061
|
-
return
|
|
5040
|
+
return { reviewStatus: "review", reviewReasons: flag.reasons.map(reasonLabel).join(", ") };
|
|
4062
5041
|
}
|
|
4063
|
-
function
|
|
5042
|
+
function computeRowReview(adapter, sourceValue, currentTarget, sourceLocale, targetLocale, glossary) {
|
|
5043
|
+
if (currentTarget === "") {
|
|
5044
|
+
return { reviewStatus: "ok", reviewReasons: "" };
|
|
5045
|
+
}
|
|
5046
|
+
const integrity = adapter.comparePlaceholders?.(sourceValue, currentTarget) ?? checkPlaceholders(
|
|
5047
|
+
adapter.extractPlaceholders(sourceValue),
|
|
5048
|
+
adapter.extractPlaceholders(currentTarget)
|
|
5049
|
+
);
|
|
5050
|
+
const flag = computeReviewFlags({
|
|
5051
|
+
sourceValue,
|
|
5052
|
+
translatedValue: currentTarget,
|
|
5053
|
+
sourceLocale,
|
|
5054
|
+
targetLocale,
|
|
5055
|
+
integrity,
|
|
5056
|
+
glossary
|
|
5057
|
+
});
|
|
5058
|
+
return reviewColumns(flag);
|
|
5059
|
+
}
|
|
5060
|
+
function buildRows(source, target, baseline, includeUnchanged, adapter, glossary) {
|
|
4064
5061
|
const diff2 = diffResources(source, target, { baseline });
|
|
4065
5062
|
const rows = [];
|
|
4066
5063
|
const add = (keys, status) => {
|
|
@@ -4069,14 +5066,23 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
4069
5066
|
if (sourceEntry === void 0) {
|
|
4070
5067
|
continue;
|
|
4071
5068
|
}
|
|
5069
|
+
const currentTarget = target.entries.get(key)?.value ?? "";
|
|
4072
5070
|
rows.push({
|
|
4073
5071
|
key,
|
|
4074
5072
|
source: sourceEntry.value,
|
|
4075
|
-
currentTarget
|
|
5073
|
+
currentTarget,
|
|
4076
5074
|
status,
|
|
4077
5075
|
sourceHash: contentHash(sourceEntry),
|
|
4078
5076
|
translation: "",
|
|
4079
|
-
context: sourceEntry.description ?? ""
|
|
5077
|
+
context: sourceEntry.description ?? "",
|
|
5078
|
+
...computeRowReview(
|
|
5079
|
+
adapter,
|
|
5080
|
+
sourceEntry.value,
|
|
5081
|
+
currentTarget,
|
|
5082
|
+
source.locale,
|
|
5083
|
+
target.locale,
|
|
5084
|
+
glossary
|
|
5085
|
+
)
|
|
4080
5086
|
});
|
|
4081
5087
|
}
|
|
4082
5088
|
};
|
|
@@ -4097,12 +5103,14 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
4097
5103
|
const locales = selectLocales(config, input.locales);
|
|
4098
5104
|
const sheets = await Promise.all(
|
|
4099
5105
|
locales.map(async (locale) => {
|
|
4100
|
-
const target = await
|
|
5106
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
4101
5107
|
const rows = buildRows(
|
|
4102
5108
|
source.resource,
|
|
4103
5109
|
target,
|
|
4104
5110
|
baselineFor(lock, locale),
|
|
4105
|
-
input.includeUnchanged ?? false
|
|
5111
|
+
input.includeUnchanged ?? false,
|
|
5112
|
+
adapter,
|
|
5113
|
+
config.glossary
|
|
4106
5114
|
);
|
|
4107
5115
|
return { locale, rows };
|
|
4108
5116
|
})
|
|
@@ -4133,14 +5141,8 @@ function judge(row, sourceEntry, adapter) {
|
|
|
4133
5141
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
4134
5142
|
return "drift";
|
|
4135
5143
|
}
|
|
4136
|
-
const
|
|
4137
|
-
|
|
4138
|
-
return "placeholder";
|
|
4139
|
-
}
|
|
4140
|
-
if (!adapter.validateMessage(row.translation)) {
|
|
4141
|
-
return "icu";
|
|
4142
|
-
}
|
|
4143
|
-
return void 0;
|
|
5144
|
+
const gate = gateCandidateValue(sourceEntry, row.translation, adapter);
|
|
5145
|
+
return gate.accepted ? void 0 : gate.reason;
|
|
4144
5146
|
}
|
|
4145
5147
|
function trackBlankDrift(row, params, buckets) {
|
|
4146
5148
|
const sourceEntry = params.source.entries.get(row.key);
|
|
@@ -4197,15 +5199,14 @@ function importLocale(params) {
|
|
|
4197
5199
|
translated: [...buckets.accepted.keys()].sort(),
|
|
4198
5200
|
unchanged: diff2.unchanged,
|
|
4199
5201
|
orphaned: diff2.orphaned,
|
|
4200
|
-
// Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
|
|
4201
5202
|
pruned: [],
|
|
4202
5203
|
invalidIcuSource,
|
|
4203
5204
|
integrityMismatches: [...buckets.mismatches].sort(),
|
|
4204
|
-
// A workbook import never calls a provider, so a provider-call failure cannot occur here.
|
|
4205
5205
|
providerFailures: [],
|
|
4206
|
-
|
|
5206
|
+
budgetWithheld: [],
|
|
4207
5207
|
generated: [],
|
|
4208
|
-
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : []
|
|
5208
|
+
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
|
|
5209
|
+
needsReview: []
|
|
4209
5210
|
};
|
|
4210
5211
|
return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
|
|
4211
5212
|
}
|
|
@@ -4225,13 +5226,6 @@ async function readWorkbookBytes(path, fs) {
|
|
|
4225
5226
|
}
|
|
4226
5227
|
return read.bytes;
|
|
4227
5228
|
}
|
|
4228
|
-
async function readTarget4(cwd, config, adapter, fs, locale) {
|
|
4229
|
-
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
4230
|
-
if (!await fs.fileExists(path)) {
|
|
4231
|
-
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
4232
|
-
}
|
|
4233
|
-
return (await adapter.read(path, locale)).resource;
|
|
4234
|
-
}
|
|
4235
5229
|
function mergeAccepted(target, accepted) {
|
|
4236
5230
|
const merged = new Map(target.entries);
|
|
4237
5231
|
for (const [key, { value, source }] of accepted) {
|
|
@@ -4262,7 +5256,7 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
4262
5256
|
`The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
|
|
4263
5257
|
);
|
|
4264
5258
|
}
|
|
4265
|
-
const target = await
|
|
5259
|
+
const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
|
|
4266
5260
|
const baseline = baselineFor(lock, sheet.locale);
|
|
4267
5261
|
const { summary, accepted } = importLocale({
|
|
4268
5262
|
sheet,
|
|
@@ -4305,8 +5299,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
4305
5299
|
} catch (error) {
|
|
4306
5300
|
throw new SdkError("SOURCE_INVALID", error.message);
|
|
4307
5301
|
}
|
|
4308
|
-
const
|
|
4309
|
-
let lock = await readLockFile(lockPath, fs);
|
|
5302
|
+
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
4310
5303
|
const ctx = {
|
|
4311
5304
|
config,
|
|
4312
5305
|
cwd,
|
|
@@ -4319,10 +5312,18 @@ async function importWorkbook(input, deps = {}) {
|
|
|
4319
5312
|
const summaries = [];
|
|
4320
5313
|
for (const sheet of data.sheets) {
|
|
4321
5314
|
try {
|
|
4322
|
-
|
|
4323
|
-
if (
|
|
4324
|
-
|
|
4325
|
-
|
|
5315
|
+
let summary;
|
|
5316
|
+
if (dryRun) {
|
|
5317
|
+
summary = (await runSheet(ctx, sheet, lock)).summary;
|
|
5318
|
+
} else {
|
|
5319
|
+
summary = await withLocaleWriteLock(cwd, sheet.locale, fs, async () => {
|
|
5320
|
+
const result = await runSheet(ctx, sheet, lock);
|
|
5321
|
+
await updateLockFileLocale(cwd, fs, sheet.locale, {
|
|
5322
|
+
mode: "replace",
|
|
5323
|
+
entries: result.lockEntries
|
|
5324
|
+
});
|
|
5325
|
+
return result.summary;
|
|
5326
|
+
});
|
|
4326
5327
|
}
|
|
4327
5328
|
summaries.push(summary);
|
|
4328
5329
|
} catch (error) {
|
|
@@ -4461,12 +5462,19 @@ exports.SdkError = SdkError;
|
|
|
4461
5462
|
exports.check = check;
|
|
4462
5463
|
exports.defineConfig = defineConfig;
|
|
4463
5464
|
exports.diff = diff;
|
|
5465
|
+
exports.diffLocaleSnapshots = diffLocaleSnapshots;
|
|
5466
|
+
exports.editEntry = editEntry;
|
|
4464
5467
|
exports.exportWorkbook = exportWorkbook;
|
|
4465
5468
|
exports.importWorkbook = importWorkbook;
|
|
5469
|
+
exports.keyIntegrity = keyIntegrity;
|
|
5470
|
+
exports.keyValue = keyValue;
|
|
4466
5471
|
exports.loadConfig = loadConfig;
|
|
4467
5472
|
exports.loadConfigWithMeta = loadConfigWithMeta;
|
|
4468
5473
|
exports.loadLockFile = loadLockFile;
|
|
4469
5474
|
exports.lockState = lockState;
|
|
5475
|
+
exports.readLocaleFileSnapshot = readLocaleFileSnapshot;
|
|
5476
|
+
exports.retranslateEntry = retranslateEntry;
|
|
5477
|
+
exports.runStatus = runStatus;
|
|
4470
5478
|
exports.scaffoldingMetadata = scaffoldingMetadata;
|
|
4471
5479
|
exports.translate = translate2;
|
|
4472
5480
|
exports.verbatraConfigSchema = verbatraConfigSchema;
|