@verbatra/sdk 0.5.0-next.4 → 0.6.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 +794 -193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +462 -31
- package/dist/index.d.ts +462 -31
- package/dist/index.js +789 -195
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -145,6 +145,24 @@ async function atomicWrite(path, data) {
|
|
|
145
145
|
throw error;
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
async function createExclusive(path$1, data) {
|
|
149
|
+
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
150
|
+
let handle;
|
|
151
|
+
try {
|
|
152
|
+
handle = await promises.open(path$1, "wx");
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.code === "EEXIST") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
await handle.writeFile(data, "utf8");
|
|
161
|
+
} finally {
|
|
162
|
+
await handle.close();
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
148
166
|
var defaultFs = {
|
|
149
167
|
async fileExists(path) {
|
|
150
168
|
try {
|
|
@@ -157,7 +175,11 @@ var defaultFs = {
|
|
|
157
175
|
readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
|
|
158
176
|
readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
|
|
159
177
|
writeFile: (path, data) => atomicWrite(path, data),
|
|
160
|
-
writeBytes: (path, data) => atomicWrite(path, data)
|
|
178
|
+
writeBytes: (path, data) => atomicWrite(path, data),
|
|
179
|
+
createExclusive: (path, data) => createExclusive(path, data),
|
|
180
|
+
deleteFile: async (path) => {
|
|
181
|
+
await promises.rm(path, { force: true });
|
|
182
|
+
}
|
|
161
183
|
};
|
|
162
184
|
var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
|
|
163
185
|
var glossaryRecordSchema = zod.z.record(zod.z.string(), zod.z.string());
|
|
@@ -346,29 +368,18 @@ var TIMEOUT_STATUS = 408;
|
|
|
346
368
|
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
347
369
|
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
348
370
|
"RateLimitError",
|
|
349
|
-
// openai, @anthropic-ai/sdk
|
|
350
371
|
"TooManyRequestsError"
|
|
351
|
-
// deepl-node
|
|
352
372
|
]);
|
|
353
373
|
var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
354
374
|
"AuthenticationError",
|
|
355
|
-
// openai, @anthropic-ai/sdk (401)
|
|
356
375
|
"PermissionDeniedError",
|
|
357
|
-
// openai, @anthropic-ai/sdk (403)
|
|
358
376
|
"AuthorizationError"
|
|
359
|
-
// deepl-node
|
|
360
377
|
]);
|
|
361
378
|
var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
362
379
|
"APIConnectionTimeoutError",
|
|
363
|
-
// openai, @anthropic-ai/sdk
|
|
364
|
-
// deepl-node has no distinct timeout class; ConnectionError is its only network-failure
|
|
365
|
-
// class, so it is folded into TIMEOUT rather than the PROVIDER_ERROR fallback.
|
|
366
380
|
"ConnectionError"
|
|
367
381
|
]);
|
|
368
|
-
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
369
|
-
"APIUserAbortError"
|
|
370
|
-
// openai, @anthropic-ai/sdk
|
|
371
|
-
]);
|
|
382
|
+
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set(["APIUserAbortError"]);
|
|
372
383
|
function getErrorStatus(error) {
|
|
373
384
|
if (typeof error !== "object" || error === null || !("status" in error)) {
|
|
374
385
|
return void 0;
|
|
@@ -422,7 +433,6 @@ function isAbortError(error, signal) {
|
|
|
422
433
|
}
|
|
423
434
|
var REDACTED = "[REDACTED]";
|
|
424
435
|
var KEY_PATTERNS = [
|
|
425
|
-
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
426
436
|
/\bsk-[A-Za-z0-9_-]{8,}/g,
|
|
427
437
|
/AIza[0-9A-Za-z_-]{35}/g,
|
|
428
438
|
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(?::fx)?/g
|
|
@@ -444,6 +454,10 @@ var ProviderError = class extends Error {
|
|
|
444
454
|
/** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
|
|
445
455
|
code;
|
|
446
456
|
/**
|
|
457
|
+
* Redacts the message as a defense-in-depth backstop before storing it. The redaction runs with an
|
|
458
|
+
* empty secret argument so the ANTHROPIC_API_KEY environment default is not re-applied, keeping
|
|
459
|
+
* this generic error decoupled from any one provider's environment variable.
|
|
460
|
+
*
|
|
447
461
|
* @param code - The stable failure code.
|
|
448
462
|
* @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
|
|
449
463
|
*/
|
|
@@ -1011,9 +1025,6 @@ function createDeepLProvider(config, deps = {}) {
|
|
|
1011
1025
|
return {
|
|
1012
1026
|
id: PROVIDER_ID2,
|
|
1013
1027
|
kind: "machine-translation",
|
|
1014
|
-
// Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
|
|
1015
|
-
// generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
|
|
1016
|
-
// silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
|
|
1017
1028
|
supportsGlossary: validConfig.glossaryId !== void 0,
|
|
1018
1029
|
translateBatch: (request) => translate(bundle, validConfig, request)
|
|
1019
1030
|
};
|
|
@@ -1125,14 +1136,14 @@ function throwAbort(signal) {
|
|
|
1125
1136
|
throw new DOMException("This operation was aborted.", "AbortError");
|
|
1126
1137
|
}
|
|
1127
1138
|
function delay(ms, signal) {
|
|
1128
|
-
return new Promise((
|
|
1139
|
+
return new Promise((resolve9) => {
|
|
1129
1140
|
const onAbort = () => {
|
|
1130
1141
|
clearTimeout(timer);
|
|
1131
|
-
|
|
1142
|
+
resolve9();
|
|
1132
1143
|
};
|
|
1133
1144
|
const timer = setTimeout(() => {
|
|
1134
1145
|
signal?.removeEventListener("abort", onAbort);
|
|
1135
|
-
|
|
1146
|
+
resolve9();
|
|
1136
1147
|
}, ms);
|
|
1137
1148
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1138
1149
|
});
|
|
@@ -1180,12 +1191,10 @@ var TYPE_MAP = {
|
|
|
1180
1191
|
object: "OBJECT"
|
|
1181
1192
|
};
|
|
1182
1193
|
var HANDLED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1183
|
-
// transformed or recursed
|
|
1184
1194
|
"type",
|
|
1185
1195
|
"required",
|
|
1186
1196
|
"properties",
|
|
1187
1197
|
"items",
|
|
1188
|
-
// deliberately dropped (not part of Google's Schema dialect)
|
|
1189
1198
|
"$schema",
|
|
1190
1199
|
"additionalProperties"
|
|
1191
1200
|
]);
|
|
@@ -1753,6 +1762,60 @@ async function loadConfig(options = {}) {
|
|
|
1753
1762
|
const { config } = await loadConfigWithMeta(options);
|
|
1754
1763
|
return config;
|
|
1755
1764
|
}
|
|
1765
|
+
var LOCAL_DIR_NAME = ".verbatra-local";
|
|
1766
|
+
var LOCK_FILE_GUARD_STEM = "_lockfile";
|
|
1767
|
+
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
1768
|
+
var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
|
|
1769
|
+
function lockPath(cwd, stem) {
|
|
1770
|
+
return path.resolve(cwd, LOCAL_DIR_NAME, "locks", `${stem}.lock`);
|
|
1771
|
+
}
|
|
1772
|
+
function localeLockPath(cwd, locale) {
|
|
1773
|
+
return lockPath(cwd, locale);
|
|
1774
|
+
}
|
|
1775
|
+
function lockFileGuardPath(cwd) {
|
|
1776
|
+
return lockPath(cwd, LOCK_FILE_GUARD_STEM);
|
|
1777
|
+
}
|
|
1778
|
+
function sleep(ms) {
|
|
1779
|
+
return new Promise((res) => {
|
|
1780
|
+
setTimeout(res, ms);
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
function lockPayload() {
|
|
1784
|
+
return JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1785
|
+
}
|
|
1786
|
+
async function acquireLock(path, fs, pollIntervalMs, deadline) {
|
|
1787
|
+
for (; ; ) {
|
|
1788
|
+
if (await fs.createExclusive(path, lockPayload())) {
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (Date.now() >= deadline) {
|
|
1792
|
+
throw new SdkError(
|
|
1793
|
+
"LOCK_CONTENDED",
|
|
1794
|
+
`Could not acquire the write lock at ${path}: another process may be holding it. If no verbatra process is currently running, this lock file was likely left behind by one that was killed; delete it and retry.`
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
const jitter = Math.random() * pollIntervalMs;
|
|
1798
|
+
await sleep(pollIntervalMs + jitter);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
async function withFileLock(path, fs, fn, options) {
|
|
1802
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1803
|
+
const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
|
|
1804
|
+
await acquireLock(path, fs, pollIntervalMs, Date.now() + acquireTimeoutMs);
|
|
1805
|
+
try {
|
|
1806
|
+
return await fn();
|
|
1807
|
+
} finally {
|
|
1808
|
+
await fs.deleteFile(path);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
async function withLocaleWriteLock(cwd, locale, fs, fn, options = {}) {
|
|
1812
|
+
return withFileLock(localeLockPath(cwd, locale), fs, fn, options);
|
|
1813
|
+
}
|
|
1814
|
+
async function withLockFileGuard(cwd, fs, fn, options = {}) {
|
|
1815
|
+
return withFileLock(lockFileGuardPath(cwd), fs, fn, options);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// src/lock/lock-file.ts
|
|
1756
1819
|
var LOCK_FILE_NAME = "verbatra.lock.json";
|
|
1757
1820
|
var CURRENT_VERSION = 1;
|
|
1758
1821
|
var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
|
|
@@ -1764,8 +1827,7 @@ var lockFileSchema = zod.z.object({
|
|
|
1764
1827
|
function lockFilePath(cwd) {
|
|
1765
1828
|
return path.resolve(cwd, LOCK_FILE_NAME);
|
|
1766
1829
|
}
|
|
1767
|
-
|
|
1768
|
-
const read = await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES);
|
|
1830
|
+
function parseLockFileRead(read, path) {
|
|
1769
1831
|
if (read.kind === "missing") {
|
|
1770
1832
|
return EMPTY_LOCK;
|
|
1771
1833
|
}
|
|
@@ -1793,6 +1855,9 @@ async function readLockFile(path, fs) {
|
|
|
1793
1855
|
}
|
|
1794
1856
|
return result.data;
|
|
1795
1857
|
}
|
|
1858
|
+
async function readLockFile(path, fs) {
|
|
1859
|
+
return parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
|
|
1860
|
+
}
|
|
1796
1861
|
function baselineFor(lock, locale) {
|
|
1797
1862
|
return new Map(Object.entries(lock.locales[locale] ?? {}));
|
|
1798
1863
|
}
|
|
@@ -1808,14 +1873,30 @@ function byKey(a, b) {
|
|
|
1808
1873
|
function sortRecord(record) {
|
|
1809
1874
|
return Object.fromEntries(Object.entries(record).sort(byKey));
|
|
1810
1875
|
}
|
|
1811
|
-
|
|
1876
|
+
function serializeLockFile(lock) {
|
|
1812
1877
|
const locales = {};
|
|
1813
1878
|
for (const [locale, entries] of Object.entries(lock.locales).sort(byKey)) {
|
|
1814
1879
|
locales[locale] = sortRecord(entries);
|
|
1815
1880
|
}
|
|
1816
1881
|
const ordered = { version: lock.version, locales };
|
|
1817
|
-
|
|
1818
|
-
|
|
1882
|
+
return `${JSON.stringify(ordered, null, 2)}
|
|
1883
|
+
`;
|
|
1884
|
+
}
|
|
1885
|
+
function applyLockLocalePatch(currentEntries, patch) {
|
|
1886
|
+
if (patch.mode === "replace") {
|
|
1887
|
+
return patch.entries;
|
|
1888
|
+
}
|
|
1889
|
+
return { ...currentEntries, ...patch.entries };
|
|
1890
|
+
}
|
|
1891
|
+
async function updateLockFileLocale(cwd, fs, locale, patch) {
|
|
1892
|
+
return withLockFileGuard(cwd, fs, async () => {
|
|
1893
|
+
const path = lockFilePath(cwd);
|
|
1894
|
+
const lock = parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
|
|
1895
|
+
const nextEntries = applyLockLocalePatch(lock.locales[locale], patch);
|
|
1896
|
+
const next = updateLockLocale(lock, locale, nextEntries);
|
|
1897
|
+
await fs.writeFile(path, serializeLockFile(next));
|
|
1898
|
+
return next;
|
|
1899
|
+
});
|
|
1819
1900
|
}
|
|
1820
1901
|
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
1821
1902
|
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
@@ -2077,10 +2158,68 @@ var AdapterError = class extends Error {
|
|
|
2077
2158
|
};
|
|
2078
2159
|
var MAX_DEPTH = 100;
|
|
2079
2160
|
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
2080
|
-
var
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2161
|
+
var SENTINEL_ESCAPE = "\\u0001";
|
|
2162
|
+
function isJsonWhitespace(char) {
|
|
2163
|
+
return char === " " || char === " " || char === "\n" || char === "\r";
|
|
2164
|
+
}
|
|
2165
|
+
function endOfStringToken(content, openQuote) {
|
|
2166
|
+
let index = openQuote + 1;
|
|
2167
|
+
while (index < content.length) {
|
|
2168
|
+
const char = content[index];
|
|
2169
|
+
if (char === "\\") {
|
|
2170
|
+
index += 2;
|
|
2171
|
+
} else if (char === '"') {
|
|
2172
|
+
return index + 1;
|
|
2173
|
+
} else {
|
|
2174
|
+
index += 1;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return index;
|
|
2178
|
+
}
|
|
2179
|
+
function isKeyToken(content, end) {
|
|
2180
|
+
let index = end;
|
|
2181
|
+
while (isJsonWhitespace(content[index])) {
|
|
2182
|
+
index += 1;
|
|
2183
|
+
}
|
|
2184
|
+
return content[index] === ":";
|
|
2185
|
+
}
|
|
2186
|
+
function prefixKeyToken(token) {
|
|
2187
|
+
if (token.includes(SENTINEL_ESCAPE)) {
|
|
2188
|
+
throw new AdapterError(
|
|
2189
|
+
"INVALID_STRUCTURE",
|
|
2190
|
+
"A key contains a reserved control-character escape."
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
return `"${SENTINEL_ESCAPE}${token.slice(1)}`;
|
|
2194
|
+
}
|
|
2195
|
+
function prefixKeyTokens(content) {
|
|
2196
|
+
let out = "";
|
|
2197
|
+
let index = 0;
|
|
2198
|
+
while (index < content.length) {
|
|
2199
|
+
const openQuote = content.indexOf('"', index);
|
|
2200
|
+
if (openQuote === -1) {
|
|
2201
|
+
return out + content.slice(index);
|
|
2202
|
+
}
|
|
2203
|
+
const end = endOfStringToken(content, openQuote);
|
|
2204
|
+
const token = content.slice(openQuote, end);
|
|
2205
|
+
out += content.slice(index, openQuote);
|
|
2206
|
+
out += isKeyToken(content, end) ? prefixKeyToken(token) : token;
|
|
2207
|
+
index = end;
|
|
2208
|
+
}
|
|
2209
|
+
return out;
|
|
2210
|
+
}
|
|
2211
|
+
function childrenOf(node) {
|
|
2212
|
+
if (node instanceof Map) {
|
|
2213
|
+
return node.values();
|
|
2214
|
+
}
|
|
2215
|
+
if (Array.isArray(node)) {
|
|
2216
|
+
return node;
|
|
2217
|
+
}
|
|
2218
|
+
if (typeof node === "object" && node !== null) {
|
|
2219
|
+
return Object.values(node);
|
|
2220
|
+
}
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2084
2223
|
function assertWithinDepth(value, max) {
|
|
2085
2224
|
const stack = [{ node: value, depth: 1 }];
|
|
2086
2225
|
while (stack.length > 0) {
|
|
@@ -2088,44 +2227,117 @@ function assertWithinDepth(value, max) {
|
|
|
2088
2227
|
if (top === void 0) {
|
|
2089
2228
|
break;
|
|
2090
2229
|
}
|
|
2091
|
-
const
|
|
2092
|
-
if (
|
|
2230
|
+
const children = childrenOf(top.node);
|
|
2231
|
+
if (children === null) {
|
|
2093
2232
|
continue;
|
|
2094
2233
|
}
|
|
2095
|
-
if (depth > max) {
|
|
2234
|
+
if (top.depth > max) {
|
|
2096
2235
|
throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
|
|
2097
2236
|
}
|
|
2098
|
-
for (const child of
|
|
2099
|
-
stack.push({ node: child, depth: depth + 1 });
|
|
2237
|
+
for (const child of children) {
|
|
2238
|
+
stack.push({ node: child, depth: top.depth + 1 });
|
|
2100
2239
|
}
|
|
2101
2240
|
}
|
|
2102
2241
|
}
|
|
2103
|
-
function
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
if (!result.success) {
|
|
2107
|
-
throw new AdapterError(
|
|
2108
|
-
"INVALID_STRUCTURE",
|
|
2109
|
-
"The file is not a valid object (expected nested objects of string, number, boolean, or null leaves)."
|
|
2110
|
-
);
|
|
2242
|
+
function toOrdered(node) {
|
|
2243
|
+
if (Array.isArray(node)) {
|
|
2244
|
+
return node.map((child) => toOrdered(child));
|
|
2111
2245
|
}
|
|
2112
|
-
|
|
2246
|
+
if (typeof node === "object" && node !== null) {
|
|
2247
|
+
const out = /* @__PURE__ */ new Map();
|
|
2248
|
+
for (const [key, child] of Object.entries(node)) {
|
|
2249
|
+
out.set(key.slice(1), toOrdered(child));
|
|
2250
|
+
}
|
|
2251
|
+
return out;
|
|
2252
|
+
}
|
|
2253
|
+
return node;
|
|
2113
2254
|
}
|
|
2114
|
-
function
|
|
2255
|
+
function parseOrderedJson(content) {
|
|
2256
|
+
const prefixed = prefixKeyTokens(content);
|
|
2115
2257
|
let parsed;
|
|
2116
2258
|
try {
|
|
2117
|
-
parsed = JSON.parse(
|
|
2259
|
+
parsed = JSON.parse(prefixed);
|
|
2118
2260
|
} catch {
|
|
2119
2261
|
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
2120
2262
|
}
|
|
2121
|
-
|
|
2263
|
+
assertWithinDepth(parsed, MAX_DEPTH);
|
|
2264
|
+
return toOrdered(parsed);
|
|
2122
2265
|
}
|
|
2123
|
-
function
|
|
2124
|
-
|
|
2266
|
+
function printRecord(record, indent) {
|
|
2267
|
+
if (record.size === 0) {
|
|
2268
|
+
return "{}";
|
|
2269
|
+
}
|
|
2270
|
+
const childIndent = `${indent} `;
|
|
2271
|
+
const lines = [];
|
|
2272
|
+
for (const [key, child] of record) {
|
|
2273
|
+
lines.push(`${childIndent}${JSON.stringify(key)}: ${printValue(child, childIndent)}`);
|
|
2274
|
+
}
|
|
2275
|
+
return `{
|
|
2276
|
+
${lines.join(",\n")}
|
|
2277
|
+
${indent}}`;
|
|
2278
|
+
}
|
|
2279
|
+
function printArray(items, indent) {
|
|
2280
|
+
if (items.length === 0) {
|
|
2281
|
+
return "[]";
|
|
2282
|
+
}
|
|
2283
|
+
const childIndent = `${indent} `;
|
|
2284
|
+
const lines = items.map((item) => `${childIndent}${printValue(item, childIndent)}`);
|
|
2285
|
+
return `[
|
|
2286
|
+
${lines.join(",\n")}
|
|
2287
|
+
${indent}]`;
|
|
2288
|
+
}
|
|
2289
|
+
function printValue(value, indent) {
|
|
2290
|
+
if (value instanceof Map) {
|
|
2291
|
+
return printRecord(value, indent);
|
|
2292
|
+
}
|
|
2293
|
+
if (Array.isArray(value)) {
|
|
2294
|
+
return printArray(value, indent);
|
|
2295
|
+
}
|
|
2296
|
+
return JSON.stringify(value);
|
|
2297
|
+
}
|
|
2298
|
+
function serializeOrderedJson(value) {
|
|
2299
|
+
return `${printValue(value, "")}
|
|
2125
2300
|
`;
|
|
2126
2301
|
}
|
|
2302
|
+
var INVALID_STRUCTURE_MESSAGE = "The file is not a valid object (expected nested objects of string, number, boolean, or null leaves).";
|
|
2303
|
+
function isJsonLeaf(value) {
|
|
2304
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
|
|
2305
|
+
}
|
|
2306
|
+
function assertNodesValid(root) {
|
|
2307
|
+
const stack = [root];
|
|
2308
|
+
while (stack.length > 0) {
|
|
2309
|
+
const node = stack.pop();
|
|
2310
|
+
if (node === void 0) {
|
|
2311
|
+
break;
|
|
2312
|
+
}
|
|
2313
|
+
for (const [key, child] of node) {
|
|
2314
|
+
if (typeof key !== "string") {
|
|
2315
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2316
|
+
}
|
|
2317
|
+
if (child instanceof Map) {
|
|
2318
|
+
stack.push(child);
|
|
2319
|
+
} else if (!isJsonLeaf(child)) {
|
|
2320
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
function assertJsonRecord(value) {
|
|
2326
|
+
assertWithinDepth(value, MAX_DEPTH);
|
|
2327
|
+
if (!(value instanceof Map)) {
|
|
2328
|
+
throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
|
|
2329
|
+
}
|
|
2330
|
+
assertNodesValid(value);
|
|
2331
|
+
return value;
|
|
2332
|
+
}
|
|
2333
|
+
function parseJsonObject(content) {
|
|
2334
|
+
return assertJsonRecord(parseOrderedJson(content));
|
|
2335
|
+
}
|
|
2336
|
+
function serializeJsonTree(tree) {
|
|
2337
|
+
return serializeOrderedJson(tree);
|
|
2338
|
+
}
|
|
2127
2339
|
function isJsonNode(value) {
|
|
2128
|
-
return
|
|
2340
|
+
return value instanceof Map;
|
|
2129
2341
|
}
|
|
2130
2342
|
function namespaceOf(filePath) {
|
|
2131
2343
|
return path.basename(filePath, path.extname(filePath));
|
|
@@ -2327,7 +2539,7 @@ function addLeaf(ctx, segments, key, value) {
|
|
|
2327
2539
|
ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
|
|
2328
2540
|
}
|
|
2329
2541
|
function addEntries(ctx, prefix, node) {
|
|
2330
|
-
for (const [key, value] of
|
|
2542
|
+
for (const [key, value] of node) {
|
|
2331
2543
|
const segments = [...prefix, key];
|
|
2332
2544
|
if (isJsonNode(value)) {
|
|
2333
2545
|
addEntries(ctx, segments, value);
|
|
@@ -2337,7 +2549,7 @@ function addEntries(ctx, prefix, node) {
|
|
|
2337
2549
|
}
|
|
2338
2550
|
}
|
|
2339
2551
|
function addPathEntries(node, prefix, namespace, derive, out, claimedPaths, excluded) {
|
|
2340
|
-
for (const [key, value] of
|
|
2552
|
+
for (const [key, value] of node) {
|
|
2341
2553
|
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
2342
2554
|
if (claimedPaths.has(path)) {
|
|
2343
2555
|
throw new AdapterError(
|
|
@@ -2367,17 +2579,14 @@ function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
|
|
|
2367
2579
|
addEntries(ctx, [], tree);
|
|
2368
2580
|
return { entries: out, excludedLeafPaths: ctx.excluded };
|
|
2369
2581
|
}
|
|
2370
|
-
function emptyNode() {
|
|
2371
|
-
return /* @__PURE__ */ Object.create(null);
|
|
2372
|
-
}
|
|
2373
2582
|
function descend(node, segment) {
|
|
2374
|
-
const next = node
|
|
2583
|
+
const next = node.get(segment);
|
|
2375
2584
|
if (next === void 0) {
|
|
2376
|
-
const created =
|
|
2377
|
-
node
|
|
2585
|
+
const created = /* @__PURE__ */ new Map();
|
|
2586
|
+
node.set(segment, created);
|
|
2378
2587
|
return created;
|
|
2379
2588
|
}
|
|
2380
|
-
if (
|
|
2589
|
+
if (next instanceof Map) {
|
|
2381
2590
|
return next;
|
|
2382
2591
|
}
|
|
2383
2592
|
throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
|
|
@@ -2391,13 +2600,13 @@ function setPath(root, segments, value) {
|
|
|
2391
2600
|
for (const segment of segments.slice(0, -1)) {
|
|
2392
2601
|
node = descend(node, segment);
|
|
2393
2602
|
}
|
|
2394
|
-
if (
|
|
2603
|
+
if (node.get(leaf) instanceof Map) {
|
|
2395
2604
|
throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
|
|
2396
2605
|
}
|
|
2397
|
-
node
|
|
2606
|
+
node.set(leaf, value);
|
|
2398
2607
|
}
|
|
2399
2608
|
function unflattenEntries(entries) {
|
|
2400
|
-
const root =
|
|
2609
|
+
const root = /* @__PURE__ */ new Map();
|
|
2401
2610
|
for (const [key, entry] of entries) {
|
|
2402
2611
|
setPath(root, decodeKeyToSegments(key), entry.value);
|
|
2403
2612
|
}
|
|
@@ -2474,13 +2683,8 @@ function isMetadataKey(key) {
|
|
|
2474
2683
|
return key.startsWith("@");
|
|
2475
2684
|
}
|
|
2476
2685
|
function parseArbObject(content) {
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
parsed = JSON.parse(content);
|
|
2480
|
-
} catch {
|
|
2481
|
-
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
2482
|
-
}
|
|
2483
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2686
|
+
const parsed = parseOrderedJson(content);
|
|
2687
|
+
if (!(parsed instanceof Map)) {
|
|
2484
2688
|
throw new AdapterError(
|
|
2485
2689
|
"INVALID_STRUCTURE",
|
|
2486
2690
|
"The file is not a valid object (expected nested objects of string values)."
|
|
@@ -2489,10 +2693,10 @@ function parseArbObject(content) {
|
|
|
2489
2693
|
return parsed;
|
|
2490
2694
|
}
|
|
2491
2695
|
function stripArbMetadata(tree) {
|
|
2492
|
-
const out = /* @__PURE__ */
|
|
2493
|
-
for (const [key, value] of
|
|
2696
|
+
const out = /* @__PURE__ */ new Map();
|
|
2697
|
+
for (const [key, value] of tree) {
|
|
2494
2698
|
if (!isMetadataKey(key)) {
|
|
2495
|
-
out
|
|
2699
|
+
out.set(key, value);
|
|
2496
2700
|
}
|
|
2497
2701
|
}
|
|
2498
2702
|
return out;
|
|
@@ -2504,16 +2708,16 @@ function messageKeyForMetadata(key) {
|
|
|
2504
2708
|
return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
|
|
2505
2709
|
}
|
|
2506
2710
|
function descriptionOf(value) {
|
|
2507
|
-
if (
|
|
2711
|
+
if (!(value instanceof Map)) {
|
|
2508
2712
|
return void 0;
|
|
2509
2713
|
}
|
|
2510
|
-
const description = value.description;
|
|
2714
|
+
const description = value.get("description");
|
|
2511
2715
|
return typeof description === "string" ? description : void 0;
|
|
2512
2716
|
}
|
|
2513
2717
|
function extractArbDescriptions(content) {
|
|
2514
2718
|
const tree = parseArbObject(content);
|
|
2515
2719
|
const out = /* @__PURE__ */ new Map();
|
|
2516
|
-
for (const [key, value] of
|
|
2720
|
+
for (const [key, value] of tree) {
|
|
2517
2721
|
const messageKey = messageKeyForMetadata(key);
|
|
2518
2722
|
if (messageKey === null) {
|
|
2519
2723
|
continue;
|
|
@@ -2545,25 +2749,25 @@ async function readDestinationPairs(filePath) {
|
|
|
2545
2749
|
}
|
|
2546
2750
|
throw error;
|
|
2547
2751
|
}
|
|
2548
|
-
return
|
|
2752
|
+
return [...parseArbObject(content)];
|
|
2549
2753
|
}
|
|
2550
2754
|
async function buildArbWriteTree(entries, filePath) {
|
|
2551
2755
|
const messages = messagesFromEntries(entries);
|
|
2552
2756
|
const pairs = await readDestinationPairs(filePath);
|
|
2553
|
-
const out = /* @__PURE__ */
|
|
2757
|
+
const out = /* @__PURE__ */ new Map();
|
|
2554
2758
|
const consumed = /* @__PURE__ */ new Set();
|
|
2555
2759
|
for (const [key, value] of pairs ?? []) {
|
|
2556
2760
|
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
2557
2761
|
if (translated !== void 0) {
|
|
2558
2762
|
consumed.add(key);
|
|
2559
|
-
out
|
|
2763
|
+
out.set(key, translated);
|
|
2560
2764
|
} else if (isMetadataKey(key) || typeof value === "string") {
|
|
2561
|
-
out
|
|
2765
|
+
out.set(key, value);
|
|
2562
2766
|
}
|
|
2563
2767
|
}
|
|
2564
2768
|
for (const [key, value] of messages) {
|
|
2565
2769
|
if (!consumed.has(key)) {
|
|
2566
|
-
out
|
|
2770
|
+
out.set(key, value);
|
|
2567
2771
|
}
|
|
2568
2772
|
}
|
|
2569
2773
|
return out;
|
|
@@ -2652,7 +2856,7 @@ function createNextIntlJsonAdapter() {
|
|
|
2652
2856
|
});
|
|
2653
2857
|
}
|
|
2654
2858
|
function assertNoDottedNestedKey(tree) {
|
|
2655
|
-
for (const [key, value] of
|
|
2859
|
+
for (const [key, value] of tree) {
|
|
2656
2860
|
if (!isJsonNode(value)) {
|
|
2657
2861
|
continue;
|
|
2658
2862
|
}
|
|
@@ -2668,7 +2872,7 @@ function assertNoDottedNestedKey(tree) {
|
|
|
2668
2872
|
function assertNotMixed(tree) {
|
|
2669
2873
|
let hasNested = false;
|
|
2670
2874
|
let hasFlatDottedKey = false;
|
|
2671
|
-
for (const [key, value] of
|
|
2875
|
+
for (const [key, value] of tree) {
|
|
2672
2876
|
if (isJsonNode(value)) {
|
|
2673
2877
|
hasNested = true;
|
|
2674
2878
|
} else if (key.includes(".")) {
|
|
@@ -2705,9 +2909,9 @@ async function detectStyle(filePath) {
|
|
|
2705
2909
|
return "flat";
|
|
2706
2910
|
}
|
|
2707
2911
|
function buildFlatTree(entries) {
|
|
2708
|
-
const out = /* @__PURE__ */
|
|
2912
|
+
const out = /* @__PURE__ */ new Map();
|
|
2709
2913
|
for (const [key, entry] of entries) {
|
|
2710
|
-
out
|
|
2914
|
+
out.set(key, entry.value);
|
|
2711
2915
|
}
|
|
2712
2916
|
return out;
|
|
2713
2917
|
}
|
|
@@ -2725,7 +2929,6 @@ function createNgxTranslateJsonAdapter() {
|
|
|
2725
2929
|
}),
|
|
2726
2930
|
validateTree: assertNotMixed,
|
|
2727
2931
|
buildWriteTree: buildNgxWriteTree,
|
|
2728
|
-
// ngx-translate flat style uses dotted keys as path notation, not literal leaves.
|
|
2729
2932
|
keyMode: "path-notation"
|
|
2730
2933
|
});
|
|
2731
2934
|
}
|
|
@@ -2751,6 +2954,11 @@ var AdapterRegistry = class {
|
|
|
2751
2954
|
}
|
|
2752
2955
|
return { status: "resolved", adapter };
|
|
2753
2956
|
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Resolve by asking every registered adapter's `canHandle`. Several adapters can claim one file
|
|
2959
|
+
* (all JSON adapters claim `.json`), so more than one match is reported as `ambiguous` rather
|
|
2960
|
+
* than guessed at.
|
|
2961
|
+
*/
|
|
2754
2962
|
resolveByDetection(filePath, sample) {
|
|
2755
2963
|
const matches = this.adapters.filter((adapter) => adapter.canHandle(filePath, sample));
|
|
2756
2964
|
const first = matches[0];
|
|
@@ -3105,14 +3313,34 @@ function createXliffAdapter() {
|
|
|
3105
3313
|
extractPlaceholders: extractXliffPlaceholders
|
|
3106
3314
|
});
|
|
3107
3315
|
}
|
|
3316
|
+
function normalizeKey(key) {
|
|
3317
|
+
if (typeof key === "object" && key !== null) {
|
|
3318
|
+
throw new AdapterError(
|
|
3319
|
+
"INVALID_STRUCTURE",
|
|
3320
|
+
"A mapping key is a map or sequence (expected scalar keys)."
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
return String(key);
|
|
3324
|
+
}
|
|
3325
|
+
function normalizeYamlTree(value) {
|
|
3326
|
+
if (!(value instanceof Map)) {
|
|
3327
|
+
return value;
|
|
3328
|
+
}
|
|
3329
|
+
const out = /* @__PURE__ */ new Map();
|
|
3330
|
+
for (const [key, child] of value) {
|
|
3331
|
+
out.set(normalizeKey(key), normalizeYamlTree(child));
|
|
3332
|
+
}
|
|
3333
|
+
return out;
|
|
3334
|
+
}
|
|
3108
3335
|
function parseYamlObject(content) {
|
|
3109
3336
|
let parsed;
|
|
3110
3337
|
try {
|
|
3111
|
-
parsed = yaml.parse(content, { maxAliasCount: 100 });
|
|
3338
|
+
parsed = yaml.parse(content, { mapAsMap: true, maxAliasCount: 100 });
|
|
3112
3339
|
} catch {
|
|
3113
3340
|
throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
|
|
3114
3341
|
}
|
|
3115
|
-
|
|
3342
|
+
assertWithinDepth(parsed, MAX_DEPTH);
|
|
3343
|
+
return assertJsonRecord(normalizeYamlTree(parsed));
|
|
3116
3344
|
}
|
|
3117
3345
|
function createYamlAdapter() {
|
|
3118
3346
|
return createTreeFileAdapter({
|
|
@@ -3143,6 +3371,15 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
|
|
|
3143
3371
|
);
|
|
3144
3372
|
}
|
|
3145
3373
|
|
|
3374
|
+
// src/flow/read-target.ts
|
|
3375
|
+
async function readTargetResource(input) {
|
|
3376
|
+
const path = localeFilePath(input.cwd, input.filesPattern, input.locale);
|
|
3377
|
+
if (!await input.fs.fileExists(path)) {
|
|
3378
|
+
return { locale: input.locale, namespace: "", format: input.format, entries: /* @__PURE__ */ new Map() };
|
|
3379
|
+
}
|
|
3380
|
+
return (await input.adapter.read(path, input.locale)).resource;
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3146
3383
|
// src/flow/select-locales.ts
|
|
3147
3384
|
function selectLocales(config, requested) {
|
|
3148
3385
|
if (requested === void 0) {
|
|
@@ -3183,11 +3420,14 @@ async function readSource(config, cwd, fs, adapter) {
|
|
|
3183
3420
|
|
|
3184
3421
|
// src/flow/diff-locales.ts
|
|
3185
3422
|
async function readTarget(cwd, config, adapter, fs, locale) {
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3423
|
+
return readTargetResource({
|
|
3424
|
+
cwd,
|
|
3425
|
+
filesPattern: config.files.pattern,
|
|
3426
|
+
format: config.format,
|
|
3427
|
+
locale,
|
|
3428
|
+
adapter,
|
|
3429
|
+
fs
|
|
3430
|
+
});
|
|
3191
3431
|
}
|
|
3192
3432
|
async function diffLocales(input, deps = {}) {
|
|
3193
3433
|
const config = input.config;
|
|
@@ -3237,6 +3477,166 @@ async function diff(input, deps = {}) {
|
|
|
3237
3477
|
return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
|
|
3238
3478
|
}
|
|
3239
3479
|
|
|
3480
|
+
// src/flow/integrity-gate.ts
|
|
3481
|
+
function gateCandidateValue(sourceEntry, candidateValue, adapter) {
|
|
3482
|
+
const placeholderResult = adapter.comparePlaceholders?.(sourceEntry.value, candidateValue) ?? checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(candidateValue));
|
|
3483
|
+
if (!placeholderResult.matches) {
|
|
3484
|
+
return { accepted: false, reason: "placeholder" };
|
|
3485
|
+
}
|
|
3486
|
+
if (!adapter.validateMessage(candidateValue)) {
|
|
3487
|
+
return { accepted: false, reason: "icu" };
|
|
3488
|
+
}
|
|
3489
|
+
return { accepted: true };
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// src/flow/edit-entry.ts
|
|
3493
|
+
async function editEntry(input, deps = {}) {
|
|
3494
|
+
const config = input.config;
|
|
3495
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3496
|
+
const fs = deps.fs ?? defaultFs;
|
|
3497
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3498
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3499
|
+
if (locale === void 0) {
|
|
3500
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3501
|
+
}
|
|
3502
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3503
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3504
|
+
if (sourceEntry === void 0) {
|
|
3505
|
+
throw new SdkError(
|
|
3506
|
+
"UNKNOWN_KEY",
|
|
3507
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
3511
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3512
|
+
const gate = gateCandidateValue(sourceEntry, input.value, adapter);
|
|
3513
|
+
if (!gate.accepted) {
|
|
3514
|
+
return { accepted: false, reason: gate.reason, value: input.value };
|
|
3515
|
+
}
|
|
3516
|
+
const merged = new Map(target.entries);
|
|
3517
|
+
merged.set(input.key, { ...sourceEntry, value: input.value, namespace: target.namespace });
|
|
3518
|
+
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
3519
|
+
await adapter.write(
|
|
3520
|
+
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
3521
|
+
path
|
|
3522
|
+
);
|
|
3523
|
+
await updateLockFileLocale(cwd, fs, locale, {
|
|
3524
|
+
mode: "merge",
|
|
3525
|
+
entries: { [input.key]: contentHash(sourceEntry) }
|
|
3526
|
+
});
|
|
3527
|
+
return { accepted: true, value: input.value };
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
// src/flow/key-integrity.ts
|
|
3532
|
+
function checkEntryIntegrity(adapter, sourceEntry, targetEntry) {
|
|
3533
|
+
const result = adapter.comparePlaceholders?.(sourceEntry.value, targetEntry.value) ?? checkPlaceholders(sourceEntry.placeholders, targetEntry.placeholders);
|
|
3534
|
+
return {
|
|
3535
|
+
key: sourceEntry.key,
|
|
3536
|
+
hasPlaceholders: sourceEntry.placeholders.length > 0,
|
|
3537
|
+
matches: result.matches,
|
|
3538
|
+
missing: result.missing,
|
|
3539
|
+
extra: result.extra,
|
|
3540
|
+
icuValid: adapter.validateMessage(targetEntry.value)
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
function selectChangedKeys(changed, requested) {
|
|
3544
|
+
if (requested === void 0) {
|
|
3545
|
+
return changed;
|
|
3546
|
+
}
|
|
3547
|
+
const wanted = new Set(requested);
|
|
3548
|
+
return changed.filter((key) => wanted.has(key));
|
|
3549
|
+
}
|
|
3550
|
+
function integrityEntriesFor(source, target, adapter, changedKeys) {
|
|
3551
|
+
const entries = [];
|
|
3552
|
+
for (const key of changedKeys) {
|
|
3553
|
+
const sourceEntry = source.entries.get(key);
|
|
3554
|
+
const targetEntry = target.entries.get(key);
|
|
3555
|
+
if (sourceEntry === void 0 || targetEntry === void 0) {
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
entries.push(checkEntryIntegrity(adapter, sourceEntry, targetEntry));
|
|
3559
|
+
}
|
|
3560
|
+
return entries;
|
|
3561
|
+
}
|
|
3562
|
+
async function keyIntegrity(input, deps = {}) {
|
|
3563
|
+
const config = input.config;
|
|
3564
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3565
|
+
const fs = deps.fs ?? defaultFs;
|
|
3566
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3567
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3568
|
+
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
3569
|
+
return Promise.all(
|
|
3570
|
+
selectLocales(config, input.locales).map(async (locale) => {
|
|
3571
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3572
|
+
const diffResult = diffResources(source.resource, target, {
|
|
3573
|
+
baseline: baselineFor(lock, locale)
|
|
3574
|
+
});
|
|
3575
|
+
const changedKeys = selectChangedKeys(diffResult.changed, input.keys);
|
|
3576
|
+
const entries = integrityEntriesFor(source.resource, target, adapter, changedKeys);
|
|
3577
|
+
return { locale, entries };
|
|
3578
|
+
})
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// src/flow/key-value.ts
|
|
3583
|
+
async function keyValue(input, deps = {}) {
|
|
3584
|
+
const config = input.config;
|
|
3585
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3586
|
+
const fs = deps.fs ?? defaultFs;
|
|
3587
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3588
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3589
|
+
if (locale === void 0) {
|
|
3590
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3591
|
+
}
|
|
3592
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3593
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3594
|
+
if (sourceEntry === void 0) {
|
|
3595
|
+
throw new SdkError(
|
|
3596
|
+
"UNKNOWN_KEY",
|
|
3597
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3601
|
+
const targetEntry = target.entries.get(input.key);
|
|
3602
|
+
return {
|
|
3603
|
+
source: sourceEntry.value,
|
|
3604
|
+
...targetEntry !== void 0 ? { target: targetEntry.value } : {}
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// src/flow/locale-snapshot.ts
|
|
3609
|
+
async function readLocaleFileSnapshot(input, deps = {}) {
|
|
3610
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3611
|
+
const fs = deps.fs ?? defaultFs;
|
|
3612
|
+
const adapter = selectAdapter(input.config.format, deps.adapterRegistry);
|
|
3613
|
+
const resource = await readTarget(cwd, input.config, adapter, fs, input.locale);
|
|
3614
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
3615
|
+
for (const [key, entry] of resource.entries) {
|
|
3616
|
+
hashes.set(key, contentHash(entry));
|
|
3617
|
+
}
|
|
3618
|
+
return { locale: input.locale, hashes };
|
|
3619
|
+
}
|
|
3620
|
+
function diffLocaleSnapshots(previous, current) {
|
|
3621
|
+
let added = 0;
|
|
3622
|
+
let changed = 0;
|
|
3623
|
+
for (const [key, hash] of current.hashes) {
|
|
3624
|
+
const previousHash = previous.hashes.get(key);
|
|
3625
|
+
if (previousHash === void 0) {
|
|
3626
|
+
added += 1;
|
|
3627
|
+
} else if (previousHash !== hash) {
|
|
3628
|
+
changed += 1;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
let removed = 0;
|
|
3632
|
+
for (const key of previous.hashes.keys()) {
|
|
3633
|
+
if (!current.hashes.has(key)) {
|
|
3634
|
+
removed += 1;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
return { added, changed, removed };
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3240
3640
|
// src/flow/lock-state.ts
|
|
3241
3641
|
function toLockLocaleState(locale, keyCount, diff2) {
|
|
3242
3642
|
return {
|
|
@@ -3284,6 +3684,174 @@ function selectProvider(config, createProvider = buildProvider) {
|
|
|
3284
3684
|
}
|
|
3285
3685
|
}
|
|
3286
3686
|
|
|
3687
|
+
// src/flow/retranslate-entry.ts
|
|
3688
|
+
function buildSingleEntryRequest(config, locale, sourceEntry, adapter) {
|
|
3689
|
+
return {
|
|
3690
|
+
sourceLocale: config.sourceLocale,
|
|
3691
|
+
targetLocale: locale,
|
|
3692
|
+
entries: [sourceEntry],
|
|
3693
|
+
extractPlaceholders: adapter.extractPlaceholders,
|
|
3694
|
+
...config.glossary !== void 0 ? { glossary: config.glossary } : {},
|
|
3695
|
+
...config.tone !== void 0 ? { tone: config.tone } : {},
|
|
3696
|
+
...adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: adapter.comparePlaceholders } : {}
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
async function retranslateEntry(input, deps = {}) {
|
|
3700
|
+
const config = input.config;
|
|
3701
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3702
|
+
const fs = deps.fs ?? defaultFs;
|
|
3703
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3704
|
+
const [locale] = selectLocales(config, [input.locale]);
|
|
3705
|
+
if (locale === void 0) {
|
|
3706
|
+
throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
|
|
3707
|
+
}
|
|
3708
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
3709
|
+
const sourceEntry = source.resource.entries.get(input.key);
|
|
3710
|
+
if (sourceEntry === void 0) {
|
|
3711
|
+
throw new SdkError(
|
|
3712
|
+
"UNKNOWN_KEY",
|
|
3713
|
+
`The key "${input.key}" was not found in the source resource.`
|
|
3714
|
+
);
|
|
3715
|
+
}
|
|
3716
|
+
const provider = selectProvider(config.provider, deps.createProvider);
|
|
3717
|
+
return withLocaleWriteLock(cwd, locale, fs, async () => {
|
|
3718
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
3719
|
+
const result = await provider.translateBatch(
|
|
3720
|
+
buildSingleEntryRequest(config, locale, sourceEntry, adapter)
|
|
3721
|
+
);
|
|
3722
|
+
const value = result.values.get(input.key);
|
|
3723
|
+
if (value === void 0) {
|
|
3724
|
+
throw new ProviderError(
|
|
3725
|
+
"INVALID_RESPONSE",
|
|
3726
|
+
`The provider returned no translated value for key "${input.key}".`
|
|
3727
|
+
);
|
|
3728
|
+
}
|
|
3729
|
+
const gate = gateCandidateValue(sourceEntry, value, adapter);
|
|
3730
|
+
if (!gate.accepted) {
|
|
3731
|
+
return { accepted: false, reason: gate.reason, value };
|
|
3732
|
+
}
|
|
3733
|
+
const merged = new Map(target.entries);
|
|
3734
|
+
merged.set(input.key, { ...sourceEntry, value, namespace: target.namespace });
|
|
3735
|
+
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
3736
|
+
await adapter.write(
|
|
3737
|
+
{ locale, namespace: target.namespace, format: config.format, entries: merged },
|
|
3738
|
+
path
|
|
3739
|
+
);
|
|
3740
|
+
await updateLockFileLocale(cwd, fs, locale, {
|
|
3741
|
+
mode: "merge",
|
|
3742
|
+
entries: { [input.key]: contentHash(sourceEntry) }
|
|
3743
|
+
});
|
|
3744
|
+
const reviewReasons = result.reviewFlags?.get(input.key)?.reasons ?? [];
|
|
3745
|
+
return { accepted: true, value, reviewReasons };
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
var RUN_STATUS_DIR_NAME = ".verbatra-local";
|
|
3749
|
+
var RUN_STATUS_FILE_NAME = "run-status.json";
|
|
3750
|
+
var CURRENT_VERSION2 = 1;
|
|
3751
|
+
var MAX_RUN_STATUS_FILE_BYTES = 16 * 1024 * 1024;
|
|
3752
|
+
var reviewReasonCodeSchema = zod.z.enum([
|
|
3753
|
+
"LENGTH_RATIO_OUTLIER",
|
|
3754
|
+
"EQUALS_SOURCE",
|
|
3755
|
+
"GLOSSARY_TERM_MISSED",
|
|
3756
|
+
"INTEGRITY_REORDERED",
|
|
3757
|
+
"PROVIDER_DEGRADED"
|
|
3758
|
+
]);
|
|
3759
|
+
var needsReviewEntrySchema = zod.z.object({
|
|
3760
|
+
key: zod.z.string(),
|
|
3761
|
+
reasons: zod.z.array(reviewReasonCodeSchema)
|
|
3762
|
+
});
|
|
3763
|
+
var usageSummarySchema = zod.z.object({
|
|
3764
|
+
inputTokens: zod.z.number().int().nonnegative(),
|
|
3765
|
+
outputTokens: zod.z.number().int().nonnegative()
|
|
3766
|
+
});
|
|
3767
|
+
var runBudgetSchema = zod.z.object({
|
|
3768
|
+
maxTokens: zod.z.number().int().nonnegative(),
|
|
3769
|
+
behavior: zod.z.enum(["warn", "stop"]),
|
|
3770
|
+
supported: zod.z.boolean(),
|
|
3771
|
+
tokensUsed: zod.z.number().int().nonnegative(),
|
|
3772
|
+
exceeded: zod.z.boolean()
|
|
3773
|
+
});
|
|
3774
|
+
var runStatusLocaleSchema = zod.z.object({
|
|
3775
|
+
locale: zod.z.string(),
|
|
3776
|
+
status: zod.z.enum(["succeeded", "failed"]),
|
|
3777
|
+
needsReview: zod.z.array(needsReviewEntrySchema),
|
|
3778
|
+
usage: usageSummarySchema.optional()
|
|
3779
|
+
});
|
|
3780
|
+
var runStatusFileSchema = zod.z.object({
|
|
3781
|
+
version: zod.z.number().int().positive(),
|
|
3782
|
+
generatedAt: zod.z.string(),
|
|
3783
|
+
usage: usageSummarySchema.optional(),
|
|
3784
|
+
budget: runBudgetSchema.optional(),
|
|
3785
|
+
locales: zod.z.array(runStatusLocaleSchema)
|
|
3786
|
+
});
|
|
3787
|
+
function runStatusFilePath(cwd) {
|
|
3788
|
+
return path.resolve(cwd, RUN_STATUS_DIR_NAME, RUN_STATUS_FILE_NAME);
|
|
3789
|
+
}
|
|
3790
|
+
function toRunStatusLocale(locale) {
|
|
3791
|
+
return {
|
|
3792
|
+
locale: locale.locale,
|
|
3793
|
+
status: locale.status,
|
|
3794
|
+
needsReview: locale.needsReview,
|
|
3795
|
+
...locale.usage !== void 0 ? { usage: locale.usage } : {}
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
function buildRunStatusFile(summary, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3799
|
+
return {
|
|
3800
|
+
version: CURRENT_VERSION2,
|
|
3801
|
+
generatedAt,
|
|
3802
|
+
...summary.usage !== void 0 ? { usage: summary.usage } : {},
|
|
3803
|
+
...summary.budget !== void 0 ? { budget: summary.budget } : {},
|
|
3804
|
+
locales: summary.locales.map(toRunStatusLocale)
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
function fromParsed(data) {
|
|
3808
|
+
return {
|
|
3809
|
+
version: data.version,
|
|
3810
|
+
generatedAt: data.generatedAt,
|
|
3811
|
+
...data.usage !== void 0 ? { usage: data.usage } : {},
|
|
3812
|
+
...data.budget !== void 0 ? { budget: data.budget } : {},
|
|
3813
|
+
locales: data.locales.map((locale) => ({
|
|
3814
|
+
locale: locale.locale,
|
|
3815
|
+
status: locale.status,
|
|
3816
|
+
needsReview: locale.needsReview,
|
|
3817
|
+
...locale.usage !== void 0 ? { usage: locale.usage } : {}
|
|
3818
|
+
}))
|
|
3819
|
+
};
|
|
3820
|
+
}
|
|
3821
|
+
async function readRunStatusFile(path, fs) {
|
|
3822
|
+
const read = await fs.readFileBounded(path, MAX_RUN_STATUS_FILE_BYTES);
|
|
3823
|
+
if (read.kind !== "ok") {
|
|
3824
|
+
return void 0;
|
|
3825
|
+
}
|
|
3826
|
+
let parsed;
|
|
3827
|
+
try {
|
|
3828
|
+
parsed = JSON.parse(read.content);
|
|
3829
|
+
} catch {
|
|
3830
|
+
return void 0;
|
|
3831
|
+
}
|
|
3832
|
+
const result = runStatusFileSchema.safeParse(parsed);
|
|
3833
|
+
if (!result.success || result.data.version !== CURRENT_VERSION2) {
|
|
3834
|
+
return void 0;
|
|
3835
|
+
}
|
|
3836
|
+
return fromParsed(result.data);
|
|
3837
|
+
}
|
|
3838
|
+
async function writeRunStatusFile(path$1, data, fs) {
|
|
3839
|
+
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
3840
|
+
await fs.writeFile(path$1, `${JSON.stringify(data, null, 2)}
|
|
3841
|
+
`);
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
// src/flow/run-status.ts
|
|
3845
|
+
async function runStatus(input = {}, deps = {}) {
|
|
3846
|
+
const cwd = input.cwd ?? process.cwd();
|
|
3847
|
+
const fs = deps.fs ?? defaultFs;
|
|
3848
|
+
const file = await readRunStatusFile(runStatusFilePath(cwd), fs);
|
|
3849
|
+
if (file === void 0) {
|
|
3850
|
+
return { available: false };
|
|
3851
|
+
}
|
|
3852
|
+
return { available: true, ...file };
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3287
3855
|
// src/flow/budget.ts
|
|
3288
3856
|
function createBudgetTracker(maxTokens, behavior) {
|
|
3289
3857
|
return { maxTokens, behavior, tokensUsed: 0, usageSeen: false, exceeded: false, stopped: false };
|
|
@@ -3566,7 +4134,6 @@ function syntheticEntry(item) {
|
|
|
3566
4134
|
...item.sourceEntry,
|
|
3567
4135
|
key: item.targetKey,
|
|
3568
4136
|
isPlural: true,
|
|
3569
|
-
// The CLDR category travels as data context (the meaning field), never the instruction channel.
|
|
3570
4137
|
meaning: `CLDR plural category "${item.category}"`
|
|
3571
4138
|
};
|
|
3572
4139
|
}
|
|
@@ -3643,17 +4210,17 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
|
|
|
3643
4210
|
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3644
4211
|
}
|
|
3645
4212
|
for (const item of batch) {
|
|
3646
|
-
foldGenerationItem(item, result, accepted, withheld, providerFailures);
|
|
4213
|
+
foldGenerationItem(item, result, context.adapter, accepted, withheld, providerFailures);
|
|
3647
4214
|
}
|
|
3648
4215
|
return { notices: readNotices(result), usage: result.usage };
|
|
3649
4216
|
}
|
|
3650
|
-
function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
|
|
4217
|
+
function foldGenerationItem(item, result, adapter, accepted, withheld, providerFailures) {
|
|
3651
4218
|
const value = result.values.get(item.targetKey);
|
|
3652
4219
|
if (value === void 0) {
|
|
3653
4220
|
providerFailures.push(item.targetKey);
|
|
3654
4221
|
return;
|
|
3655
4222
|
}
|
|
3656
|
-
if (
|
|
4223
|
+
if (gateCandidateValue(item.sourceEntry, value, adapter).accepted) {
|
|
3657
4224
|
accepted.push({
|
|
3658
4225
|
targetKey: item.targetKey,
|
|
3659
4226
|
entry: { ...syntheticEntry(item), value },
|
|
@@ -3665,16 +4232,6 @@ function foldGenerationItem(item, result, accepted, withheld, providerFailures)
|
|
|
3665
4232
|
}
|
|
3666
4233
|
|
|
3667
4234
|
// src/flow/locale-run.ts
|
|
3668
|
-
function emptyResource(locale, format) {
|
|
3669
|
-
return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
|
|
3670
|
-
}
|
|
3671
|
-
async function readTarget2(params) {
|
|
3672
|
-
const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
|
|
3673
|
-
if (!await params.fs.fileExists(path)) {
|
|
3674
|
-
return emptyResource(params.targetLocale, params.format);
|
|
3675
|
-
}
|
|
3676
|
-
return (await params.adapter.read(path, params.targetLocale)).resource;
|
|
3677
|
-
}
|
|
3678
4235
|
function buildRequest3(params, entries) {
|
|
3679
4236
|
return {
|
|
3680
4237
|
sourceLocale: params.sourceLocale,
|
|
@@ -3687,7 +4244,14 @@ function buildRequest3(params, entries) {
|
|
|
3687
4244
|
};
|
|
3688
4245
|
}
|
|
3689
4246
|
async function runLocale(params) {
|
|
3690
|
-
const target = await
|
|
4247
|
+
const target = await readTargetResource({
|
|
4248
|
+
cwd: params.cwd,
|
|
4249
|
+
filesPattern: params.filesPattern,
|
|
4250
|
+
format: params.format,
|
|
4251
|
+
locale: params.targetLocale,
|
|
4252
|
+
adapter: params.adapter,
|
|
4253
|
+
fs: params.fs
|
|
4254
|
+
});
|
|
3691
4255
|
const diff2 = diffResources(params.source, target, { baseline: params.baseline });
|
|
3692
4256
|
const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
|
|
3693
4257
|
const pruned = params.prune ? orphaned : [];
|
|
@@ -3741,8 +4305,11 @@ async function runLocale(params) {
|
|
|
3741
4305
|
for (const key of pruned) {
|
|
3742
4306
|
merged.delete(key);
|
|
3743
4307
|
}
|
|
3744
|
-
for (const
|
|
3745
|
-
|
|
4308
|
+
for (const key of params.source.entries.keys()) {
|
|
4309
|
+
const hit = accepted.get(key);
|
|
4310
|
+
if (hit !== void 0) {
|
|
4311
|
+
merged.set(key, { ...hit.source, value: hit.value, namespace: target.namespace });
|
|
4312
|
+
}
|
|
3746
4313
|
}
|
|
3747
4314
|
const generation = await runGeneration(params, provider);
|
|
3748
4315
|
for (const form of generation.accepted) {
|
|
@@ -3771,7 +4338,6 @@ async function runLocale(params) {
|
|
|
3771
4338
|
...invalidIcuSource,
|
|
3772
4339
|
...generation.withheld,
|
|
3773
4340
|
...generation.providerFailures,
|
|
3774
|
-
// Source-present keys withheld by the budget guardrail must keep their prior lock hash too.
|
|
3775
4341
|
...budgetWithheld
|
|
3776
4342
|
]);
|
|
3777
4343
|
const localeUsage = combineUsage(translation.usage, generation.usage);
|
|
@@ -3783,9 +4349,7 @@ async function runLocale(params) {
|
|
|
3783
4349
|
invalidIcuSource,
|
|
3784
4350
|
translated: [...accepted.keys()],
|
|
3785
4351
|
generated: generation.accepted.map((form) => form.targetKey).sort(),
|
|
3786
|
-
// Withheld generated forms surface alongside withheld translations: both failed integrity.
|
|
3787
4352
|
integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
|
|
3788
|
-
// A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
|
|
3789
4353
|
providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
|
|
3790
4354
|
budgetWithheld: [...budgetWithheld, ...generation.budgetWithheld].sort(),
|
|
3791
4355
|
pruned,
|
|
@@ -3903,7 +4467,7 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3903
4467
|
return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
|
|
3904
4468
|
}
|
|
3905
4469
|
for (const entry of batch) {
|
|
3906
|
-
foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
|
|
4470
|
+
foldEntryResult(entry, result, params.adapter, accepted, integrityMismatches, providerFailures);
|
|
3907
4471
|
}
|
|
3908
4472
|
if (result.reviewFlags !== void 0) {
|
|
3909
4473
|
for (const [key, flag] of result.reviewFlags) {
|
|
@@ -3912,13 +4476,13 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
|
|
|
3912
4476
|
}
|
|
3913
4477
|
return { notices: readNotices(result), usage: result.usage };
|
|
3914
4478
|
}
|
|
3915
|
-
function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
|
|
4479
|
+
function foldEntryResult(entry, result, adapter, accepted, integrityMismatches, providerFailures) {
|
|
3916
4480
|
const value = result.values.get(entry.key);
|
|
3917
4481
|
if (value === void 0) {
|
|
3918
4482
|
providerFailures.push(entry.key);
|
|
3919
4483
|
return;
|
|
3920
4484
|
}
|
|
3921
|
-
if (
|
|
4485
|
+
if (gateCandidateValue(entry, value, adapter).accepted) {
|
|
3922
4486
|
accepted.set(entry.key, { value, source: entry });
|
|
3923
4487
|
} else {
|
|
3924
4488
|
integrityMismatches.push(entry.key);
|
|
@@ -3960,6 +4524,79 @@ function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
|
|
|
3960
4524
|
}
|
|
3961
4525
|
|
|
3962
4526
|
// src/flow/translate-project.ts
|
|
4527
|
+
async function recordRunStatus(cwd, dryRun, summary, fs) {
|
|
4528
|
+
if (dryRun) {
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
try {
|
|
4532
|
+
await writeRunStatusFile(runStatusFilePath(cwd), buildRunStatusFile(summary), fs);
|
|
4533
|
+
} catch {
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
function buildLocaleRunParams(context, targetLocale, baseline) {
|
|
4537
|
+
return {
|
|
4538
|
+
source: context.source.resource,
|
|
4539
|
+
sourceInvalidIcuKeys: context.source.invalidIcuKeys,
|
|
4540
|
+
baseline,
|
|
4541
|
+
adapter: context.adapter,
|
|
4542
|
+
provider: context.provider,
|
|
4543
|
+
cwd: context.cwd,
|
|
4544
|
+
filesPattern: context.config.files.pattern,
|
|
4545
|
+
sourceLocale: context.config.sourceLocale,
|
|
4546
|
+
targetLocale,
|
|
4547
|
+
format: context.config.format,
|
|
4548
|
+
glossary: context.config.glossary,
|
|
4549
|
+
tone: context.config.tone,
|
|
4550
|
+
prune: context.prune,
|
|
4551
|
+
generatePlurals: context.generatePlurals,
|
|
4552
|
+
maxBatchSize: context.maxBatchSize,
|
|
4553
|
+
fs: context.fs,
|
|
4554
|
+
budget: context.budget
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
async function runDryLocale(context, targetLocale, lock) {
|
|
4558
|
+
const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
|
|
4559
|
+
return (await runLocale(params)).summary;
|
|
4560
|
+
}
|
|
4561
|
+
async function runLiveLocale(context, targetLocale) {
|
|
4562
|
+
return withLocaleWriteLock(context.cwd, targetLocale, context.fs, async () => {
|
|
4563
|
+
const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
|
|
4564
|
+
const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
|
|
4565
|
+
const result = await runLocale(params);
|
|
4566
|
+
await updateLockFileLocale(context.cwd, context.fs, targetLocale, {
|
|
4567
|
+
mode: "replace",
|
|
4568
|
+
entries: result.lockEntries
|
|
4569
|
+
});
|
|
4570
|
+
return result.summary;
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
async function runOneLocale(targetLocale, run) {
|
|
4574
|
+
try {
|
|
4575
|
+
return await run();
|
|
4576
|
+
} catch (error) {
|
|
4577
|
+
if (error instanceof SdkError && error.code === "LOCK_FILE_INVALID") {
|
|
4578
|
+
throw error;
|
|
4579
|
+
}
|
|
4580
|
+
return failureSummary(targetLocale, error);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
async function runAllLocalesDry(context, targetLocales) {
|
|
4584
|
+
const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
|
|
4585
|
+
const summaries = [];
|
|
4586
|
+
for (const targetLocale of targetLocales) {
|
|
4587
|
+
summaries.push(
|
|
4588
|
+
await runOneLocale(targetLocale, () => runDryLocale(context, targetLocale, lock))
|
|
4589
|
+
);
|
|
4590
|
+
}
|
|
4591
|
+
return summaries;
|
|
4592
|
+
}
|
|
4593
|
+
async function runAllLocalesLive(context, targetLocales) {
|
|
4594
|
+
const summaries = [];
|
|
4595
|
+
for (const targetLocale of targetLocales) {
|
|
4596
|
+
summaries.push(await runOneLocale(targetLocale, () => runLiveLocale(context, targetLocale)));
|
|
4597
|
+
}
|
|
4598
|
+
return summaries;
|
|
4599
|
+
}
|
|
3963
4600
|
async function translate2(input, deps = {}) {
|
|
3964
4601
|
const config = input.config;
|
|
3965
4602
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -3975,47 +4612,26 @@ async function translate2(input, deps = {}) {
|
|
|
3975
4612
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
3976
4613
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
3977
4614
|
const source = await readSource(config, cwd, fs, adapter);
|
|
3978
|
-
const
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
sourceLocale: config.sourceLocale,
|
|
3992
|
-
targetLocale,
|
|
3993
|
-
format: config.format,
|
|
3994
|
-
glossary: config.glossary,
|
|
3995
|
-
tone: config.tone,
|
|
3996
|
-
prune,
|
|
3997
|
-
generatePlurals,
|
|
3998
|
-
maxBatchSize,
|
|
3999
|
-
fs,
|
|
4000
|
-
budget
|
|
4001
|
-
};
|
|
4002
|
-
const { summary, lockEntries } = await runLocale(params);
|
|
4003
|
-
if (!dryRun) {
|
|
4004
|
-
lock = updateLockLocale(lock, targetLocale, lockEntries);
|
|
4005
|
-
await writeLockFile(lockPath, lock, fs);
|
|
4006
|
-
}
|
|
4007
|
-
summaries.push(summary);
|
|
4008
|
-
} catch (error) {
|
|
4009
|
-
summaries.push(failureSummary(targetLocale, error));
|
|
4010
|
-
}
|
|
4011
|
-
}
|
|
4615
|
+
const context = {
|
|
4616
|
+
source,
|
|
4617
|
+
adapter,
|
|
4618
|
+
provider,
|
|
4619
|
+
cwd,
|
|
4620
|
+
config,
|
|
4621
|
+
prune,
|
|
4622
|
+
generatePlurals,
|
|
4623
|
+
maxBatchSize,
|
|
4624
|
+
fs,
|
|
4625
|
+
budget
|
|
4626
|
+
};
|
|
4627
|
+
const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales) : await runAllLocalesLive(context, config.targetLocales);
|
|
4012
4628
|
const { succeeded, failed } = partition(summaries);
|
|
4013
4629
|
const usage = summaries.reduce(
|
|
4014
|
-
(total,
|
|
4630
|
+
(total, summary2) => combineUsage(total, summary2.usage),
|
|
4015
4631
|
void 0
|
|
4016
4632
|
);
|
|
4017
4633
|
const budgetSummary = toBudgetSummary(budget);
|
|
4018
|
-
|
|
4634
|
+
const summary = {
|
|
4019
4635
|
dryRun,
|
|
4020
4636
|
locales: summaries,
|
|
4021
4637
|
succeeded,
|
|
@@ -4023,6 +4639,8 @@ async function translate2(input, deps = {}) {
|
|
|
4023
4639
|
...usage !== void 0 ? { usage } : {},
|
|
4024
4640
|
...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
|
|
4025
4641
|
};
|
|
4642
|
+
await recordRunStatus(cwd, dryRun, summary, fs);
|
|
4643
|
+
return summary;
|
|
4026
4644
|
}
|
|
4027
4645
|
var ExchangeError = class extends Error {
|
|
4028
4646
|
code;
|
|
@@ -4308,8 +4926,6 @@ var rowSchema = zod.z.object({
|
|
|
4308
4926
|
sourceHash: zod.z.string(),
|
|
4309
4927
|
translation: zod.z.string(),
|
|
4310
4928
|
context: zod.z.string(),
|
|
4311
|
-
// A legacy workbook exported before these columns existed has no such cell; cellString then yields
|
|
4312
|
-
// "", and .catch maps it to "ok" / "" rather than rejecting the row.
|
|
4313
4929
|
reviewStatus: zod.z.enum(["ok", "review"]).catch("ok"),
|
|
4314
4930
|
reviewReasons: zod.z.string().catch("")
|
|
4315
4931
|
});
|
|
@@ -4345,10 +4961,7 @@ function parseRow(sheet, row) {
|
|
|
4345
4961
|
status: cellString(row.getCell(COLUMN.status)),
|
|
4346
4962
|
sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
|
|
4347
4963
|
translation: cellString(row.getCell(COLUMN.translation)),
|
|
4348
|
-
// A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
|
|
4349
|
-
// empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
|
|
4350
4964
|
context: cellString(row.getCell(COLUMN.context)),
|
|
4351
|
-
// Same backward-compatible pattern for a workbook built before the Review columns existed.
|
|
4352
4965
|
reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
|
|
4353
4966
|
reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
|
|
4354
4967
|
};
|
|
@@ -4417,13 +5030,6 @@ async function readWorkbook(bytes, options = {}) {
|
|
|
4417
5030
|
|
|
4418
5031
|
// src/flow/workbook/export-workbook.ts
|
|
4419
5032
|
var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
|
|
4420
|
-
async function readTarget3(cwd, config, adapter, fs, locale) {
|
|
4421
|
-
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
4422
|
-
if (!await fs.fileExists(path)) {
|
|
4423
|
-
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
4424
|
-
}
|
|
4425
|
-
return (await adapter.read(path, locale)).resource;
|
|
4426
|
-
}
|
|
4427
5033
|
function reasonLabel(reason) {
|
|
4428
5034
|
return reason.toLowerCase().replace(/_/g, "-");
|
|
4429
5035
|
}
|
|
@@ -4497,7 +5103,7 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
4497
5103
|
const locales = selectLocales(config, input.locales);
|
|
4498
5104
|
const sheets = await Promise.all(
|
|
4499
5105
|
locales.map(async (locale) => {
|
|
4500
|
-
const target = await
|
|
5106
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
4501
5107
|
const rows = buildRows(
|
|
4502
5108
|
source.resource,
|
|
4503
5109
|
target,
|
|
@@ -4535,14 +5141,8 @@ function judge(row, sourceEntry, adapter) {
|
|
|
4535
5141
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
4536
5142
|
return "drift";
|
|
4537
5143
|
}
|
|
4538
|
-
const
|
|
4539
|
-
|
|
4540
|
-
return "placeholder";
|
|
4541
|
-
}
|
|
4542
|
-
if (!adapter.validateMessage(row.translation)) {
|
|
4543
|
-
return "icu";
|
|
4544
|
-
}
|
|
4545
|
-
return void 0;
|
|
5144
|
+
const gate = gateCandidateValue(sourceEntry, row.translation, adapter);
|
|
5145
|
+
return gate.accepted ? void 0 : gate.reason;
|
|
4546
5146
|
}
|
|
4547
5147
|
function trackBlankDrift(row, params, buckets) {
|
|
4548
5148
|
const sourceEntry = params.source.entries.get(row.key);
|
|
@@ -4599,19 +5199,13 @@ function importLocale(params) {
|
|
|
4599
5199
|
translated: [...buckets.accepted.keys()].sort(),
|
|
4600
5200
|
unchanged: diff2.unchanged,
|
|
4601
5201
|
orphaned: diff2.orphaned,
|
|
4602
|
-
// Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
|
|
4603
5202
|
pruned: [],
|
|
4604
5203
|
invalidIcuSource,
|
|
4605
5204
|
integrityMismatches: [...buckets.mismatches].sort(),
|
|
4606
|
-
// A workbook import never calls a provider, so a provider-call failure cannot occur here.
|
|
4607
5205
|
providerFailures: [],
|
|
4608
|
-
// A workbook import never calls a provider, so the budget guardrail never withholds anything here.
|
|
4609
5206
|
budgetWithheld: [],
|
|
4610
|
-
// Plural generation is a translate-flow concern; the manual workbook import never generates forms.
|
|
4611
5207
|
generated: [],
|
|
4612
5208
|
notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
|
|
4613
|
-
// A workbook import never calls a provider and never recomputes review flags on its own path; the
|
|
4614
|
-
// Excel workbook's Review status/reasons columns are informational only (see export-workbook.ts).
|
|
4615
5209
|
needsReview: []
|
|
4616
5210
|
};
|
|
4617
5211
|
return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
|
|
@@ -4632,13 +5226,6 @@ async function readWorkbookBytes(path, fs) {
|
|
|
4632
5226
|
}
|
|
4633
5227
|
return read.bytes;
|
|
4634
5228
|
}
|
|
4635
|
-
async function readTarget4(cwd, config, adapter, fs, locale) {
|
|
4636
|
-
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
4637
|
-
if (!await fs.fileExists(path)) {
|
|
4638
|
-
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
4639
|
-
}
|
|
4640
|
-
return (await adapter.read(path, locale)).resource;
|
|
4641
|
-
}
|
|
4642
5229
|
function mergeAccepted(target, accepted) {
|
|
4643
5230
|
const merged = new Map(target.entries);
|
|
4644
5231
|
for (const [key, { value, source }] of accepted) {
|
|
@@ -4669,7 +5256,7 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
4669
5256
|
`The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
|
|
4670
5257
|
);
|
|
4671
5258
|
}
|
|
4672
|
-
const target = await
|
|
5259
|
+
const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
|
|
4673
5260
|
const baseline = baselineFor(lock, sheet.locale);
|
|
4674
5261
|
const { summary, accepted } = importLocale({
|
|
4675
5262
|
sheet,
|
|
@@ -4712,8 +5299,7 @@ async function importWorkbook(input, deps = {}) {
|
|
|
4712
5299
|
} catch (error) {
|
|
4713
5300
|
throw new SdkError("SOURCE_INVALID", error.message);
|
|
4714
5301
|
}
|
|
4715
|
-
const
|
|
4716
|
-
let lock = await readLockFile(lockPath, fs);
|
|
5302
|
+
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
4717
5303
|
const ctx = {
|
|
4718
5304
|
config,
|
|
4719
5305
|
cwd,
|
|
@@ -4726,10 +5312,18 @@ async function importWorkbook(input, deps = {}) {
|
|
|
4726
5312
|
const summaries = [];
|
|
4727
5313
|
for (const sheet of data.sheets) {
|
|
4728
5314
|
try {
|
|
4729
|
-
|
|
4730
|
-
if (
|
|
4731
|
-
|
|
4732
|
-
|
|
5315
|
+
let summary;
|
|
5316
|
+
if (dryRun) {
|
|
5317
|
+
summary = (await runSheet(ctx, sheet, lock)).summary;
|
|
5318
|
+
} else {
|
|
5319
|
+
summary = await withLocaleWriteLock(cwd, sheet.locale, fs, async () => {
|
|
5320
|
+
const result = await runSheet(ctx, sheet, lock);
|
|
5321
|
+
await updateLockFileLocale(cwd, fs, sheet.locale, {
|
|
5322
|
+
mode: "replace",
|
|
5323
|
+
entries: result.lockEntries
|
|
5324
|
+
});
|
|
5325
|
+
return result.summary;
|
|
5326
|
+
});
|
|
4733
5327
|
}
|
|
4734
5328
|
summaries.push(summary);
|
|
4735
5329
|
} catch (error) {
|
|
@@ -4868,12 +5462,19 @@ exports.SdkError = SdkError;
|
|
|
4868
5462
|
exports.check = check;
|
|
4869
5463
|
exports.defineConfig = defineConfig;
|
|
4870
5464
|
exports.diff = diff;
|
|
5465
|
+
exports.diffLocaleSnapshots = diffLocaleSnapshots;
|
|
5466
|
+
exports.editEntry = editEntry;
|
|
4871
5467
|
exports.exportWorkbook = exportWorkbook;
|
|
4872
5468
|
exports.importWorkbook = importWorkbook;
|
|
5469
|
+
exports.keyIntegrity = keyIntegrity;
|
|
5470
|
+
exports.keyValue = keyValue;
|
|
4873
5471
|
exports.loadConfig = loadConfig;
|
|
4874
5472
|
exports.loadConfigWithMeta = loadConfigWithMeta;
|
|
4875
5473
|
exports.loadLockFile = loadLockFile;
|
|
4876
5474
|
exports.lockState = lockState;
|
|
5475
|
+
exports.readLocaleFileSnapshot = readLocaleFileSnapshot;
|
|
5476
|
+
exports.retranslateEntry = retranslateEntry;
|
|
5477
|
+
exports.runStatus = runStatus;
|
|
4877
5478
|
exports.scaffoldingMetadata = scaffoldingMetadata;
|
|
4878
5479
|
exports.translate = translate2;
|
|
4879
5480
|
exports.verbatraConfigSchema = verbatraConfigSchema;
|