@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.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, resolve, join, basename, extname } from 'path';
|
|
|
3
3
|
import { cosmiconfig } from 'cosmiconfig';
|
|
4
4
|
import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
|
-
import { access,
|
|
6
|
+
import { rm, access, mkdir, open, writeFile, rename } from 'fs/promises';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import Anthropic from '@anthropic-ai/sdk';
|
|
9
9
|
import * as deepl from 'deepl-node';
|
|
@@ -115,6 +115,24 @@ async function atomicWrite(path, data) {
|
|
|
115
115
|
throw error;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
async function createExclusive(path, data) {
|
|
119
|
+
await mkdir(dirname(path), { recursive: true });
|
|
120
|
+
let handle;
|
|
121
|
+
try {
|
|
122
|
+
handle = await open(path, "wx");
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error.code === "EEXIST") {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await handle.writeFile(data, "utf8");
|
|
131
|
+
} finally {
|
|
132
|
+
await handle.close();
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
118
136
|
var defaultFs = {
|
|
119
137
|
async fileExists(path) {
|
|
120
138
|
try {
|
|
@@ -127,7 +145,11 @@ var defaultFs = {
|
|
|
127
145
|
readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
|
|
128
146
|
readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
|
|
129
147
|
writeFile: (path, data) => atomicWrite(path, data),
|
|
130
|
-
writeBytes: (path, data) => atomicWrite(path, data)
|
|
148
|
+
writeBytes: (path, data) => atomicWrite(path, data),
|
|
149
|
+
createExclusive: (path, data) => createExclusive(path, data),
|
|
150
|
+
deleteFile: async (path) => {
|
|
151
|
+
await rm(path, { force: true });
|
|
152
|
+
}
|
|
131
153
|
};
|
|
132
154
|
var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
|
|
133
155
|
var glossaryRecordSchema = z.record(z.string(), z.string());
|
|
@@ -316,29 +338,18 @@ var TIMEOUT_STATUS = 408;
|
|
|
316
338
|
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
317
339
|
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
318
340
|
"RateLimitError",
|
|
319
|
-
// openai, @anthropic-ai/sdk
|
|
320
341
|
"TooManyRequestsError"
|
|
321
|
-
// deepl-node
|
|
322
342
|
]);
|
|
323
343
|
var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
324
344
|
"AuthenticationError",
|
|
325
|
-
// openai, @anthropic-ai/sdk (401)
|
|
326
345
|
"PermissionDeniedError",
|
|
327
|
-
// openai, @anthropic-ai/sdk (403)
|
|
328
346
|
"AuthorizationError"
|
|
329
|
-
// deepl-node
|
|
330
347
|
]);
|
|
331
348
|
var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
332
349
|
"APIConnectionTimeoutError",
|
|
333
|
-
// openai, @anthropic-ai/sdk
|
|
334
|
-
// deepl-node has no distinct timeout class; ConnectionError is its only network-failure
|
|
335
|
-
// class, so it is folded into TIMEOUT rather than the PROVIDER_ERROR fallback.
|
|
336
350
|
"ConnectionError"
|
|
337
351
|
]);
|
|
338
|
-
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
339
|
-
"APIUserAbortError"
|
|
340
|
-
// openai, @anthropic-ai/sdk
|
|
341
|
-
]);
|
|
352
|
+
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set(["APIUserAbortError"]);
|
|
342
353
|
function getErrorStatus(error) {
|
|
343
354
|
if (typeof error !== "object" || error === null || !("status" in error)) {
|
|
344
355
|
return void 0;
|
|
@@ -392,7 +403,6 @@ function isAbortError(error, signal) {
|
|
|
392
403
|
}
|
|
393
404
|
var REDACTED = "[REDACTED]";
|
|
394
405
|
var KEY_PATTERNS = [
|
|
395
|
-
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
396
406
|
/\bsk-[A-Za-z0-9_-]{8,}/g,
|
|
397
407
|
/AIza[0-9A-Za-z_-]{35}/g,
|
|
398
408
|
/[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
|
|
@@ -414,6 +424,10 @@ var ProviderError = class extends Error {
|
|
|
414
424
|
/** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
|
|
415
425
|
code;
|
|
416
426
|
/**
|
|
427
|
+
* Redacts the message as a defense-in-depth backstop before storing it. The redaction runs with an
|
|
428
|
+
* empty secret argument so the ANTHROPIC_API_KEY environment default is not re-applied, keeping
|
|
429
|
+
* this generic error decoupled from any one provider's environment variable.
|
|
430
|
+
*
|
|
417
431
|
* @param code - The stable failure code.
|
|
418
432
|
* @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
|
|
419
433
|
*/
|
|
@@ -472,6 +486,76 @@ function validateRequest(request) {
|
|
|
472
486
|
}
|
|
473
487
|
return parsed.data;
|
|
474
488
|
}
|
|
489
|
+
var LENGTH_RATIO_MIN = 0.35;
|
|
490
|
+
var LENGTH_RATIO_MAX = 3;
|
|
491
|
+
var LENGTH_RATIO_MIN_SOURCE_LENGTH = 12;
|
|
492
|
+
var UNICODE_LETTER = /\p{L}/u;
|
|
493
|
+
var DEGRADATION_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
494
|
+
"FORMALITY_DOWNGRADED",
|
|
495
|
+
"GLOSSARY_IGNORED"
|
|
496
|
+
]);
|
|
497
|
+
function isLengthRatioOutlier(sourceValue, translatedValue) {
|
|
498
|
+
const trimmedSource = sourceValue.trim();
|
|
499
|
+
if (trimmedSource.length < LENGTH_RATIO_MIN_SOURCE_LENGTH) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
const ratio = translatedValue.trim().length / trimmedSource.length;
|
|
503
|
+
return ratio < LENGTH_RATIO_MIN || ratio > LENGTH_RATIO_MAX;
|
|
504
|
+
}
|
|
505
|
+
function isEqualsSource(input) {
|
|
506
|
+
const trimmedSource = input.sourceValue.trim();
|
|
507
|
+
const trimmedTranslated = input.translatedValue.trim();
|
|
508
|
+
return trimmedTranslated === trimmedSource && input.targetLocale !== input.sourceLocale && UNICODE_LETTER.test(trimmedSource);
|
|
509
|
+
}
|
|
510
|
+
function isGlossaryTermMissed(input) {
|
|
511
|
+
const glossary = input.glossary;
|
|
512
|
+
if (glossary === void 0 || Object.keys(glossary).length === 0) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
const sourceLower = input.sourceValue.toLowerCase();
|
|
516
|
+
const translatedLower = input.translatedValue.toLowerCase();
|
|
517
|
+
for (const [sourceTerm, targetTerm] of Object.entries(glossary)) {
|
|
518
|
+
const sourceHit = sourceLower.includes(sourceTerm.toLowerCase());
|
|
519
|
+
const targetHit = translatedLower.includes(targetTerm.toLowerCase());
|
|
520
|
+
if (sourceHit && !targetHit) {
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
function isIntegrityReordered(integrity) {
|
|
527
|
+
return integrity.matches && integrity.reordered;
|
|
528
|
+
}
|
|
529
|
+
function computeReviewFlags(input) {
|
|
530
|
+
const reasons = [];
|
|
531
|
+
if (isLengthRatioOutlier(input.sourceValue, input.translatedValue)) {
|
|
532
|
+
reasons.push("LENGTH_RATIO_OUTLIER");
|
|
533
|
+
}
|
|
534
|
+
if (isEqualsSource(input)) {
|
|
535
|
+
reasons.push("EQUALS_SOURCE");
|
|
536
|
+
}
|
|
537
|
+
if (isGlossaryTermMissed(input)) {
|
|
538
|
+
reasons.push("GLOSSARY_TERM_MISSED");
|
|
539
|
+
}
|
|
540
|
+
if (isIntegrityReordered(input.integrity)) {
|
|
541
|
+
reasons.push("INTEGRITY_REORDERED");
|
|
542
|
+
}
|
|
543
|
+
return reasons.length > 0 ? { status: "review", reasons } : void 0;
|
|
544
|
+
}
|
|
545
|
+
function applyProviderDegraded(reviewFlags, notices, acceptedKeys) {
|
|
546
|
+
if (!notices.some((notice) => DEGRADATION_NOTICE_CODES.has(notice.code))) {
|
|
547
|
+
return reviewFlags;
|
|
548
|
+
}
|
|
549
|
+
const next = new Map(reviewFlags);
|
|
550
|
+
for (const key of acceptedKeys) {
|
|
551
|
+
const existing = next.get(key);
|
|
552
|
+
next.set(key, {
|
|
553
|
+
status: "review",
|
|
554
|
+
reasons: existing !== void 0 ? [...existing.reasons, "PROVIDER_DEGRADED"] : ["PROVIDER_DEGRADED"]
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
return next;
|
|
558
|
+
}
|
|
475
559
|
function toIntegrityInputs(entries, values) {
|
|
476
560
|
const inputs = [];
|
|
477
561
|
for (const entry of entries) {
|
|
@@ -567,7 +651,33 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
567
651
|
request.extractPlaceholders,
|
|
568
652
|
request.comparePlaceholders
|
|
569
653
|
);
|
|
570
|
-
|
|
654
|
+
const notices = [];
|
|
655
|
+
const reviewFlags = applyProviderDegraded(buildReviewFlags(data, values, integrity), notices, [
|
|
656
|
+
...values.keys()
|
|
657
|
+
]);
|
|
658
|
+
return usage === void 0 ? { values, integrity, notices, reviewFlags } : { values, integrity, usage, notices, reviewFlags };
|
|
659
|
+
}
|
|
660
|
+
function buildReviewFlags(data, values, integrity) {
|
|
661
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
662
|
+
for (const entry of data.entries) {
|
|
663
|
+
const translatedValue = values.get(entry.key);
|
|
664
|
+
const entryIntegrity = integrity.get(entry.key);
|
|
665
|
+
if (translatedValue === void 0 || entryIntegrity === void 0) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const flag = computeReviewFlags({
|
|
669
|
+
sourceValue: entry.value,
|
|
670
|
+
translatedValue,
|
|
671
|
+
sourceLocale: data.sourceLocale,
|
|
672
|
+
targetLocale: data.targetLocale,
|
|
673
|
+
integrity: entryIntegrity,
|
|
674
|
+
glossary: data.glossary
|
|
675
|
+
});
|
|
676
|
+
if (flag !== void 0) {
|
|
677
|
+
reviewFlags.set(entry.key, flag);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return reviewFlags;
|
|
571
681
|
}
|
|
572
682
|
async function requestTranslations(mechanism, data, signal) {
|
|
573
683
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
@@ -885,9 +995,6 @@ function createDeepLProvider(config, deps = {}) {
|
|
|
885
995
|
return {
|
|
886
996
|
id: PROVIDER_ID2,
|
|
887
997
|
kind: "machine-translation",
|
|
888
|
-
// Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
|
|
889
|
-
// generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
|
|
890
|
-
// silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
|
|
891
998
|
supportsGlossary: validConfig.glossaryId !== void 0,
|
|
892
999
|
translateBatch: (request) => translate(bundle, validConfig, request)
|
|
893
1000
|
};
|
|
@@ -922,7 +1029,34 @@ async function translate(bundle, config, request) {
|
|
|
922
1029
|
if (unprotectable.length > 0) {
|
|
923
1030
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
924
1031
|
}
|
|
925
|
-
|
|
1032
|
+
const reviewFlags = applyProviderDegraded(
|
|
1033
|
+
buildReviewFlags2(protectable, data, values, integrity, request.glossary),
|
|
1034
|
+
notices,
|
|
1035
|
+
[...values.keys()]
|
|
1036
|
+
);
|
|
1037
|
+
return { values, integrity, notices, reviewFlags };
|
|
1038
|
+
}
|
|
1039
|
+
function buildReviewFlags2(protectable, data, values, integrity, glossary) {
|
|
1040
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
1041
|
+
for (const entry of protectable) {
|
|
1042
|
+
const translatedValue = values.get(entry.key);
|
|
1043
|
+
const entryIntegrity = integrity.get(entry.key);
|
|
1044
|
+
if (translatedValue === void 0 || entryIntegrity === void 0) {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
const flag = computeReviewFlags({
|
|
1048
|
+
sourceValue: entry.value,
|
|
1049
|
+
translatedValue,
|
|
1050
|
+
sourceLocale: data.sourceLocale,
|
|
1051
|
+
targetLocale: data.targetLocale,
|
|
1052
|
+
integrity: entryIntegrity,
|
|
1053
|
+
glossary
|
|
1054
|
+
});
|
|
1055
|
+
if (flag !== void 0) {
|
|
1056
|
+
reviewFlags.set(entry.key, flag);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return reviewFlags;
|
|
926
1060
|
}
|
|
927
1061
|
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
928
1062
|
if (protectable.length === 0) {
|
|
@@ -972,14 +1106,14 @@ function throwAbort(signal) {
|
|
|
972
1106
|
throw new DOMException("This operation was aborted.", "AbortError");
|
|
973
1107
|
}
|
|
974
1108
|
function delay(ms, signal) {
|
|
975
|
-
return new Promise((
|
|
1109
|
+
return new Promise((resolve9) => {
|
|
976
1110
|
const onAbort = () => {
|
|
977
1111
|
clearTimeout(timer);
|
|
978
|
-
|
|
1112
|
+
resolve9();
|
|
979
1113
|
};
|
|
980
1114
|
const timer = setTimeout(() => {
|
|
981
1115
|
signal?.removeEventListener("abort", onAbort);
|
|
982
|
-
|
|
1116
|
+
resolve9();
|
|
983
1117
|
}, ms);
|
|
984
1118
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
985
1119
|
});
|
|
@@ -1027,12 +1161,10 @@ var TYPE_MAP = {
|
|
|
1027
1161
|
object: "OBJECT"
|
|
1028
1162
|
};
|
|
1029
1163
|
var HANDLED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1030
|
-
// transformed or recursed
|
|
1031
1164
|
"type",
|
|
1032
1165
|
"required",
|
|
1033
1166
|
"properties",
|
|
1034
1167
|
"items",
|
|
1035
|
-
// deliberately dropped (not part of Google's Schema dialect)
|
|
1036
1168
|
"$schema",
|
|
1037
1169
|
"additionalProperties"
|
|
1038
1170
|
]);
|
|
@@ -1426,6 +1558,7 @@ function buildProvider(config) {
|
|
|
1426
1558
|
|
|
1427
1559
|
// src/config/schema.ts
|
|
1428
1560
|
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
1561
|
+
var DEFAULT_BUDGET_BEHAVIOR = "warn";
|
|
1429
1562
|
function findCaseInsensitiveDuplicate(locales) {
|
|
1430
1563
|
const seen = /* @__PURE__ */ new Set();
|
|
1431
1564
|
for (const locale of locales) {
|
|
@@ -1474,7 +1607,23 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
1474
1607
|
* be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
|
|
1475
1608
|
* {@link DEFAULT_MAX_BATCH_SIZE} applies.
|
|
1476
1609
|
*/
|
|
1477
|
-
maxBatchSize: z.number().int().positive().optional()
|
|
1610
|
+
maxBatchSize: z.number().int().positive().optional(),
|
|
1611
|
+
/**
|
|
1612
|
+
* Optional whole-run token ceiling (input plus output tokens summed across every provider call, main
|
|
1613
|
+
* translation and plural generation alike, across all target locales). Checked after each completed
|
|
1614
|
+
* sub-batch, never mid-batch: the sub-batch whose completion crosses the ceiling is retained and
|
|
1615
|
+
* counted, since a call already in flight cannot be undone. Must be a positive integer. Config-only,
|
|
1616
|
+
* no CLI flag. Absent means no budget is enforced. Inert (never a false trip) against a token-less
|
|
1617
|
+
* provider such as DeepL, since it never reports usage to measure against the ceiling.
|
|
1618
|
+
*/
|
|
1619
|
+
maxTokens: z.number().int().positive().optional(),
|
|
1620
|
+
/**
|
|
1621
|
+
* What happens once `maxTokens` is reached: `"warn"` (default) flags it and lets the run continue
|
|
1622
|
+
* unchanged; `"stop"` withholds every not-yet-attempted key for the rest of the run (the current
|
|
1623
|
+
* locale's remaining candidates and every later locale's), retried automatically next run. Present
|
|
1624
|
+
* without `maxTokens` is accepted and has no effect. Never changes the command's exit code.
|
|
1625
|
+
*/
|
|
1626
|
+
budgetBehavior: z.enum(["warn", "stop"]).optional()
|
|
1478
1627
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
1479
1628
|
message: "targetLocales must not include the source locale",
|
|
1480
1629
|
path: ["targetLocales"]
|
|
@@ -1583,6 +1732,60 @@ async function loadConfig(options = {}) {
|
|
|
1583
1732
|
const { config } = await loadConfigWithMeta(options);
|
|
1584
1733
|
return config;
|
|
1585
1734
|
}
|
|
1735
|
+
var LOCAL_DIR_NAME = ".verbatra-local";
|
|
1736
|
+
var LOCK_FILE_GUARD_STEM = "_lockfile";
|
|
1737
|
+
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
1738
|
+
var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
|
|
1739
|
+
function lockPath(cwd, stem) {
|
|
1740
|
+
return resolve(cwd, LOCAL_DIR_NAME, "locks", `${stem}.lock`);
|
|
1741
|
+
}
|
|
1742
|
+
function localeLockPath(cwd, locale) {
|
|
1743
|
+
return lockPath(cwd, locale);
|
|
1744
|
+
}
|
|
1745
|
+
function lockFileGuardPath(cwd) {
|
|
1746
|
+
return lockPath(cwd, LOCK_FILE_GUARD_STEM);
|
|
1747
|
+
}
|
|
1748
|
+
function sleep(ms) {
|
|
1749
|
+
return new Promise((res) => {
|
|
1750
|
+
setTimeout(res, ms);
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
function lockPayload() {
|
|
1754
|
+
return JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1755
|
+
}
|
|
1756
|
+
async function acquireLock(path, fs, pollIntervalMs, deadline) {
|
|
1757
|
+
for (; ; ) {
|
|
1758
|
+
if (await fs.createExclusive(path, lockPayload())) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (Date.now() >= deadline) {
|
|
1762
|
+
throw new SdkError(
|
|
1763
|
+
"LOCK_CONTENDED",
|
|
1764
|
+
`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.`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
const jitter = Math.random() * pollIntervalMs;
|
|
1768
|
+
await sleep(pollIntervalMs + jitter);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
async function withFileLock(path, fs, fn, options) {
|
|
1772
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1773
|
+
const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
|
|
1774
|
+
await acquireLock(path, fs, pollIntervalMs, Date.now() + acquireTimeoutMs);
|
|
1775
|
+
try {
|
|
1776
|
+
return await fn();
|
|
1777
|
+
} finally {
|
|
1778
|
+
await fs.deleteFile(path);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
async function withLocaleWriteLock(cwd, locale, fs, fn, options = {}) {
|
|
1782
|
+
return withFileLock(localeLockPath(cwd, locale), fs, fn, options);
|
|
1783
|
+
}
|
|
1784
|
+
async function withLockFileGuard(cwd, fs, fn, options = {}) {
|
|
1785
|
+
return withFileLock(lockFileGuardPath(cwd), fs, fn, options);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/lock/lock-file.ts
|
|
1586
1789
|
var LOCK_FILE_NAME = "verbatra.lock.json";
|
|
1587
1790
|
var CURRENT_VERSION = 1;
|
|
1588
1791
|
var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
|
|
@@ -1594,8 +1797,7 @@ var lockFileSchema = z.object({
|
|
|
1594
1797
|
function lockFilePath(cwd) {
|
|
1595
1798
|
return resolve(cwd, LOCK_FILE_NAME);
|
|
1596
1799
|
}
|
|
1597
|
-
|
|
1598
|
-
const read = await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES);
|
|
1800
|
+
function parseLockFileRead(read, path) {
|
|
1599
1801
|
if (read.kind === "missing") {
|
|
1600
1802
|
return EMPTY_LOCK;
|
|
1601
1803
|
}
|
|
@@ -1623,6 +1825,9 @@ async function readLockFile(path, fs) {
|
|
|
1623
1825
|
}
|
|
1624
1826
|
return result.data;
|
|
1625
1827
|
}
|
|
1828
|
+
async function readLockFile(path, fs) {
|
|
1829
|
+
return parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
|
|
1830
|
+
}
|
|
1626
1831
|
function baselineFor(lock, locale) {
|
|
1627
1832
|
return new Map(Object.entries(lock.locales[locale] ?? {}));
|
|
1628
1833
|
}
|
|
@@ -1638,14 +1843,30 @@ function byKey(a, b) {
|
|
|
1638
1843
|
function sortRecord(record) {
|
|
1639
1844
|
return Object.fromEntries(Object.entries(record).sort(byKey));
|
|
1640
1845
|
}
|
|
1641
|
-
|
|
1846
|
+
function serializeLockFile(lock) {
|
|
1642
1847
|
const locales = {};
|
|
1643
1848
|
for (const [locale, entries] of Object.entries(lock.locales).sort(byKey)) {
|
|
1644
1849
|
locales[locale] = sortRecord(entries);
|
|
1645
1850
|
}
|
|
1646
1851
|
const ordered = { version: lock.version, locales };
|
|
1647
|
-
|
|
1648
|
-
|
|
1852
|
+
return `${JSON.stringify(ordered, null, 2)}
|
|
1853
|
+
`;
|
|
1854
|
+
}
|
|
1855
|
+
function applyLockLocalePatch(currentEntries, patch) {
|
|
1856
|
+
if (patch.mode === "replace") {
|
|
1857
|
+
return patch.entries;
|
|
1858
|
+
}
|
|
1859
|
+
return { ...currentEntries, ...patch.entries };
|
|
1860
|
+
}
|
|
1861
|
+
async function updateLockFileLocale(cwd, fs, locale, patch) {
|
|
1862
|
+
return withLockFileGuard(cwd, fs, async () => {
|
|
1863
|
+
const path = lockFilePath(cwd);
|
|
1864
|
+
const lock = parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
|
|
1865
|
+
const nextEntries = applyLockLocalePatch(lock.locales[locale], patch);
|
|
1866
|
+
const next = updateLockLocale(lock, locale, nextEntries);
|
|
1867
|
+
await fs.writeFile(path, serializeLockFile(next));
|
|
1868
|
+
return next;
|
|
1869
|
+
});
|
|
1649
1870
|
}
|
|
1650
1871
|
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
1651
1872
|
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
@@ -1907,10 +2128,68 @@ var AdapterError = class extends Error {
|
|
|
1907
2128
|
};
|
|
1908
2129
|
var MAX_DEPTH = 100;
|
|
1909
2130
|
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1910
|
-
var
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2131
|
+
var SENTINEL_ESCAPE = "\\u0001";
|
|
2132
|
+
function isJsonWhitespace(char) {
|
|
2133
|
+
return char === " " || char === " " || char === "\n" || char === "\r";
|
|
2134
|
+
}
|
|
2135
|
+
function endOfStringToken(content, openQuote) {
|
|
2136
|
+
let index = openQuote + 1;
|
|
2137
|
+
while (index < content.length) {
|
|
2138
|
+
const char = content[index];
|
|
2139
|
+
if (char === "\\") {
|
|
2140
|
+
index += 2;
|
|
2141
|
+
} else if (char === '"') {
|
|
2142
|
+
return index + 1;
|
|
2143
|
+
} else {
|
|
2144
|
+
index += 1;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return index;
|
|
2148
|
+
}
|
|
2149
|
+
function isKeyToken(content, end) {
|
|
2150
|
+
let index = end;
|
|
2151
|
+
while (isJsonWhitespace(content[index])) {
|
|
2152
|
+
index += 1;
|
|
2153
|
+
}
|
|
2154
|
+
return content[index] === ":";
|
|
2155
|
+
}
|
|
2156
|
+
function prefixKeyToken(token) {
|
|
2157
|
+
if (token.includes(SENTINEL_ESCAPE)) {
|
|
2158
|
+
throw new AdapterError(
|
|
2159
|
+
"INVALID_STRUCTURE",
|
|
2160
|
+
"A key contains a reserved control-character escape."
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
return `"${SENTINEL_ESCAPE}${token.slice(1)}`;
|
|
2164
|
+
}
|
|
2165
|
+
function prefixKeyTokens(content) {
|
|
2166
|
+
let out = "";
|
|
2167
|
+
let index = 0;
|
|
2168
|
+
while (index < content.length) {
|
|
2169
|
+
const openQuote = content.indexOf('"', index);
|
|
2170
|
+
if (openQuote === -1) {
|
|
2171
|
+
return out + content.slice(index);
|
|
2172
|
+
}
|
|
2173
|
+
const end = endOfStringToken(content, openQuote);
|
|
2174
|
+
const token = content.slice(openQuote, end);
|
|
2175
|
+
out += content.slice(index, openQuote);
|
|
2176
|
+
out += isKeyToken(content, end) ? prefixKeyToken(token) : token;
|
|
2177
|
+
index = end;
|
|
2178
|
+
}
|
|
2179
|
+
return out;
|
|
2180
|
+
}
|
|
2181
|
+
function childrenOf(node) {
|
|
2182
|
+
if (node instanceof Map) {
|
|
2183
|
+
return node.values();
|
|
2184
|
+
}
|
|
2185
|
+
if (Array.isArray(node)) {
|
|
2186
|
+
return node;
|
|
2187
|
+
}
|
|
2188
|
+
if (typeof node === "object" && node !== null) {
|
|
2189
|
+
return Object.values(node);
|
|
2190
|
+
}
|
|
2191
|
+
return null;
|
|
2192
|
+
}
|
|
1914
2193
|
function assertWithinDepth(value, max) {
|
|
1915
2194
|
const stack = [{ node: value, depth: 1 }];
|
|
1916
2195
|
while (stack.length > 0) {
|
|
@@ -1918,42 +2197,118 @@ function assertWithinDepth(value, max) {
|
|
|
1918
2197
|
if (top === void 0) {
|
|
1919
2198
|
break;
|
|
1920
2199
|
}
|
|
1921
|
-
const
|
|
1922
|
-
if (
|
|
2200
|
+
const children = childrenOf(top.node);
|
|
2201
|
+
if (children === null) {
|
|
1923
2202
|
continue;
|
|
1924
2203
|
}
|
|
1925
|
-
if (depth > max) {
|
|
2204
|
+
if (top.depth > max) {
|
|
1926
2205
|
throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
|
|
1927
2206
|
}
|
|
1928
|
-
for (const child of
|
|
1929
|
-
stack.push({ node: child, depth: depth + 1 });
|
|
2207
|
+
for (const child of children) {
|
|
2208
|
+
stack.push({ node: child, depth: top.depth + 1 });
|
|
1930
2209
|
}
|
|
1931
2210
|
}
|
|
1932
2211
|
}
|
|
1933
|
-
function
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
if (!result.success) {
|
|
1937
|
-
throw new AdapterError(
|
|
1938
|
-
"INVALID_STRUCTURE",
|
|
1939
|
-
"The file is not a valid object (expected nested objects of string values)."
|
|
1940
|
-
);
|
|
2212
|
+
function toOrdered(node) {
|
|
2213
|
+
if (Array.isArray(node)) {
|
|
2214
|
+
return node.map((child) => toOrdered(child));
|
|
1941
2215
|
}
|
|
1942
|
-
|
|
2216
|
+
if (typeof node === "object" && node !== null) {
|
|
2217
|
+
const out = /* @__PURE__ */ new Map();
|
|
2218
|
+
for (const [key, child] of Object.entries(node)) {
|
|
2219
|
+
out.set(key.slice(1), toOrdered(child));
|
|
2220
|
+
}
|
|
2221
|
+
return out;
|
|
2222
|
+
}
|
|
2223
|
+
return node;
|
|
1943
2224
|
}
|
|
1944
|
-
function
|
|
2225
|
+
function parseOrderedJson(content) {
|
|
2226
|
+
const prefixed = prefixKeyTokens(content);
|
|
1945
2227
|
let parsed;
|
|
1946
2228
|
try {
|
|
1947
|
-
parsed = JSON.parse(
|
|
2229
|
+
parsed = JSON.parse(prefixed);
|
|
1948
2230
|
} catch {
|
|
1949
2231
|
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
1950
2232
|
}
|
|
1951
|
-
|
|
2233
|
+
assertWithinDepth(parsed, MAX_DEPTH);
|
|
2234
|
+
return toOrdered(parsed);
|
|
1952
2235
|
}
|
|
1953
|
-
function
|
|
1954
|
-
|
|
2236
|
+
function printRecord(record, indent) {
|
|
2237
|
+
if (record.size === 0) {
|
|
2238
|
+
return "{}";
|
|
2239
|
+
}
|
|
2240
|
+
const childIndent = `${indent} `;
|
|
2241
|
+
const lines = [];
|
|
2242
|
+
for (const [key, child] of record) {
|
|
2243
|
+
lines.push(`${childIndent}${JSON.stringify(key)}: ${printValue(child, childIndent)}`);
|
|
2244
|
+
}
|
|
2245
|
+
return `{
|
|
2246
|
+
${lines.join(",\n")}
|
|
2247
|
+
${indent}}`;
|
|
2248
|
+
}
|
|
2249
|
+
function printArray(items, indent) {
|
|
2250
|
+
if (items.length === 0) {
|
|
2251
|
+
return "[]";
|
|
2252
|
+
}
|
|
2253
|
+
const childIndent = `${indent} `;
|
|
2254
|
+
const lines = items.map((item) => `${childIndent}${printValue(item, childIndent)}`);
|
|
2255
|
+
return `[
|
|
2256
|
+
${lines.join(",\n")}
|
|
2257
|
+
${indent}]`;
|
|
2258
|
+
}
|
|
2259
|
+
function printValue(value, indent) {
|
|
2260
|
+
if (value instanceof Map) {
|
|
2261
|
+
return printRecord(value, indent);
|
|
2262
|
+
}
|
|
2263
|
+
if (Array.isArray(value)) {
|
|
2264
|
+
return printArray(value, indent);
|
|
2265
|
+
}
|
|
2266
|
+
return JSON.stringify(value);
|
|
2267
|
+
}
|
|
2268
|
+
function serializeOrderedJson(value) {
|
|
2269
|
+
return `${printValue(value, "")}
|
|
1955
2270
|
`;
|
|
1956
2271
|
}
|
|
2272
|
+
var INVALID_STRUCTURE_MESSAGE = "The file is not a valid object (expected nested objects of string, number, boolean, or null leaves).";
|
|
2273
|
+
function isJsonLeaf(value) {
|
|
2274
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
|
|
2275
|
+
}
|
|
2276
|
+
function assertNodesValid(root) {
|
|
2277
|
+
const stack = [root];
|
|
2278
|
+
while (stack.length > 0) {
|
|
2279
|
+
const node = stack.pop();
|
|
2280
|
+
if (node === void 0) {
|
|
2281
|
+
break;
|
|
2282
|
+
}
|
|
2283
|
+
for (const [key, child] of node) {
|
|
2284
|
+
if (typeof key !== "string") {
|
|
2285
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2286
|
+
}
|
|
2287
|
+
if (child instanceof Map) {
|
|
2288
|
+
stack.push(child);
|
|
2289
|
+
} else if (!isJsonLeaf(child)) {
|
|
2290
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function assertJsonRecord(value) {
|
|
2296
|
+
assertWithinDepth(value, MAX_DEPTH);
|
|
2297
|
+
if (!(value instanceof Map)) {
|
|
2298
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2299
|
+
}
|
|
2300
|
+
assertNodesValid(value);
|
|
2301
|
+
return value;
|
|
2302
|
+
}
|
|
2303
|
+
function parseJsonObject(content) {
|
|
2304
|
+
return assertJsonRecord(parseOrderedJson(content));
|
|
2305
|
+
}
|
|
2306
|
+
function serializeJsonTree(tree) {
|
|
2307
|
+
return serializeOrderedJson(tree);
|
|
2308
|
+
}
|
|
2309
|
+
function isJsonNode(value) {
|
|
2310
|
+
return value instanceof Map;
|
|
2311
|
+
}
|
|
1957
2312
|
function namespaceOf(filePath) {
|
|
1958
2313
|
return basename(filePath, extname(filePath));
|
|
1959
2314
|
}
|
|
@@ -1981,9 +2336,25 @@ function buildCanHandle(extensions, sniff) {
|
|
|
1981
2336
|
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
1982
2337
|
};
|
|
1983
2338
|
}
|
|
2339
|
+
async function fsyncPath(path) {
|
|
2340
|
+
const handle = await open(path, "r");
|
|
2341
|
+
try {
|
|
2342
|
+
await handle.sync();
|
|
2343
|
+
} finally {
|
|
2344
|
+
await handle.close();
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
async function fsyncDirBestEffort(path) {
|
|
2348
|
+
try {
|
|
2349
|
+
await fsyncPath(path);
|
|
2350
|
+
} catch {
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
1984
2353
|
var nodeOps = {
|
|
1985
2354
|
writeFile: (path, data) => writeFile(path, data, "utf8"),
|
|
2355
|
+
fsyncFile: (path) => fsyncPath(path),
|
|
1986
2356
|
rename: (from, to) => rename(from, to),
|
|
2357
|
+
fsyncDir: (path) => fsyncDirBestEffort(path),
|
|
1987
2358
|
rm: (path) => rm(path, { force: true })
|
|
1988
2359
|
};
|
|
1989
2360
|
async function cleanup(ops, tmp) {
|
|
@@ -1999,11 +2370,16 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1999
2370
|
const tmp = tempFileName2(path);
|
|
2000
2371
|
try {
|
|
2001
2372
|
await ops.writeFile(tmp, data);
|
|
2373
|
+
await ops.fsyncFile(tmp);
|
|
2002
2374
|
await ops.rename(tmp, path);
|
|
2003
2375
|
} catch (error) {
|
|
2004
2376
|
await cleanup(ops, tmp);
|
|
2005
2377
|
throw error;
|
|
2006
2378
|
}
|
|
2379
|
+
try {
|
|
2380
|
+
await ops.fsyncDir(dirname(path));
|
|
2381
|
+
} catch {
|
|
2382
|
+
}
|
|
2007
2383
|
}
|
|
2008
2384
|
function stripLeadingBom(content) {
|
|
2009
2385
|
return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
@@ -2125,21 +2501,25 @@ function addLeaf(ctx, segments, key, value) {
|
|
|
2125
2501
|
);
|
|
2126
2502
|
}
|
|
2127
2503
|
ctx.claimed.set(effectivePath, mapKey);
|
|
2504
|
+
if (typeof value !== "string") {
|
|
2505
|
+
ctx.excluded.push(mapKey);
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2128
2508
|
const { placeholders, isPlural } = ctx.derive(key, value);
|
|
2129
2509
|
ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
|
|
2130
2510
|
}
|
|
2131
2511
|
function addEntries(ctx, prefix, node) {
|
|
2132
|
-
for (const [key, value] of
|
|
2512
|
+
for (const [key, value] of node) {
|
|
2133
2513
|
const segments = [...prefix, key];
|
|
2134
|
-
if (
|
|
2135
|
-
addLeaf(ctx, segments, key, value);
|
|
2136
|
-
} else {
|
|
2514
|
+
if (isJsonNode(value)) {
|
|
2137
2515
|
addEntries(ctx, segments, value);
|
|
2516
|
+
} else {
|
|
2517
|
+
addLeaf(ctx, segments, key, value);
|
|
2138
2518
|
}
|
|
2139
2519
|
}
|
|
2140
2520
|
}
|
|
2141
|
-
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
2142
|
-
for (const [key, value] of
|
|
2521
|
+
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths, excluded) {
|
|
2522
|
+
for (const [key, value] of node) {
|
|
2143
2523
|
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
2144
2524
|
if (claimedPaths.has(path)) {
|
|
2145
2525
|
throw new AdapterError(
|
|
@@ -2148,34 +2528,35 @@ function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
|
|
|
2148
2528
|
);
|
|
2149
2529
|
}
|
|
2150
2530
|
claimedPaths.add(path);
|
|
2151
|
-
if (
|
|
2531
|
+
if (isJsonNode(value)) {
|
|
2532
|
+
addPathEntries(value, path, namespace, derive, out, claimedPaths, excluded);
|
|
2533
|
+
} else if (typeof value === "string") {
|
|
2152
2534
|
const { placeholders, isPlural } = derive(key, value);
|
|
2153
2535
|
out.set(path, { key: path, namespace, value, placeholders, isPlural });
|
|
2154
2536
|
} else {
|
|
2155
|
-
|
|
2537
|
+
excluded.push(path);
|
|
2156
2538
|
}
|
|
2157
2539
|
}
|
|
2158
2540
|
}
|
|
2159
2541
|
function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
|
|
2160
2542
|
const out = /* @__PURE__ */ new Map();
|
|
2161
2543
|
if (keyMode === "path-notation") {
|
|
2162
|
-
|
|
2163
|
-
|
|
2544
|
+
const excluded = [];
|
|
2545
|
+
addPathEntries(tree, "", namespace, derive, out, /* @__PURE__ */ new Set(), excluded);
|
|
2546
|
+
return { entries: out, excludedLeafPaths: excluded };
|
|
2164
2547
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
}
|
|
2168
|
-
function emptyNode() {
|
|
2169
|
-
return /* @__PURE__ */ Object.create(null);
|
|
2548
|
+
const ctx = { namespace, derive, out, claimed: /* @__PURE__ */ new Map(), excluded: [] };
|
|
2549
|
+
addEntries(ctx, [], tree);
|
|
2550
|
+
return { entries: out, excludedLeafPaths: ctx.excluded };
|
|
2170
2551
|
}
|
|
2171
2552
|
function descend(node, segment) {
|
|
2172
|
-
const next = node
|
|
2553
|
+
const next = node.get(segment);
|
|
2173
2554
|
if (next === void 0) {
|
|
2174
|
-
const created =
|
|
2175
|
-
node
|
|
2555
|
+
const created = /* @__PURE__ */ new Map();
|
|
2556
|
+
node.set(segment, created);
|
|
2176
2557
|
return created;
|
|
2177
2558
|
}
|
|
2178
|
-
if (
|
|
2559
|
+
if (next instanceof Map) {
|
|
2179
2560
|
return next;
|
|
2180
2561
|
}
|
|
2181
2562
|
throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
|
|
@@ -2189,13 +2570,13 @@ function setPath(root, segments, value) {
|
|
|
2189
2570
|
for (const segment of segments.slice(0, -1)) {
|
|
2190
2571
|
node = descend(node, segment);
|
|
2191
2572
|
}
|
|
2192
|
-
if (
|
|
2573
|
+
if (node.get(leaf) instanceof Map) {
|
|
2193
2574
|
throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
|
|
2194
2575
|
}
|
|
2195
|
-
node
|
|
2576
|
+
node.set(leaf, value);
|
|
2196
2577
|
}
|
|
2197
2578
|
function unflattenEntries(entries) {
|
|
2198
|
-
const root =
|
|
2579
|
+
const root = /* @__PURE__ */ new Map();
|
|
2199
2580
|
for (const [key, entry] of entries) {
|
|
2200
2581
|
setPath(root, decodeKeyToSegments(key), entry.value);
|
|
2201
2582
|
}
|
|
@@ -2216,9 +2597,9 @@ function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTre
|
|
|
2216
2597
|
try {
|
|
2217
2598
|
const tree = parse3(content);
|
|
2218
2599
|
validateTree?.(tree);
|
|
2219
|
-
const
|
|
2220
|
-
mergeDescriptions(entries, content, deriveDescriptions);
|
|
2221
|
-
return
|
|
2600
|
+
const result = flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
2601
|
+
mergeDescriptions(result.entries, content, deriveDescriptions);
|
|
2602
|
+
return result;
|
|
2222
2603
|
} catch (error) {
|
|
2223
2604
|
rethrowStructured(error, "The file could not be parsed.");
|
|
2224
2605
|
}
|
|
@@ -2249,7 +2630,7 @@ function createTreeFileAdapter(options) {
|
|
|
2249
2630
|
async read(filePath, locale) {
|
|
2250
2631
|
const content = await readFileContent(filePath);
|
|
2251
2632
|
const namespace = namespaceOf(filePath);
|
|
2252
|
-
const entries = toEntries(
|
|
2633
|
+
const { entries, excludedLeafPaths } = toEntries(
|
|
2253
2634
|
content,
|
|
2254
2635
|
namespace,
|
|
2255
2636
|
parse3,
|
|
@@ -2260,7 +2641,7 @@ function createTreeFileAdapter(options) {
|
|
|
2260
2641
|
);
|
|
2261
2642
|
const resource = { locale, namespace, format, entries };
|
|
2262
2643
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2263
|
-
return { resource, invalidIcuKeys };
|
|
2644
|
+
return { resource, invalidIcuKeys, excludedLeafPaths };
|
|
2264
2645
|
},
|
|
2265
2646
|
async write(resource, filePath) {
|
|
2266
2647
|
const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
|
|
@@ -2272,13 +2653,8 @@ function isMetadataKey(key) {
|
|
|
2272
2653
|
return key.startsWith("@");
|
|
2273
2654
|
}
|
|
2274
2655
|
function parseArbObject(content) {
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
parsed = JSON.parse(content);
|
|
2278
|
-
} catch {
|
|
2279
|
-
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
2280
|
-
}
|
|
2281
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2656
|
+
const parsed = parseOrderedJson(content);
|
|
2657
|
+
if (!(parsed instanceof Map)) {
|
|
2282
2658
|
throw new AdapterError(
|
|
2283
2659
|
"INVALID_STRUCTURE",
|
|
2284
2660
|
"The file is not a valid object (expected nested objects of string values)."
|
|
@@ -2287,10 +2663,10 @@ function parseArbObject(content) {
|
|
|
2287
2663
|
return parsed;
|
|
2288
2664
|
}
|
|
2289
2665
|
function stripArbMetadata(tree) {
|
|
2290
|
-
const out = /* @__PURE__ */
|
|
2291
|
-
for (const [key, value] of
|
|
2666
|
+
const out = /* @__PURE__ */ new Map();
|
|
2667
|
+
for (const [key, value] of tree) {
|
|
2292
2668
|
if (!isMetadataKey(key)) {
|
|
2293
|
-
out
|
|
2669
|
+
out.set(key, value);
|
|
2294
2670
|
}
|
|
2295
2671
|
}
|
|
2296
2672
|
return out;
|
|
@@ -2302,16 +2678,16 @@ function messageKeyForMetadata(key) {
|
|
|
2302
2678
|
return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
|
|
2303
2679
|
}
|
|
2304
2680
|
function descriptionOf(value) {
|
|
2305
|
-
if (
|
|
2681
|
+
if (!(value instanceof Map)) {
|
|
2306
2682
|
return void 0;
|
|
2307
2683
|
}
|
|
2308
|
-
const description = value.description;
|
|
2684
|
+
const description = value.get("description");
|
|
2309
2685
|
return typeof description === "string" ? description : void 0;
|
|
2310
2686
|
}
|
|
2311
2687
|
function extractArbDescriptions(content) {
|
|
2312
2688
|
const tree = parseArbObject(content);
|
|
2313
2689
|
const out = /* @__PURE__ */ new Map();
|
|
2314
|
-
for (const [key, value] of
|
|
2690
|
+
for (const [key, value] of tree) {
|
|
2315
2691
|
const messageKey = messageKeyForMetadata(key);
|
|
2316
2692
|
if (messageKey === null) {
|
|
2317
2693
|
continue;
|
|
@@ -2343,23 +2719,25 @@ async function readDestinationPairs(filePath) {
|
|
|
2343
2719
|
}
|
|
2344
2720
|
throw error;
|
|
2345
2721
|
}
|
|
2346
|
-
return
|
|
2722
|
+
return [...parseArbObject(content)];
|
|
2347
2723
|
}
|
|
2348
2724
|
async function buildArbWriteTree(entries, filePath) {
|
|
2349
2725
|
const messages = messagesFromEntries(entries);
|
|
2350
2726
|
const pairs = await readDestinationPairs(filePath);
|
|
2351
|
-
const out = /* @__PURE__ */
|
|
2727
|
+
const out = /* @__PURE__ */ new Map();
|
|
2352
2728
|
const consumed = /* @__PURE__ */ new Set();
|
|
2353
2729
|
for (const [key, value] of pairs ?? []) {
|
|
2354
2730
|
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
2355
2731
|
if (translated !== void 0) {
|
|
2356
2732
|
consumed.add(key);
|
|
2733
|
+
out.set(key, translated);
|
|
2734
|
+
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
2735
|
+
out.set(key, value);
|
|
2357
2736
|
}
|
|
2358
|
-
out[key] = translated ?? value;
|
|
2359
2737
|
}
|
|
2360
2738
|
for (const [key, value] of messages) {
|
|
2361
2739
|
if (!consumed.has(key)) {
|
|
2362
|
-
out
|
|
2740
|
+
out.set(key, value);
|
|
2363
2741
|
}
|
|
2364
2742
|
}
|
|
2365
2743
|
return out;
|
|
@@ -2448,8 +2826,8 @@ function createNextIntlJsonAdapter() {
|
|
|
2448
2826
|
});
|
|
2449
2827
|
}
|
|
2450
2828
|
function assertNoDottedNestedKey(tree) {
|
|
2451
|
-
for (const [key, value] of
|
|
2452
|
-
if (
|
|
2829
|
+
for (const [key, value] of tree) {
|
|
2830
|
+
if (!isJsonNode(value)) {
|
|
2453
2831
|
continue;
|
|
2454
2832
|
}
|
|
2455
2833
|
if (key.includes(".")) {
|
|
@@ -2464,8 +2842,8 @@ function assertNoDottedNestedKey(tree) {
|
|
|
2464
2842
|
function assertNotMixed(tree) {
|
|
2465
2843
|
let hasNested = false;
|
|
2466
2844
|
let hasFlatDottedKey = false;
|
|
2467
|
-
for (const [key, value] of
|
|
2468
|
-
if (
|
|
2845
|
+
for (const [key, value] of tree) {
|
|
2846
|
+
if (isJsonNode(value)) {
|
|
2469
2847
|
hasNested = true;
|
|
2470
2848
|
} else if (key.includes(".")) {
|
|
2471
2849
|
hasFlatDottedKey = true;
|
|
@@ -2501,9 +2879,9 @@ async function detectStyle(filePath) {
|
|
|
2501
2879
|
return "flat";
|
|
2502
2880
|
}
|
|
2503
2881
|
function buildFlatTree(entries) {
|
|
2504
|
-
const out = /* @__PURE__ */
|
|
2882
|
+
const out = /* @__PURE__ */ new Map();
|
|
2505
2883
|
for (const [key, entry] of entries) {
|
|
2506
|
-
out
|
|
2884
|
+
out.set(key, entry.value);
|
|
2507
2885
|
}
|
|
2508
2886
|
return out;
|
|
2509
2887
|
}
|
|
@@ -2521,7 +2899,6 @@ function createNgxTranslateJsonAdapter() {
|
|
|
2521
2899
|
}),
|
|
2522
2900
|
validateTree: assertNotMixed,
|
|
2523
2901
|
buildWriteTree: buildNgxWriteTree,
|
|
2524
|
-
// ngx-translate flat style uses dotted keys as path notation, not literal leaves.
|
|
2525
2902
|
keyMode: "path-notation"
|
|
2526
2903
|
});
|
|
2527
2904
|
}
|
|
@@ -2547,6 +2924,11 @@ var AdapterRegistry = class {
|
|
|
2547
2924
|
}
|
|
2548
2925
|
return { status: "resolved", adapter };
|
|
2549
2926
|
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Resolve by asking every registered adapter's `canHandle`. Several adapters can claim one file
|
|
2929
|
+
* (all JSON adapters claim `.json`), so more than one match is reported as `ambiguous` rather
|
|
2930
|
+
* than guessed at.
|
|
2931
|
+
*/
|
|
2550
2932
|
resolveByDetection(filePath, sample) {
|
|
2551
2933
|
const matches = this.adapters.filter((adapter) => adapter.canHandle(filePath, sample));
|
|
2552
2934
|
const first = matches[0];
|
|
@@ -2626,7 +3008,7 @@ function createFlatFileAdapter(options) {
|
|
|
2626
3008
|
const entries = toEntries2(content, namespace, parseEntries);
|
|
2627
3009
|
const resource = { locale, namespace, format, entries };
|
|
2628
3010
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
2629
|
-
return { resource, invalidIcuKeys };
|
|
3011
|
+
return { resource, invalidIcuKeys, excludedLeafPaths: [] };
|
|
2630
3012
|
},
|
|
2631
3013
|
async write(resource, filePath) {
|
|
2632
3014
|
const data = await serializeEntries(resource.entries, filePath);
|
|
@@ -2901,14 +3283,34 @@ function createXliffAdapter() {
|
|
|
2901
3283
|
extractPlaceholders: extractXliffPlaceholders
|
|
2902
3284
|
});
|
|
2903
3285
|
}
|
|
3286
|
+
function normalizeKey(key) {
|
|
3287
|
+
if (typeof key === "object" && key !== null) {
|
|
3288
|
+
throw new AdapterError(
|
|
3289
|
+
"INVALID_STRUCTURE",
|
|
3290
|
+
"A mapping key is a map or sequence (expected scalar keys)."
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
return String(key);
|
|
3294
|
+
}
|
|
3295
|
+
function normalizeYamlTree(value) {
|
|
3296
|
+
if (!(value instanceof Map)) {
|
|
3297
|
+
return value;
|
|
3298
|
+
}
|
|
3299
|
+
const out = /* @__PURE__ */ new Map();
|
|
3300
|
+
for (const [key, child] of value) {
|
|
3301
|
+
out.set(normalizeKey(key), normalizeYamlTree(child));
|
|
3302
|
+
}
|
|
3303
|
+
return out;
|
|
3304
|
+
}
|
|
2904
3305
|
function parseYamlObject(content) {
|
|
2905
3306
|
let parsed;
|
|
2906
3307
|
try {
|
|
2907
|
-
parsed = parse$1(content, { maxAliasCount: 100 });
|
|
3308
|
+
parsed = parse$1(content, { mapAsMap: true, maxAliasCount: 100 });
|
|
2908
3309
|
} catch {
|
|
2909
3310
|
throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
|
|
2910
3311
|
}
|
|
2911
|
-
|
|
3312
|
+
assertWithinDepth(parsed, MAX_DEPTH);
|
|
3313
|
+
return assertJsonRecord(normalizeYamlTree(parsed));
|
|
2912
3314
|
}
|
|
2913
3315
|
function createYamlAdapter() {
|
|
2914
3316
|
return createTreeFileAdapter({
|
|
@@ -2939,6 +3341,15 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
|
|
|
2939
3341
|
);
|
|
2940
3342
|
}
|
|
2941
3343
|
|
|
3344
|
+
// src/flow/read-target.ts
|
|
3345
|
+
async function readTargetResource(input) {
|
|
3346
|
+
const path = localeFilePath(input.cwd, input.filesPattern, input.locale);
|
|
3347
|
+
if (!await input.fs.fileExists(path)) {
|
|
3348
|
+
return { locale: input.locale, namespace: "", format: input.format, entries: /* @__PURE__ */ new Map() };
|
|
3349
|
+
}
|
|
3350
|
+
return (await input.adapter.read(path, input.locale)).resource;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
2942
3353
|
// src/flow/select-locales.ts
|
|
2943
3354
|
function selectLocales(config, requested) {
|
|
2944
3355
|
if (requested === void 0) {
|
|
@@ -2979,11 +3390,14 @@ async function readSource(config, cwd, fs, adapter) {
|
|
|
2979
3390
|
|
|
2980
3391
|
// src/flow/diff-locales.ts
|
|
2981
3392
|
async function readTarget(cwd, config, adapter, fs, locale) {
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3393
|
+
return readTargetResource({
|
|
3394
|
+
cwd,
|
|
3395
|
+
filesPattern: config.files.pattern,
|
|
3396
|
+
format: config.format,
|
|
3397
|
+
locale,
|
|
3398
|
+
adapter,
|
|
3399
|
+
fs
|
|
3400
|
+
});
|
|
2987
3401
|
}
|
|
2988
3402
|
async function diffLocales(input, deps = {}) {
|
|
2989
3403
|
const config = input.config;
|
|
@@ -3033,6 +3447,166 @@ async function diff(input, deps = {}) {
|
|
|
3033
3447
|
return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
|
|
3034
3448
|
}
|
|
3035
3449
|
|
|
3450
|
+
// src/flow/integrity-gate.ts
|
|
3451
|
+
function gateCandidateValue(sourceEntry, candidateValue, adapter) {
|
|
3452
|
+
const placeholderResult = adapter.comparePlaceholders?.(sourceEntry.value, candidateValue) ?? checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(candidateValue));
|
|
3453
|
+
if (!placeholderResult.matches) {
|
|
3454
|
+
return { accepted: false, reason: "placeholder" };
|
|
3455
|
+
}
|
|
3456
|
+
if (!adapter.validateMessage(candidateValue)) {
|
|
3457
|
+
return { accepted: false, reason: "icu" };
|
|
3458
|
+
}
|
|
3459
|
+
return { accepted: true };
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
// src/flow/edit-entry.ts
|
|
3463
|
+
async function editEntry(input, deps = {}) {
|
|
3464
|
+
const config = input.config;
|
|
3465
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3466
|
+
const fs = deps.fs ?? defaultFs;
|
|
3467
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3468
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3469
|
+
if (locale === void 0) {
|
|
3470
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3471
|
+
}
|
|
3472
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3473
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3474
|
+
if (sourceEntry === void 0) {
|
|
3475
|
+
throw new SdkError(
|
|
3476
|
+
"UNKNOWN_KEY",
|
|
3477
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3478
|
+
);
|
|
3479
|
+
}
|
|
3480
|
+
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
3481
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3482
|
+
const gate = gateCandidateValue(sourceEntry, input.value, adapter);
|
|
3483
|
+
if (!gate.accepted) {
|
|
3484
|
+
return { accepted: false, reason: gate.reason, value: input.value };
|
|
3485
|
+
}
|
|
3486
|
+
const merged = new Map(target.entries);
|
|
3487
|
+
merged.set(input.key, { ...sourceEntry, value: input.value, namespace: target.namespace });
|
|
3488
|
+
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
3489
|
+
await adapter.write(
|
|
3490
|
+
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
3491
|
+
path
|
|
3492
|
+
);
|
|
3493
|
+
await updateLockFileLocale(cwd, fs, locale, {
|
|
3494
|
+
mode: "merge",
|
|
3495
|
+
entries: { [input.key]: contentHash(sourceEntry) }
|
|
3496
|
+
});
|
|
3497
|
+
return { accepted: true, value: input.value };
|
|
3498
|
+
});
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
// src/flow/key-integrity.ts
|
|
3502
|
+
function checkEntryIntegrity(adapter, sourceEntry, targetEntry) {
|
|
3503
|
+
const result = adapter.comparePlaceholders?.(sourceEntry.value, targetEntry.value) ?? checkPlaceholders(sourceEntry.placeholders, targetEntry.placeholders);
|
|
3504
|
+
return {
|
|
3505
|
+
key: sourceEntry.key,
|
|
3506
|
+
hasPlaceholders: sourceEntry.placeholders.length > 0,
|
|
3507
|
+
matches: result.matches,
|
|
3508
|
+
missing: result.missing,
|
|
3509
|
+
extra: result.extra,
|
|
3510
|
+
icuValid: adapter.validateMessage(targetEntry.value)
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
function selectChangedKeys(changed, requested) {
|
|
3514
|
+
if (requested === void 0) {
|
|
3515
|
+
return changed;
|
|
3516
|
+
}
|
|
3517
|
+
const wanted = new Set(requested);
|
|
3518
|
+
return changed.filter((key) => wanted.has(key));
|
|
3519
|
+
}
|
|
3520
|
+
function integrityEntriesFor(source, target, adapter, changedKeys) {
|
|
3521
|
+
const entries = [];
|
|
3522
|
+
for (const key of changedKeys) {
|
|
3523
|
+
const sourceEntry = source.entries.get(key);
|
|
3524
|
+
const targetEntry = target.entries.get(key);
|
|
3525
|
+
if (sourceEntry === void 0 || targetEntry === void 0) {
|
|
3526
|
+
continue;
|
|
3527
|
+
}
|
|
3528
|
+
entries.push(checkEntryIntegrity(adapter, sourceEntry, targetEntry));
|
|
3529
|
+
}
|
|
3530
|
+
return entries;
|
|
3531
|
+
}
|
|
3532
|
+
async function keyIntegrity(input, deps = {}) {
|
|
3533
|
+
const config = input.config;
|
|
3534
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3535
|
+
const fs = deps.fs ?? defaultFs;
|
|
3536
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3537
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3538
|
+
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
3539
|
+
return Promise.all(
|
|
3540
|
+
selectLocales(config, input.locales).map(async (locale) => {
|
|
3541
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3542
|
+
const diffResult = diffResources(source.resource, target, {
|
|
3543
|
+
baseline: baselineFor(lock, locale)
|
|
3544
|
+
});
|
|
3545
|
+
const changedKeys = selectChangedKeys(diffResult.changed, input.keys);
|
|
3546
|
+
const entries = integrityEntriesFor(source.resource, target, adapter, changedKeys);
|
|
3547
|
+
return { locale, entries };
|
|
3548
|
+
})
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
// src/flow/key-value.ts
|
|
3553
|
+
async function keyValue(input, deps = {}) {
|
|
3554
|
+
const config = input.config;
|
|
3555
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3556
|
+
const fs = deps.fs ?? defaultFs;
|
|
3557
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3558
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3559
|
+
if (locale === void 0) {
|
|
3560
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3561
|
+
}
|
|
3562
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3563
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3564
|
+
if (sourceEntry === void 0) {
|
|
3565
|
+
throw new SdkError(
|
|
3566
|
+
"UNKNOWN_KEY",
|
|
3567
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3571
|
+
const targetEntry = target.entries.get(input.key);
|
|
3572
|
+
return {
|
|
3573
|
+
source: sourceEntry.value,
|
|
3574
|
+
...targetEntry !== void 0 ? { target: targetEntry.value } : {}
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
// src/flow/locale-snapshot.ts
|
|
3579
|
+
async function readLocaleFileSnapshot(input, deps = {}) {
|
|
3580
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3581
|
+
const fs = deps.fs ?? defaultFs;
|
|
3582
|
+
const adapter = selectAdapter(input.config.format, deps.adapterRegistry);
|
|
3583
|
+
const resource = await readTarget(cwd, input.config, adapter, fs, input.locale);
|
|
3584
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
3585
|
+
for (const [key, entry] of resource.entries) {
|
|
3586
|
+
hashes.set(key, contentHash(entry));
|
|
3587
|
+
}
|
|
3588
|
+
return { locale: input.locale, hashes };
|
|
3589
|
+
}
|
|
3590
|
+
function diffLocaleSnapshots(previous, current) {
|
|
3591
|
+
let added = 0;
|
|
3592
|
+
let changed = 0;
|
|
3593
|
+
for (const [key, hash] of current.hashes) {
|
|
3594
|
+
const previousHash = previous.hashes.get(key);
|
|
3595
|
+
if (previousHash === void 0) {
|
|
3596
|
+
added += 1;
|
|
3597
|
+
} else if (previousHash !== hash) {
|
|
3598
|
+
changed += 1;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
let removed = 0;
|
|
3602
|
+
for (const key of previous.hashes.keys()) {
|
|
3603
|
+
if (!current.hashes.has(key)) {
|
|
3604
|
+
removed += 1;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
return { added, changed, removed };
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3036
3610
|
// src/flow/lock-state.ts
|
|
3037
3611
|
function toLockLocaleState(locale, keyCount, diff2) {
|
|
3038
3612
|
return {
|
|
@@ -3080,6 +3654,214 @@ function selectProvider(config, createProvider = buildProvider) {
|
|
|
3080
3654
|
}
|
|
3081
3655
|
}
|
|
3082
3656
|
|
|
3657
|
+
// src/flow/retranslate-entry.ts
|
|
3658
|
+
function buildSingleEntryRequest(config, locale, sourceEntry, adapter) {
|
|
3659
|
+
return {
|
|
3660
|
+
sourceLocale: config.sourceLocale,
|
|
3661
|
+
targetLocale: locale,
|
|
3662
|
+
entries: [sourceEntry],
|
|
3663
|
+
extractPlaceholders: adapter.extractPlaceholders,
|
|
3664
|
+
...config.glossary !== void 0 ? { glossary: config.glossary } : {},
|
|
3665
|
+
...config.tone !== void 0 ? { tone: config.tone } : {},
|
|
3666
|
+
...adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: adapter.comparePlaceholders } : {}
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
async function retranslateEntry(input, deps = {}) {
|
|
3670
|
+
const config = input.config;
|
|
3671
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3672
|
+
const fs = deps.fs ?? defaultFs;
|
|
3673
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3674
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3675
|
+
if (locale === void 0) {
|
|
3676
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3677
|
+
}
|
|
3678
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3679
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3680
|
+
if (sourceEntry === void 0) {
|
|
3681
|
+
throw new SdkError(
|
|
3682
|
+
"UNKNOWN_KEY",
|
|
3683
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3684
|
+
);
|
|
3685
|
+
}
|
|
3686
|
+
const provider = selectProvider(config.provider, deps.createProvider);
|
|
3687
|
+
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
3688
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3689
|
+
const result = await provider.translateBatch(
|
|
3690
|
+
buildSingleEntryRequest(config, locale, sourceEntry, adapter)
|
|
3691
|
+
);
|
|
3692
|
+
const value = result.values.get(input.key);
|
|
3693
|
+
if (value === void 0) {
|
|
3694
|
+
throw new ProviderError(
|
|
3695
|
+
"INVALID_RESPONSE",
|
|
3696
|
+
`The provider returned no translated value for key "${input.key}".`
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
3699
|
+
const gate = gateCandidateValue(sourceEntry, value, adapter);
|
|
3700
|
+
if (!gate.accepted) {
|
|
3701
|
+
return { accepted: false, reason: gate.reason, value };
|
|
3702
|
+
}
|
|
3703
|
+
const merged = new Map(target.entries);
|
|
3704
|
+
merged.set(input.key, { ...sourceEntry, value, namespace: target.namespace });
|
|
3705
|
+
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
3706
|
+
await adapter.write(
|
|
3707
|
+
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
3708
|
+
path
|
|
3709
|
+
);
|
|
3710
|
+
await updateLockFileLocale(cwd, fs, locale, {
|
|
3711
|
+
mode: "merge",
|
|
3712
|
+
entries: { [input.key]: contentHash(sourceEntry) }
|
|
3713
|
+
});
|
|
3714
|
+
const reviewReasons = result.reviewFlags?.get(input.key)?.reasons ?? [];
|
|
3715
|
+
return { accepted: true, value, reviewReasons };
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
var RUN_STATUS_DIR_NAME = ".verbatra-local";
|
|
3719
|
+
var RUN_STATUS_FILE_NAME = "run-status.json";
|
|
3720
|
+
var CURRENT_VERSION2 = 1;
|
|
3721
|
+
var MAX_RUN_STATUS_FILE_BYTES = 16 * 1024 * 1024;
|
|
3722
|
+
var reviewReasonCodeSchema = z.enum([
|
|
3723
|
+
"LENGTH_RATIO_OUTLIER",
|
|
3724
|
+
"EQUALS_SOURCE",
|
|
3725
|
+
"GLOSSARY_TERM_MISSED",
|
|
3726
|
+
"INTEGRITY_REORDERED",
|
|
3727
|
+
"PROVIDER_DEGRADED"
|
|
3728
|
+
]);
|
|
3729
|
+
var needsReviewEntrySchema = z.object({
|
|
3730
|
+
key: z.string(),
|
|
3731
|
+
reasons: z.array(reviewReasonCodeSchema)
|
|
3732
|
+
});
|
|
3733
|
+
var usageSummarySchema = z.object({
|
|
3734
|
+
inputTokens: z.number().int().nonnegative(),
|
|
3735
|
+
outputTokens: z.number().int().nonnegative()
|
|
3736
|
+
});
|
|
3737
|
+
var runBudgetSchema = z.object({
|
|
3738
|
+
maxTokens: z.number().int().nonnegative(),
|
|
3739
|
+
behavior: z.enum(["warn", "stop"]),
|
|
3740
|
+
supported: z.boolean(),
|
|
3741
|
+
tokensUsed: z.number().int().nonnegative(),
|
|
3742
|
+
exceeded: z.boolean()
|
|
3743
|
+
});
|
|
3744
|
+
var runStatusLocaleSchema = z.object({
|
|
3745
|
+
locale: z.string(),
|
|
3746
|
+
status: z.enum(["succeeded", "failed"]),
|
|
3747
|
+
needsReview: z.array(needsReviewEntrySchema),
|
|
3748
|
+
usage: usageSummarySchema.optional()
|
|
3749
|
+
});
|
|
3750
|
+
var runStatusFileSchema = z.object({
|
|
3751
|
+
version: z.number().int().positive(),
|
|
3752
|
+
generatedAt: z.string(),
|
|
3753
|
+
usage: usageSummarySchema.optional(),
|
|
3754
|
+
budget: runBudgetSchema.optional(),
|
|
3755
|
+
locales: z.array(runStatusLocaleSchema)
|
|
3756
|
+
});
|
|
3757
|
+
function runStatusFilePath(cwd) {
|
|
3758
|
+
return resolve(cwd, RUN_STATUS_DIR_NAME, RUN_STATUS_FILE_NAME);
|
|
3759
|
+
}
|
|
3760
|
+
function toRunStatusLocale(locale) {
|
|
3761
|
+
return {
|
|
3762
|
+
locale: locale.locale,
|
|
3763
|
+
status: locale.status,
|
|
3764
|
+
needsReview: locale.needsReview,
|
|
3765
|
+
...locale.usage !== void 0 ? { usage: locale.usage } : {}
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
function buildRunStatusFile(summary, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3769
|
+
return {
|
|
3770
|
+
version: CURRENT_VERSION2,
|
|
3771
|
+
generatedAt,
|
|
3772
|
+
...summary.usage !== void 0 ? { usage: summary.usage } : {},
|
|
3773
|
+
...summary.budget !== void 0 ? { budget: summary.budget } : {},
|
|
3774
|
+
locales: summary.locales.map(toRunStatusLocale)
|
|
3775
|
+
};
|
|
3776
|
+
}
|
|
3777
|
+
function fromParsed(data) {
|
|
3778
|
+
return {
|
|
3779
|
+
version: data.version,
|
|
3780
|
+
generatedAt: data.generatedAt,
|
|
3781
|
+
...data.usage !== void 0 ? { usage: data.usage } : {},
|
|
3782
|
+
...data.budget !== void 0 ? { budget: data.budget } : {},
|
|
3783
|
+
locales: data.locales.map((locale) => ({
|
|
3784
|
+
locale: locale.locale,
|
|
3785
|
+
status: locale.status,
|
|
3786
|
+
needsReview: locale.needsReview,
|
|
3787
|
+
...locale.usage !== void 0 ? { usage: locale.usage } : {}
|
|
3788
|
+
}))
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
async function readRunStatusFile(path, fs) {
|
|
3792
|
+
const read = await fs.readFileBounded(path, MAX_RUN_STATUS_FILE_BYTES);
|
|
3793
|
+
if (read.kind !== "ok") {
|
|
3794
|
+
return void 0;
|
|
3795
|
+
}
|
|
3796
|
+
let parsed;
|
|
3797
|
+
try {
|
|
3798
|
+
parsed = JSON.parse(read.content);
|
|
3799
|
+
} catch {
|
|
3800
|
+
return void 0;
|
|
3801
|
+
}
|
|
3802
|
+
const result = runStatusFileSchema.safeParse(parsed);
|
|
3803
|
+
if (!result.success || result.data.version !== CURRENT_VERSION2) {
|
|
3804
|
+
return void 0;
|
|
3805
|
+
}
|
|
3806
|
+
return fromParsed(result.data);
|
|
3807
|
+
}
|
|
3808
|
+
async function writeRunStatusFile(path, data, fs) {
|
|
3809
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3810
|
+
await fs.writeFile(path, `${JSON.stringify(data, null, 2)}
|
|
3811
|
+
`);
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
// src/flow/run-status.ts
|
|
3815
|
+
async function runStatus(input = {}, deps = {}) {
|
|
3816
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3817
|
+
const fs = deps.fs ?? defaultFs;
|
|
3818
|
+
const file = await readRunStatusFile(runStatusFilePath(cwd), fs);
|
|
3819
|
+
if (file === void 0) {
|
|
3820
|
+
return { available: false };
|
|
3821
|
+
}
|
|
3822
|
+
return { available: true, ...file };
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
// src/flow/budget.ts
|
|
3826
|
+
function createBudgetTracker(maxTokens, behavior) {
|
|
3827
|
+
return { maxTokens, behavior, tokensUsed: 0, usageSeen: false, exceeded: false, stopped: false };
|
|
3828
|
+
}
|
|
3829
|
+
function foldTrackerUsage(tracker, usage) {
|
|
3830
|
+
if (usage === void 0) {
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
tracker.usageSeen = true;
|
|
3834
|
+
tracker.tokensUsed += usage.inputTokens + usage.outputTokens;
|
|
3835
|
+
}
|
|
3836
|
+
function checkBudgetTrip(tracker) {
|
|
3837
|
+
if (tracker.maxTokens === void 0 || tracker.exceeded || tracker.tokensUsed < tracker.maxTokens) {
|
|
3838
|
+
return false;
|
|
3839
|
+
}
|
|
3840
|
+
tracker.exceeded = true;
|
|
3841
|
+
if (tracker.behavior === "stop") {
|
|
3842
|
+
tracker.stopped = true;
|
|
3843
|
+
}
|
|
3844
|
+
return true;
|
|
3845
|
+
}
|
|
3846
|
+
function toBudgetSummary(tracker) {
|
|
3847
|
+
if (tracker.maxTokens === void 0) {
|
|
3848
|
+
return void 0;
|
|
3849
|
+
}
|
|
3850
|
+
return {
|
|
3851
|
+
maxTokens: tracker.maxTokens,
|
|
3852
|
+
behavior: tracker.behavior,
|
|
3853
|
+
supported: tracker.usageSeen,
|
|
3854
|
+
tokensUsed: tracker.tokensUsed,
|
|
3855
|
+
exceeded: tracker.exceeded
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
function budgetExceededNotice(tracker) {
|
|
3859
|
+
return {
|
|
3860
|
+
code: "BUDGET_TOKENS_EXCEEDED",
|
|
3861
|
+
message: `The run's cumulative token usage (${tracker.tokensUsed}) reached the configured budget of ${tracker.maxTokens} tokens (behavior: ${tracker.behavior}).`
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3083
3865
|
// src/flow/locale-failure.ts
|
|
3084
3866
|
function describeError(error) {
|
|
3085
3867
|
if (error instanceof Error) {
|
|
@@ -3099,8 +3881,10 @@ function failureSummary(locale, error) {
|
|
|
3099
3881
|
invalidIcuSource: [],
|
|
3100
3882
|
integrityMismatches: [],
|
|
3101
3883
|
providerFailures: [],
|
|
3884
|
+
budgetWithheld: [],
|
|
3102
3885
|
generated: [],
|
|
3103
3886
|
notices: [],
|
|
3887
|
+
needsReview: [],
|
|
3104
3888
|
error: describeError(error)
|
|
3105
3889
|
};
|
|
3106
3890
|
}
|
|
@@ -3270,7 +4054,43 @@ function planPluralGeneration(source, targetLocale, format) {
|
|
|
3270
4054
|
return { items };
|
|
3271
4055
|
}
|
|
3272
4056
|
|
|
4057
|
+
// src/flow/usage.ts
|
|
4058
|
+
function createUsageAccumulator() {
|
|
4059
|
+
return { total: void 0 };
|
|
4060
|
+
}
|
|
4061
|
+
function foldUsage(accumulator, usage) {
|
|
4062
|
+
if (usage === void 0) {
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
const prior = accumulator.total ?? { inputTokens: 0, outputTokens: 0 };
|
|
4066
|
+
accumulator.total = {
|
|
4067
|
+
inputTokens: prior.inputTokens + usage.inputTokens,
|
|
4068
|
+
outputTokens: prior.outputTokens + usage.outputTokens
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
4071
|
+
function combineUsage(a, b) {
|
|
4072
|
+
if (a === void 0) {
|
|
4073
|
+
return b;
|
|
4074
|
+
}
|
|
4075
|
+
if (b === void 0) {
|
|
4076
|
+
return a;
|
|
4077
|
+
}
|
|
4078
|
+
return {
|
|
4079
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
4080
|
+
outputTokens: a.outputTokens + b.outputTokens
|
|
4081
|
+
};
|
|
4082
|
+
}
|
|
4083
|
+
|
|
3273
4084
|
// src/flow/plural-generation.ts
|
|
4085
|
+
var EMPTY_RESULT = {
|
|
4086
|
+
accepted: [],
|
|
4087
|
+
withheld: [],
|
|
4088
|
+
providerFailures: [],
|
|
4089
|
+
budgetWithheld: [],
|
|
4090
|
+
notices: [],
|
|
4091
|
+
usage: void 0,
|
|
4092
|
+
tripped: false
|
|
4093
|
+
};
|
|
3274
4094
|
function generatedLockHash(governingEntries, category) {
|
|
3275
4095
|
const governingHashes = governingEntries.map(contentHash).sort();
|
|
3276
4096
|
return contentHash({
|
|
@@ -3284,7 +4104,6 @@ function syntheticEntry(item) {
|
|
|
3284
4104
|
...item.sourceEntry,
|
|
3285
4105
|
key: item.targetKey,
|
|
3286
4106
|
isPlural: true,
|
|
3287
|
-
// The CLDR category travels as data context (the meaning field), never the instruction channel.
|
|
3288
4107
|
meaning: `CLDR plural category "${item.category}"`
|
|
3289
4108
|
};
|
|
3290
4109
|
}
|
|
@@ -3309,23 +4128,45 @@ async function generatePluralForms(context) {
|
|
|
3309
4128
|
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
3310
4129
|
const stale = staleItems(plan.items, context.baseline);
|
|
3311
4130
|
if (stale.length === 0) {
|
|
3312
|
-
return
|
|
4131
|
+
return EMPTY_RESULT;
|
|
3313
4132
|
}
|
|
3314
4133
|
const accepted = [];
|
|
3315
4134
|
const withheld = [];
|
|
3316
4135
|
const providerFailures = [];
|
|
4136
|
+
const budgetWithheld = [];
|
|
3317
4137
|
const notices = [];
|
|
4138
|
+
const usage = createUsageAccumulator();
|
|
4139
|
+
let tripped = false;
|
|
3318
4140
|
for (const batch of chunk(stale, context.maxBatchSize)) {
|
|
3319
|
-
|
|
4141
|
+
if (context.budget.stopped) {
|
|
4142
|
+
for (const item of batch) {
|
|
4143
|
+
budgetWithheld.push(item.targetKey);
|
|
4144
|
+
}
|
|
4145
|
+
continue;
|
|
4146
|
+
}
|
|
4147
|
+
const subResult = await runGenerationSubBatch(
|
|
3320
4148
|
context,
|
|
3321
4149
|
batch,
|
|
3322
4150
|
accepted,
|
|
3323
4151
|
withheld,
|
|
3324
4152
|
providerFailures
|
|
3325
4153
|
);
|
|
3326
|
-
notices.push(...
|
|
4154
|
+
notices.push(...subResult.notices);
|
|
4155
|
+
foldUsage(usage, subResult.usage);
|
|
4156
|
+
foldTrackerUsage(context.budget, subResult.usage);
|
|
4157
|
+
if (checkBudgetTrip(context.budget)) {
|
|
4158
|
+
tripped = true;
|
|
4159
|
+
}
|
|
3327
4160
|
}
|
|
3328
|
-
return {
|
|
4161
|
+
return {
|
|
4162
|
+
accepted,
|
|
4163
|
+
withheld,
|
|
4164
|
+
providerFailures,
|
|
4165
|
+
budgetWithheld,
|
|
4166
|
+
notices,
|
|
4167
|
+
usage: usage.total,
|
|
4168
|
+
tripped
|
|
4169
|
+
};
|
|
3329
4170
|
}
|
|
3330
4171
|
async function runGenerationSubBatch(context, batch, accepted, withheld, providerFailures) {
|
|
3331
4172
|
let result;
|
|
@@ -3336,20 +4177,20 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3336
4177
|
for (const item of batch) {
|
|
3337
4178
|
providerFailures.push(item.targetKey);
|
|
3338
4179
|
}
|
|
3339
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
4180
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3340
4181
|
}
|
|
3341
4182
|
for (const item of batch) {
|
|
3342
|
-
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
4183
|
+
foldGenerationItem(item, result, context.adapter, accepted, withheld, providerFailures);
|
|
3343
4184
|
}
|
|
3344
|
-
return readNotices(result);
|
|
4185
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3345
4186
|
}
|
|
3346
|
-
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
4187
|
+
function foldGenerationItem(item, result, adapter, accepted, withheld, providerFailures) {
|
|
3347
4188
|
const value = result.values.get(item.targetKey);
|
|
3348
4189
|
if (value === void 0) {
|
|
3349
4190
|
providerFailures.push(item.targetKey);
|
|
3350
4191
|
return;
|
|
3351
4192
|
}
|
|
3352
|
-
if (
|
|
4193
|
+
if (gateCandidateValue(item.sourceEntry, value, adapter).accepted) {
|
|
3353
4194
|
accepted.push({
|
|
3354
4195
|
targetKey: item.targetKey,
|
|
3355
4196
|
entry: { ...syntheticEntry(item), value },
|
|
@@ -3361,16 +4202,6 @@ function foldGenerationItem(item, result, accepted, withheld, providerFailures)
|
|
|
3361
4202
|
}
|
|
3362
4203
|
|
|
3363
4204
|
// src/flow/locale-run.ts
|
|
3364
|
-
function emptyResource(locale, format) {
|
|
3365
|
-
return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
|
|
3366
|
-
}
|
|
3367
|
-
async function readTarget2(params) {
|
|
3368
|
-
const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
|
|
3369
|
-
if (!await params.fs.fileExists(path)) {
|
|
3370
|
-
return emptyResource(params.targetLocale, params.format);
|
|
3371
|
-
}
|
|
3372
|
-
return (await params.adapter.read(path, params.targetLocale)).resource;
|
|
3373
|
-
}
|
|
3374
4205
|
function buildRequest3(params, entries) {
|
|
3375
4206
|
return {
|
|
3376
4207
|
sourceLocale: params.sourceLocale,
|
|
@@ -3383,7 +4214,14 @@ function buildRequest3(params, entries) {
|
|
|
3383
4214
|
};
|
|
3384
4215
|
}
|
|
3385
4216
|
async function runLocale(params) {
|
|
3386
|
-
const target = await
|
|
4217
|
+
const target = await readTargetResource({
|
|
4218
|
+
cwd: params.cwd,
|
|
4219
|
+
filesPattern: params.filesPattern,
|
|
4220
|
+
format: params.format,
|
|
4221
|
+
locale: params.targetLocale,
|
|
4222
|
+
adapter: params.adapter,
|
|
4223
|
+
fs: params.fs
|
|
4224
|
+
});
|
|
3387
4225
|
const diff2 = diffResources(params.source, target, { baseline: params.baseline });
|
|
3388
4226
|
const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
|
|
3389
4227
|
const pruned = params.prune ? orphaned : [];
|
|
@@ -3409,6 +4247,7 @@ async function runLocale(params) {
|
|
|
3409
4247
|
generated: [],
|
|
3410
4248
|
integrityMismatches: [],
|
|
3411
4249
|
providerFailures: [],
|
|
4250
|
+
budgetWithheld: [],
|
|
3412
4251
|
pruned,
|
|
3413
4252
|
notices: sdkNotices
|
|
3414
4253
|
}),
|
|
@@ -3416,23 +4255,31 @@ async function runLocale(params) {
|
|
|
3416
4255
|
};
|
|
3417
4256
|
}
|
|
3418
4257
|
const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
|
|
4258
|
+
const startedStopped = params.budget.stopped;
|
|
3419
4259
|
const accepted = /* @__PURE__ */ new Map();
|
|
3420
4260
|
const integrityMismatches = [];
|
|
3421
4261
|
const providerFailures = [];
|
|
3422
|
-
const
|
|
4262
|
+
const budgetWithheld = [];
|
|
4263
|
+
const reviewFlags = /* @__PURE__ */ new Map();
|
|
4264
|
+
const translation = await translateAndCheck(
|
|
3423
4265
|
provider,
|
|
3424
4266
|
params,
|
|
3425
4267
|
entries,
|
|
3426
4268
|
accepted,
|
|
3427
4269
|
integrityMismatches,
|
|
3428
|
-
providerFailures
|
|
4270
|
+
providerFailures,
|
|
4271
|
+
budgetWithheld,
|
|
4272
|
+
reviewFlags
|
|
3429
4273
|
);
|
|
3430
4274
|
const merged = new Map(target.entries);
|
|
3431
4275
|
for (const key of pruned) {
|
|
3432
4276
|
merged.delete(key);
|
|
3433
4277
|
}
|
|
3434
|
-
for (const
|
|
3435
|
-
|
|
4278
|
+
for (const key of params.source.entries.keys()) {
|
|
4279
|
+
const hit = accepted.get(key);
|
|
4280
|
+
if (hit !== void 0) {
|
|
4281
|
+
merged.set(key, { ...hit.source, value: hit.value, namespace: target.namespace });
|
|
4282
|
+
}
|
|
3436
4283
|
}
|
|
3437
4284
|
const generation = await runGeneration(params, provider);
|
|
3438
4285
|
for (const form of generation.accepted) {
|
|
@@ -3451,16 +4298,19 @@ async function runLocale(params) {
|
|
|
3451
4298
|
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
3452
4299
|
const notices = [
|
|
3453
4300
|
...pluralNotices,
|
|
3454
|
-
...
|
|
3455
|
-
...generation.notices
|
|
4301
|
+
...translation.notices,
|
|
4302
|
+
...generation.notices,
|
|
4303
|
+
...budgetLocaleNotices(params.budget, startedStopped, translation.tripped, generation.tripped)
|
|
3456
4304
|
];
|
|
3457
4305
|
const withheld = /* @__PURE__ */ new Set([
|
|
3458
4306
|
...integrityMismatches,
|
|
3459
4307
|
...providerFailures,
|
|
3460
4308
|
...invalidIcuSource,
|
|
3461
4309
|
...generation.withheld,
|
|
3462
|
-
...generation.providerFailures
|
|
4310
|
+
...generation.providerFailures,
|
|
4311
|
+
...budgetWithheld
|
|
3463
4312
|
]);
|
|
4313
|
+
const localeUsage = combineUsage(translation.usage, generation.usage);
|
|
3464
4314
|
return {
|
|
3465
4315
|
summary: baseSummary({
|
|
3466
4316
|
locale: params.targetLocale,
|
|
@@ -3469,19 +4319,29 @@ async function runLocale(params) {
|
|
|
3469
4319
|
invalidIcuSource,
|
|
3470
4320
|
translated: [...accepted.keys()],
|
|
3471
4321
|
generated: generation.accepted.map((form) => form.targetKey).sort(),
|
|
3472
|
-
// Withheld generated forms surface alongside withheld translations: both failed integrity.
|
|
3473
4322
|
integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
|
|
3474
|
-
// A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
|
|
3475
4323
|
providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
|
|
4324
|
+
budgetWithheld: [...budgetWithheld, ...generation.budgetWithheld].sort(),
|
|
3476
4325
|
pruned,
|
|
3477
|
-
notices
|
|
4326
|
+
notices,
|
|
4327
|
+
needsReview: needsReviewFor(accepted.keys(), reviewFlags),
|
|
4328
|
+
...localeUsage !== void 0 ? { usage: localeUsage } : {}
|
|
3478
4329
|
}),
|
|
3479
4330
|
lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
|
|
3480
4331
|
};
|
|
3481
4332
|
}
|
|
4333
|
+
var NO_GENERATION_RESULT = {
|
|
4334
|
+
accepted: [],
|
|
4335
|
+
withheld: [],
|
|
4336
|
+
providerFailures: [],
|
|
4337
|
+
budgetWithheld: [],
|
|
4338
|
+
notices: [],
|
|
4339
|
+
usage: void 0,
|
|
4340
|
+
tripped: false
|
|
4341
|
+
};
|
|
3482
4342
|
async function runGeneration(params, provider) {
|
|
3483
4343
|
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
3484
|
-
return
|
|
4344
|
+
return NO_GENERATION_RESULT;
|
|
3485
4345
|
}
|
|
3486
4346
|
return generatePluralForms({
|
|
3487
4347
|
source: params.source,
|
|
@@ -3493,9 +4353,13 @@ async function runGeneration(params, provider) {
|
|
|
3493
4353
|
glossary: params.glossary,
|
|
3494
4354
|
tone: params.tone,
|
|
3495
4355
|
baseline: params.baseline,
|
|
3496
|
-
maxBatchSize: params.maxBatchSize
|
|
4356
|
+
maxBatchSize: params.maxBatchSize,
|
|
4357
|
+
budget: params.budget
|
|
3497
4358
|
});
|
|
3498
4359
|
}
|
|
4360
|
+
function budgetLocaleNotices(budget, startedStopped, mainTripped, generationTripped) {
|
|
4361
|
+
return startedStopped || mainTripped || generationTripped ? [budgetExceededNotice(budget)] : [];
|
|
4362
|
+
}
|
|
3499
4363
|
function pluralNoticeFor(params, merged) {
|
|
3500
4364
|
if (params.format !== "i18next-json") {
|
|
3501
4365
|
return [];
|
|
@@ -3516,26 +4380,53 @@ function baseSummary(parts) {
|
|
|
3516
4380
|
invalidIcuSource: parts.invalidIcuSource,
|
|
3517
4381
|
integrityMismatches: parts.integrityMismatches,
|
|
3518
4382
|
providerFailures: parts.providerFailures,
|
|
4383
|
+
budgetWithheld: parts.budgetWithheld,
|
|
3519
4384
|
generated: parts.generated,
|
|
3520
|
-
notices: parts.notices
|
|
4385
|
+
notices: parts.notices,
|
|
4386
|
+
needsReview: parts.needsReview ?? [],
|
|
4387
|
+
...parts.usage !== void 0 ? { usage: parts.usage } : {}
|
|
3521
4388
|
};
|
|
3522
4389
|
}
|
|
3523
|
-
|
|
4390
|
+
function needsReviewFor(acceptedKeys, reviewFlags) {
|
|
4391
|
+
const entries = [];
|
|
4392
|
+
for (const key of acceptedKeys) {
|
|
4393
|
+
const flag = reviewFlags.get(key);
|
|
4394
|
+
if (flag !== void 0) {
|
|
4395
|
+
entries.push({ key, reasons: flag.reasons });
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
return entries.sort((a, b) => a.key < b.key ? -1 : 1);
|
|
4399
|
+
}
|
|
4400
|
+
async function translateAndCheck(provider, params, entries, accepted, integrityMismatches, providerFailures, budgetWithheld, reviewFlags) {
|
|
3524
4401
|
const notices = [];
|
|
4402
|
+
const usage = createUsageAccumulator();
|
|
4403
|
+
let tripped = false;
|
|
3525
4404
|
for (const batch of chunk(entries, params.maxBatchSize)) {
|
|
3526
|
-
|
|
4405
|
+
if (params.budget.stopped) {
|
|
4406
|
+
for (const entry of batch) {
|
|
4407
|
+
budgetWithheld.push(entry.key);
|
|
4408
|
+
}
|
|
4409
|
+
continue;
|
|
4410
|
+
}
|
|
4411
|
+
const subResult = await runSubBatch(
|
|
3527
4412
|
provider,
|
|
3528
4413
|
params,
|
|
3529
4414
|
batch,
|
|
3530
4415
|
accepted,
|
|
3531
4416
|
integrityMismatches,
|
|
3532
|
-
providerFailures
|
|
4417
|
+
providerFailures,
|
|
4418
|
+
reviewFlags
|
|
3533
4419
|
);
|
|
3534
|
-
notices.push(...
|
|
4420
|
+
notices.push(...subResult.notices);
|
|
4421
|
+
foldUsage(usage, subResult.usage);
|
|
4422
|
+
foldTrackerUsage(params.budget, subResult.usage);
|
|
4423
|
+
if (checkBudgetTrip(params.budget)) {
|
|
4424
|
+
tripped = true;
|
|
4425
|
+
}
|
|
3535
4426
|
}
|
|
3536
|
-
return notices;
|
|
4427
|
+
return { notices, tripped, usage: usage.total };
|
|
3537
4428
|
}
|
|
3538
|
-
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures) {
|
|
4429
|
+
async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures, reviewFlags) {
|
|
3539
4430
|
let result;
|
|
3540
4431
|
try {
|
|
3541
4432
|
result = await provider.translateBatch(buildRequest3(params, batch));
|
|
@@ -3543,20 +4434,25 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3543
4434
|
for (const entry of batch) {
|
|
3544
4435
|
providerFailures.push(entry.key);
|
|
3545
4436
|
}
|
|
3546
|
-
return [subBatchFailedNotice(batch.length, error)];
|
|
4437
|
+
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3547
4438
|
}
|
|
3548
4439
|
for (const entry of batch) {
|
|
3549
|
-
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
4440
|
+
foldEntryResult(entry, result, params.adapter, accepted, integrityMismatches, providerFailures);
|
|
4441
|
+
}
|
|
4442
|
+
if (result.reviewFlags !== void 0) {
|
|
4443
|
+
for (const [key, flag] of result.reviewFlags) {
|
|
4444
|
+
reviewFlags.set(key, flag);
|
|
4445
|
+
}
|
|
3550
4446
|
}
|
|
3551
|
-
return readNotices(result);
|
|
4447
|
+
return { notices: readNotices(result), usage: result.usage };
|
|
3552
4448
|
}
|
|
3553
|
-
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
4449
|
+
function foldEntryResult(entry, result, adapter, accepted, integrityMismatches, providerFailures) {
|
|
3554
4450
|
const value = result.values.get(entry.key);
|
|
3555
4451
|
if (value === void 0) {
|
|
3556
4452
|
providerFailures.push(entry.key);
|
|
3557
4453
|
return;
|
|
3558
4454
|
}
|
|
3559
|
-
if (
|
|
4455
|
+
if (gateCandidateValue(entry, value, adapter).accepted) {
|
|
3560
4456
|
accepted.set(entry.key, { value, source: entry });
|
|
3561
4457
|
} else {
|
|
3562
4458
|
integrityMismatches.push(entry.key);
|
|
@@ -3598,6 +4494,79 @@ function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
|
|
|
3598
4494
|
}
|
|
3599
4495
|
|
|
3600
4496
|
// src/flow/translate-project.ts
|
|
4497
|
+
async function recordRunStatus(cwd, dryRun, summary, fs) {
|
|
4498
|
+
if (dryRun) {
|
|
4499
|
+
return;
|
|
4500
|
+
}
|
|
4501
|
+
try {
|
|
4502
|
+
await writeRunStatusFile(runStatusFilePath(cwd), buildRunStatusFile(summary), fs);
|
|
4503
|
+
} catch {
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
function buildLocaleRunParams(context, targetLocale, baseline) {
|
|
4507
|
+
return {
|
|
4508
|
+
source: context.source.resource,
|
|
4509
|
+
sourceInvalidIcuKeys: context.source.invalidIcuKeys,
|
|
4510
|
+
baseline,
|
|
4511
|
+
adapter: context.adapter,
|
|
4512
|
+
provider: context.provider,
|
|
4513
|
+
cwd: context.cwd,
|
|
4514
|
+
filesPattern: context.config.files.pattern,
|
|
4515
|
+
sourceLocale: context.config.sourceLocale,
|
|
4516
|
+
targetLocale,
|
|
4517
|
+
format: context.config.format,
|
|
4518
|
+
glossary: context.config.glossary,
|
|
4519
|
+
tone: context.config.tone,
|
|
4520
|
+
prune: context.prune,
|
|
4521
|
+
generatePlurals: context.generatePlurals,
|
|
4522
|
+
maxBatchSize: context.maxBatchSize,
|
|
4523
|
+
fs: context.fs,
|
|
4524
|
+
budget: context.budget
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
async function runDryLocale(context, targetLocale, lock) {
|
|
4528
|
+
const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
|
|
4529
|
+
return (await runLocale(params)).summary;
|
|
4530
|
+
}
|
|
4531
|
+
async function runLiveLocale(context, targetLocale) {
|
|
4532
|
+
return withLocaleWriteLock(context.cwd, targetLocale, context.fs, async () => {
|
|
4533
|
+
const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
|
|
4534
|
+
const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
|
|
4535
|
+
const result = await runLocale(params);
|
|
4536
|
+
await updateLockFileLocale(context.cwd, context.fs, targetLocale, {
|
|
4537
|
+
mode: "replace",
|
|
4538
|
+
entries: result.lockEntries
|
|
4539
|
+
});
|
|
4540
|
+
return result.summary;
|
|
4541
|
+
});
|
|
4542
|
+
}
|
|
4543
|
+
async function runOneLocale(targetLocale, run) {
|
|
4544
|
+
try {
|
|
4545
|
+
return await run();
|
|
4546
|
+
} catch (error) {
|
|
4547
|
+
if (error instanceof SdkError && error.code === "LOCK_FILE_INVALID") {
|
|
4548
|
+
throw error;
|
|
4549
|
+
}
|
|
4550
|
+
return failureSummary(targetLocale, error);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
async function runAllLocalesDry(context, targetLocales) {
|
|
4554
|
+
const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
|
|
4555
|
+
const summaries = [];
|
|
4556
|
+
for (const targetLocale of targetLocales) {
|
|
4557
|
+
summaries.push(
|
|
4558
|
+
await runOneLocale(targetLocale, () => runDryLocale(context, targetLocale, lock))
|
|
4559
|
+
);
|
|
4560
|
+
}
|
|
4561
|
+
return summaries;
|
|
4562
|
+
}
|
|
4563
|
+
async function runAllLocalesLive(context, targetLocales) {
|
|
4564
|
+
const summaries = [];
|
|
4565
|
+
for (const targetLocale of targetLocales) {
|
|
4566
|
+
summaries.push(await runOneLocale(targetLocale, () => runLiveLocale(context, targetLocale)));
|
|
4567
|
+
}
|
|
4568
|
+
return summaries;
|
|
4569
|
+
}
|
|
3601
4570
|
async function translate2(input, deps = {}) {
|
|
3602
4571
|
const config = input.config;
|
|
3603
4572
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -3606,44 +4575,42 @@ async function translate2(input, deps = {}) {
|
|
|
3606
4575
|
const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
|
|
3607
4576
|
const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
3608
4577
|
const fs = deps.fs ?? defaultFs;
|
|
4578
|
+
const budget = createBudgetTracker(
|
|
4579
|
+
config.maxTokens,
|
|
4580
|
+
config.budgetBehavior ?? DEFAULT_BUDGET_BEHAVIOR
|
|
4581
|
+
);
|
|
3609
4582
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3610
4583
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
3611
4584
|
const source = await readSource(config, cwd, fs, adapter);
|
|
3612
|
-
const
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
sourceLocale: config.sourceLocale,
|
|
3626
|
-
targetLocale,
|
|
3627
|
-
format: config.format,
|
|
3628
|
-
glossary: config.glossary,
|
|
3629
|
-
tone: config.tone,
|
|
3630
|
-
prune,
|
|
3631
|
-
generatePlurals,
|
|
3632
|
-
maxBatchSize,
|
|
3633
|
-
fs
|
|
3634
|
-
};
|
|
3635
|
-
const { summary, lockEntries } = await runLocale(params);
|
|
3636
|
-
if (!dryRun) {
|
|
3637
|
-
lock = updateLockLocale(lock, targetLocale, lockEntries);
|
|
3638
|
-
await writeLockFile(lockPath, lock, fs);
|
|
3639
|
-
}
|
|
3640
|
-
summaries.push(summary);
|
|
3641
|
-
} catch (error) {
|
|
3642
|
-
summaries.push(failureSummary(targetLocale, error));
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
4585
|
+
const context = {
|
|
4586
|
+
source,
|
|
4587
|
+
adapter,
|
|
4588
|
+
provider,
|
|
4589
|
+
cwd,
|
|
4590
|
+
config,
|
|
4591
|
+
prune,
|
|
4592
|
+
generatePlurals,
|
|
4593
|
+
maxBatchSize,
|
|
4594
|
+
fs,
|
|
4595
|
+
budget
|
|
4596
|
+
};
|
|
4597
|
+
const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales) : await runAllLocalesLive(context, config.targetLocales);
|
|
3645
4598
|
const { succeeded, failed } = partition(summaries);
|
|
3646
|
-
|
|
4599
|
+
const usage = summaries.reduce(
|
|
4600
|
+
(total, summary2) => combineUsage(total, summary2.usage),
|
|
4601
|
+
void 0
|
|
4602
|
+
);
|
|
4603
|
+
const budgetSummary = toBudgetSummary(budget);
|
|
4604
|
+
const summary = {
|
|
4605
|
+
dryRun,
|
|
4606
|
+
locales: summaries,
|
|
4607
|
+
succeeded,
|
|
4608
|
+
failed,
|
|
4609
|
+
...usage !== void 0 ? { usage } : {},
|
|
4610
|
+
...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
|
|
4611
|
+
};
|
|
4612
|
+
await recordRunStatus(cwd, dryRun, summary, fs);
|
|
4613
|
+
return summary;
|
|
3647
4614
|
}
|
|
3648
4615
|
var ExchangeError = class extends Error {
|
|
3649
4616
|
code;
|
|
@@ -3684,7 +4651,9 @@ var COLUMN = {
|
|
|
3684
4651
|
status: 4,
|
|
3685
4652
|
translation: 5,
|
|
3686
4653
|
sourceHash: 6,
|
|
3687
|
-
context: 7
|
|
4654
|
+
context: 7,
|
|
4655
|
+
reviewStatus: 8,
|
|
4656
|
+
reviewReasons: 9
|
|
3688
4657
|
};
|
|
3689
4658
|
var HEADERS = [
|
|
3690
4659
|
"Key",
|
|
@@ -3693,7 +4662,9 @@ var HEADERS = [
|
|
|
3693
4662
|
"Status",
|
|
3694
4663
|
"Translation",
|
|
3695
4664
|
"Source hash",
|
|
3696
|
-
"Context"
|
|
4665
|
+
"Context",
|
|
4666
|
+
"Review status",
|
|
4667
|
+
"Review reasons"
|
|
3697
4668
|
];
|
|
3698
4669
|
var HEADER_ROW = 1;
|
|
3699
4670
|
var INSTRUCTIONS_SHEET_NAME = "Instructions";
|
|
@@ -3713,7 +4684,9 @@ var COLUMN_WIDTHS = {
|
|
|
3713
4684
|
[COLUMN.current]: 50,
|
|
3714
4685
|
[COLUMN.status]: 12,
|
|
3715
4686
|
[COLUMN.translation]: 50,
|
|
3716
|
-
[COLUMN.context]: 50
|
|
4687
|
+
[COLUMN.context]: 50,
|
|
4688
|
+
[COLUMN.reviewStatus]: 12,
|
|
4689
|
+
[COLUMN.reviewReasons]: 40
|
|
3717
4690
|
};
|
|
3718
4691
|
var TEXT_NUMBER_FORMAT = "@";
|
|
3719
4692
|
function styleHeader(sheet) {
|
|
@@ -3745,7 +4718,9 @@ function writeRow(sheet, sheetRow) {
|
|
|
3745
4718
|
translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
|
|
3746
4719
|
row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
|
|
3747
4720
|
row.getCell(COLUMN.context).value = sheetRow.context;
|
|
3748
|
-
|
|
4721
|
+
row.getCell(COLUMN.reviewStatus).value = sheetRow.reviewStatus;
|
|
4722
|
+
row.getCell(COLUMN.reviewReasons).value = sheetRow.reviewReasons;
|
|
4723
|
+
for (let column = COLUMN.key; column <= COLUMN.reviewReasons; column += 1) {
|
|
3749
4724
|
const cell = row.getCell(column);
|
|
3750
4725
|
cell.protection = { locked: column !== COLUMN.translation };
|
|
3751
4726
|
if (column !== COLUMN.translation) {
|
|
@@ -3920,7 +4895,9 @@ var rowSchema = z.object({
|
|
|
3920
4895
|
status: z.enum(["new", "changed", "unchanged"]),
|
|
3921
4896
|
sourceHash: z.string(),
|
|
3922
4897
|
translation: z.string(),
|
|
3923
|
-
context: z.string()
|
|
4898
|
+
context: z.string(),
|
|
4899
|
+
reviewStatus: z.enum(["ok", "review"]).catch("ok"),
|
|
4900
|
+
reviewReasons: z.string().catch("")
|
|
3924
4901
|
});
|
|
3925
4902
|
function cellString(cell) {
|
|
3926
4903
|
const value = cell.value;
|
|
@@ -3954,9 +4931,9 @@ function parseRow(sheet, row) {
|
|
|
3954
4931
|
status: cellString(row.getCell(COLUMN.status)),
|
|
3955
4932
|
sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
|
|
3956
4933
|
translation: cellString(row.getCell(COLUMN.translation)),
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
4934
|
+
context: cellString(row.getCell(COLUMN.context)),
|
|
4935
|
+
reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
|
|
4936
|
+
reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
|
|
3960
4937
|
};
|
|
3961
4938
|
const result = rowSchema.safeParse(candidate);
|
|
3962
4939
|
if (!result.success) {
|
|
@@ -4023,14 +5000,34 @@ async function readWorkbook(bytes, options = {}) {
|
|
|
4023
5000
|
|
|
4024
5001
|
// src/flow/workbook/export-workbook.ts
|
|
4025
5002
|
var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
5003
|
+
function reasonLabel(reason) {
|
|
5004
|
+
return reason.toLowerCase().replace(/_/g, "-");
|
|
5005
|
+
}
|
|
5006
|
+
function reviewColumns(flag) {
|
|
5007
|
+
if (flag === void 0) {
|
|
5008
|
+
return { reviewStatus: "ok", reviewReasons: "" };
|
|
4030
5009
|
}
|
|
4031
|
-
return
|
|
5010
|
+
return { reviewStatus: "review", reviewReasons: flag.reasons.map(reasonLabel).join(", ") };
|
|
4032
5011
|
}
|
|
4033
|
-
function
|
|
5012
|
+
function computeRowReview(adapter, sourceValue, currentTarget, sourceLocale, targetLocale, glossary) {
|
|
5013
|
+
if (currentTarget === "") {
|
|
5014
|
+
return { reviewStatus: "ok", reviewReasons: "" };
|
|
5015
|
+
}
|
|
5016
|
+
const integrity = adapter.comparePlaceholders?.(sourceValue, currentTarget) ?? checkPlaceholders(
|
|
5017
|
+
adapter.extractPlaceholders(sourceValue),
|
|
5018
|
+
adapter.extractPlaceholders(currentTarget)
|
|
5019
|
+
);
|
|
5020
|
+
const flag = computeReviewFlags({
|
|
5021
|
+
sourceValue,
|
|
5022
|
+
translatedValue: currentTarget,
|
|
5023
|
+
sourceLocale,
|
|
5024
|
+
targetLocale,
|
|
5025
|
+
integrity,
|
|
5026
|
+
glossary
|
|
5027
|
+
});
|
|
5028
|
+
return reviewColumns(flag);
|
|
5029
|
+
}
|
|
5030
|
+
function buildRows(source, target, baseline, includeUnchanged, adapter, glossary) {
|
|
4034
5031
|
const diff2 = diffResources(source, target, { baseline });
|
|
4035
5032
|
const rows = [];
|
|
4036
5033
|
const add = (keys, status) => {
|
|
@@ -4039,14 +5036,23 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
4039
5036
|
if (sourceEntry === void 0) {
|
|
4040
5037
|
continue;
|
|
4041
5038
|
}
|
|
5039
|
+
const currentTarget = target.entries.get(key)?.value ?? "";
|
|
4042
5040
|
rows.push({
|
|
4043
5041
|
key,
|
|
4044
5042
|
source: sourceEntry.value,
|
|
4045
|
-
currentTarget
|
|
5043
|
+
currentTarget,
|
|
4046
5044
|
status,
|
|
4047
5045
|
sourceHash: contentHash(sourceEntry),
|
|
4048
5046
|
translation: "",
|
|
4049
|
-
context: sourceEntry.description ?? ""
|
|
5047
|
+
context: sourceEntry.description ?? "",
|
|
5048
|
+
...computeRowReview(
|
|
5049
|
+
adapter,
|
|
5050
|
+
sourceEntry.value,
|
|
5051
|
+
currentTarget,
|
|
5052
|
+
source.locale,
|
|
5053
|
+
target.locale,
|
|
5054
|
+
glossary
|
|
5055
|
+
)
|
|
4050
5056
|
});
|
|
4051
5057
|
}
|
|
4052
5058
|
};
|
|
@@ -4067,12 +5073,14 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
4067
5073
|
const locales = selectLocales(config, input.locales);
|
|
4068
5074
|
const sheets = await Promise.all(
|
|
4069
5075
|
locales.map(async (locale) => {
|
|
4070
|
-
const target = await
|
|
5076
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
4071
5077
|
const rows = buildRows(
|
|
4072
5078
|
source.resource,
|
|
4073
5079
|
target,
|
|
4074
5080
|
baselineFor(lock, locale),
|
|
4075
|
-
input.includeUnchanged ?? false
|
|
5081
|
+
input.includeUnchanged ?? false,
|
|
5082
|
+
adapter,
|
|
5083
|
+
config.glossary
|
|
4076
5084
|
);
|
|
4077
5085
|
return { locale, rows };
|
|
4078
5086
|
})
|
|
@@ -4103,14 +5111,8 @@ function judge(row, sourceEntry, adapter) {
|
|
|
4103
5111
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
4104
5112
|
return "drift";
|
|
4105
5113
|
}
|
|
4106
|
-
const
|
|
4107
|
-
|
|
4108
|
-
return "placeholder";
|
|
4109
|
-
}
|
|
4110
|
-
if (!adapter.validateMessage(row.translation)) {
|
|
4111
|
-
return "icu";
|
|
4112
|
-
}
|
|
4113
|
-
return void 0;
|
|
5114
|
+
const gate = gateCandidateValue(sourceEntry, row.translation, adapter);
|
|
5115
|
+
return gate.accepted ? void 0 : gate.reason;
|
|
4114
5116
|
}
|
|
4115
5117
|
function trackBlankDrift(row, params, buckets) {
|
|
4116
5118
|
const sourceEntry = params.source.entries.get(row.key);
|
|
@@ -4167,15 +5169,14 @@ function importLocale(params) {
|
|
|
4167
5169
|
translated: [...buckets.accepted.keys()].sort(),
|
|
4168
5170
|
unchanged: diff2.unchanged,
|
|
4169
5171
|
orphaned: diff2.orphaned,
|
|
4170
|
-
// Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
|
|
4171
5172
|
pruned: [],
|
|
4172
5173
|
invalidIcuSource,
|
|
4173
5174
|
integrityMismatches: [...buckets.mismatches].sort(),
|
|
4174
|
-
// A workbook import never calls a provider, so a provider-call failure cannot occur here.
|
|
4175
5175
|
providerFailures: [],
|
|
4176
|
-
|
|
5176
|
+
budgetWithheld: [],
|
|
4177
5177
|
generated: [],
|
|
4178
|
-
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : []
|
|
5178
|
+
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
|
|
5179
|
+
needsReview: []
|
|
4179
5180
|
};
|
|
4180
5181
|
return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
|
|
4181
5182
|
}
|
|
@@ -4195,13 +5196,6 @@ async function readWorkbookBytes(path, fs) {
|
|
|
4195
5196
|
}
|
|
4196
5197
|
return read.bytes;
|
|
4197
5198
|
}
|
|
4198
|
-
async function readTarget4(cwd, config, adapter, fs, locale) {
|
|
4199
|
-
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
4200
|
-
if (!await fs.fileExists(path)) {
|
|
4201
|
-
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
4202
|
-
}
|
|
4203
|
-
return (await adapter.read(path, locale)).resource;
|
|
4204
|
-
}
|
|
4205
5199
|
function mergeAccepted(target, accepted) {
|
|
4206
5200
|
const merged = new Map(target.entries);
|
|
4207
5201
|
for (const [key, { value, source }] of accepted) {
|
|
@@ -4232,7 +5226,7 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
4232
5226
|
`The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
|
|
4233
5227
|
);
|
|
4234
5228
|
}
|
|
4235
|
-
const target = await
|
|
5229
|
+
const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
|
|
4236
5230
|
const baseline = baselineFor(lock, sheet.locale);
|
|
4237
5231
|
const { summary, accepted } = importLocale({
|
|
4238
5232
|
sheet,
|
|
@@ -4275,8 +5269,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
4275
5269
|
} catch (error) {
|
|
4276
5270
|
throw new SdkError("SOURCE_INVALID", error.message);
|
|
4277
5271
|
}
|
|
4278
|
-
const
|
|
4279
|
-
let lock = await readLockFile(lockPath, fs);
|
|
5272
|
+
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
4280
5273
|
const ctx = {
|
|
4281
5274
|
config,
|
|
4282
5275
|
cwd,
|
|
@@ -4289,10 +5282,18 @@ async function importWorkbook(input, deps = {}) {
|
|
|
4289
5282
|
const summaries = [];
|
|
4290
5283
|
for (const sheet of data.sheets) {
|
|
4291
5284
|
try {
|
|
4292
|
-
|
|
4293
|
-
if (
|
|
4294
|
-
|
|
4295
|
-
|
|
5285
|
+
let summary;
|
|
5286
|
+
if (dryRun) {
|
|
5287
|
+
summary = (await runSheet(ctx, sheet, lock)).summary;
|
|
5288
|
+
} else {
|
|
5289
|
+
summary = await withLocaleWriteLock(cwd, sheet.locale, fs, async () => {
|
|
5290
|
+
const result = await runSheet(ctx, sheet, lock);
|
|
5291
|
+
await updateLockFileLocale(cwd, fs, sheet.locale, {
|
|
5292
|
+
mode: "replace",
|
|
5293
|
+
entries: result.lockEntries
|
|
5294
|
+
});
|
|
5295
|
+
return result.summary;
|
|
5296
|
+
});
|
|
4296
5297
|
}
|
|
4297
5298
|
summaries.push(summary);
|
|
4298
5299
|
} catch (error) {
|
|
@@ -4425,6 +5426,6 @@ async function watch(input, deps = {}) {
|
|
|
4425
5426
|
return { stop };
|
|
4426
5427
|
}
|
|
4427
5428
|
|
|
4428
|
-
export { DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, loadConfigWithMeta, loadLockFile, lockState, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
|
|
5429
|
+
export { DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
|
|
4429
5430
|
//# sourceMappingURL=index.js.map
|
|
4430
5431
|
//# sourceMappingURL=index.js.map
|