@verbatra/sdk 0.4.4 → 0.5.0-next.1
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 +0 -1
- package/dist/index.cjs +830 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +299 -62
- package/dist/index.d.ts +299 -62
- package/dist/index.js +832 -178
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,8 @@ var fs = require('fs');
|
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var cosmiconfig = require('cosmiconfig');
|
|
6
6
|
var cosmiconfigTypescriptLoader = require('cosmiconfig-typescript-loader');
|
|
7
|
+
var crypto = require('crypto');
|
|
8
|
+
var promises = require('fs/promises');
|
|
7
9
|
var zod = require('zod');
|
|
8
10
|
var Anthropic = require('@anthropic-ai/sdk');
|
|
9
11
|
var deepl = require('deepl-node');
|
|
@@ -11,8 +13,6 @@ var module$1 = require('module');
|
|
|
11
13
|
var log = require('loglevel');
|
|
12
14
|
var genai = require('@google/genai');
|
|
13
15
|
var OpenAI = require('openai');
|
|
14
|
-
var crypto = require('crypto');
|
|
15
|
-
var promises = require('fs/promises');
|
|
16
16
|
var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
|
|
17
17
|
var xmldom = require('@xmldom/xmldom');
|
|
18
18
|
var yaml = require('yaml');
|
|
@@ -68,6 +68,155 @@ var SdkError = class extends Error {
|
|
|
68
68
|
this.code = code;
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
|
+
async function readBoundedUtf8(handle, size) {
|
|
72
|
+
const buffer = Buffer.allocUnsafe(size);
|
|
73
|
+
let offset = 0;
|
|
74
|
+
while (offset < size) {
|
|
75
|
+
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
76
|
+
if (bytesRead === 0) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
offset += bytesRead;
|
|
80
|
+
}
|
|
81
|
+
return buffer.toString("utf8", 0, offset);
|
|
82
|
+
}
|
|
83
|
+
async function readBounded(path, maxBytes) {
|
|
84
|
+
let handle;
|
|
85
|
+
try {
|
|
86
|
+
handle = await promises.open(path, "r");
|
|
87
|
+
} catch {
|
|
88
|
+
return { kind: "missing" };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const info = await handle.stat();
|
|
92
|
+
if (!info.isFile()) {
|
|
93
|
+
return { kind: "missing" };
|
|
94
|
+
}
|
|
95
|
+
if (info.size > maxBytes) {
|
|
96
|
+
return { kind: "too-large" };
|
|
97
|
+
}
|
|
98
|
+
return { kind: "ok", content: await readBoundedUtf8(handle, info.size) };
|
|
99
|
+
} finally {
|
|
100
|
+
await handle.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function readBoundedBytesInto(handle, size) {
|
|
104
|
+
const buffer = Buffer.allocUnsafeSlow(size);
|
|
105
|
+
let offset = 0;
|
|
106
|
+
while (offset < size) {
|
|
107
|
+
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
108
|
+
if (bytesRead === 0) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
offset += bytesRead;
|
|
112
|
+
}
|
|
113
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, offset);
|
|
114
|
+
}
|
|
115
|
+
async function readBoundedBytes(path, maxBytes) {
|
|
116
|
+
let handle;
|
|
117
|
+
try {
|
|
118
|
+
handle = await promises.open(path, "r");
|
|
119
|
+
} catch {
|
|
120
|
+
return { kind: "missing" };
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const info = await handle.stat();
|
|
124
|
+
if (!info.isFile()) {
|
|
125
|
+
return { kind: "missing" };
|
|
126
|
+
}
|
|
127
|
+
if (info.size > maxBytes) {
|
|
128
|
+
return { kind: "too-large" };
|
|
129
|
+
}
|
|
130
|
+
return { kind: "ok", bytes: await readBoundedBytesInto(handle, info.size) };
|
|
131
|
+
} finally {
|
|
132
|
+
await handle.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function tempFileName(path$1) {
|
|
136
|
+
return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
|
|
137
|
+
}
|
|
138
|
+
async function atomicWrite(path, data) {
|
|
139
|
+
const tmp = tempFileName(path);
|
|
140
|
+
await (typeof data === "string" ? promises.writeFile(tmp, data, "utf8") : promises.writeFile(tmp, data));
|
|
141
|
+
try {
|
|
142
|
+
await promises.rename(tmp, path);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
await promises.rm(tmp, { force: true });
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
var defaultFs = {
|
|
149
|
+
async fileExists(path) {
|
|
150
|
+
try {
|
|
151
|
+
await promises.access(path);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
|
|
158
|
+
readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
|
|
159
|
+
writeFile: (path, data) => atomicWrite(path, data),
|
|
160
|
+
writeBytes: (path, data) => atomicWrite(path, data)
|
|
161
|
+
};
|
|
162
|
+
var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
|
|
163
|
+
var glossaryRecordSchema = zod.z.record(zod.z.string(), zod.z.string());
|
|
164
|
+
var BOM = "\uFEFF";
|
|
165
|
+
var REPLACEMENT_CHARACTER = "\uFFFD";
|
|
166
|
+
var NUL = "\0";
|
|
167
|
+
function stripBom(content) {
|
|
168
|
+
return content.startsWith(BOM) ? content.slice(BOM.length) : content;
|
|
169
|
+
}
|
|
170
|
+
function looksLikeInvalidEncoding(content) {
|
|
171
|
+
return content.startsWith(REPLACEMENT_CHARACTER) || content.includes(NUL);
|
|
172
|
+
}
|
|
173
|
+
async function readGlossaryFile(resolvedPath, fs) {
|
|
174
|
+
const read = await fs.readFileBounded(resolvedPath, MAX_GLOSSARY_FILE_BYTES);
|
|
175
|
+
if (read.kind === "missing") {
|
|
176
|
+
throw new SdkError(
|
|
177
|
+
"CONFIG_INVALID",
|
|
178
|
+
`The glossary file at ${resolvedPath} was not found or could not be read.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (read.kind === "too-large") {
|
|
182
|
+
throw new SdkError(
|
|
183
|
+
"CONFIG_INVALID",
|
|
184
|
+
`The glossary file at ${resolvedPath} exceeds the maximum allowed size of ${MAX_GLOSSARY_FILE_BYTES} bytes.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const content = stripBom(read.content);
|
|
188
|
+
if (looksLikeInvalidEncoding(content)) {
|
|
189
|
+
throw new SdkError(
|
|
190
|
+
"CONFIG_INVALID",
|
|
191
|
+
`The glossary file at ${resolvedPath} must be UTF-8 encoded.`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(content);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new SdkError("CONFIG_INVALID", `The glossary file at ${resolvedPath} is not valid JSON.`);
|
|
199
|
+
}
|
|
200
|
+
const result = glossaryRecordSchema.safeParse(parsed);
|
|
201
|
+
if (!result.success) {
|
|
202
|
+
throw new SdkError(
|
|
203
|
+
"CONFIG_INVALID",
|
|
204
|
+
`The glossary file at ${resolvedPath} must contain a flat object of string keys to string values.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return result.data;
|
|
208
|
+
}
|
|
209
|
+
async function resolveGlossary(glossary, baseDir, fs) {
|
|
210
|
+
if (glossary === void 0) {
|
|
211
|
+
return { glossary: void 0, provenance: { source: "none" } };
|
|
212
|
+
}
|
|
213
|
+
if (typeof glossary === "string") {
|
|
214
|
+
const resolvedPath = path.resolve(baseDir, glossary);
|
|
215
|
+
const record = await readGlossaryFile(resolvedPath, fs);
|
|
216
|
+
return { glossary: record, provenance: { source: "file", path: resolvedPath } };
|
|
217
|
+
}
|
|
218
|
+
return { glossary, provenance: { source: "inline" } };
|
|
219
|
+
}
|
|
71
220
|
var FNV_OFFSET_BASIS = 14695981039346656037n;
|
|
72
221
|
var FNV_PRIME = 1099511628211n;
|
|
73
222
|
var U64_MASK = (1n << 64n) - 1n;
|
|
@@ -192,6 +341,85 @@ var LOCALE_TOKEN = "{locale}";
|
|
|
192
341
|
function localeFilePath(cwd, pattern, locale) {
|
|
193
342
|
return path.resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
|
|
194
343
|
}
|
|
344
|
+
var RATE_LIMITED_STATUS = 429;
|
|
345
|
+
var TIMEOUT_STATUS = 408;
|
|
346
|
+
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
347
|
+
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
348
|
+
"RateLimitError",
|
|
349
|
+
// openai, @anthropic-ai/sdk
|
|
350
|
+
"TooManyRequestsError"
|
|
351
|
+
// deepl-node
|
|
352
|
+
]);
|
|
353
|
+
var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
354
|
+
"AuthenticationError",
|
|
355
|
+
// openai, @anthropic-ai/sdk (401)
|
|
356
|
+
"PermissionDeniedError",
|
|
357
|
+
// openai, @anthropic-ai/sdk (403)
|
|
358
|
+
"AuthorizationError"
|
|
359
|
+
// deepl-node
|
|
360
|
+
]);
|
|
361
|
+
var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
362
|
+
"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
|
+
"ConnectionError"
|
|
367
|
+
]);
|
|
368
|
+
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
369
|
+
"APIUserAbortError"
|
|
370
|
+
// openai, @anthropic-ai/sdk
|
|
371
|
+
]);
|
|
372
|
+
function getErrorStatus(error) {
|
|
373
|
+
if (typeof error !== "object" || error === null || !("status" in error)) {
|
|
374
|
+
return void 0;
|
|
375
|
+
}
|
|
376
|
+
const status = error.status;
|
|
377
|
+
return typeof status === "number" ? status : void 0;
|
|
378
|
+
}
|
|
379
|
+
function readClassName(error) {
|
|
380
|
+
if (typeof error !== "object" || error === null) {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
return error.constructor?.name;
|
|
384
|
+
}
|
|
385
|
+
function classifyProviderError(error) {
|
|
386
|
+
const status = getErrorStatus(error);
|
|
387
|
+
if (status === RATE_LIMITED_STATUS) {
|
|
388
|
+
return "RATE_LIMITED";
|
|
389
|
+
}
|
|
390
|
+
if (status !== void 0 && AUTH_FAILED_STATUSES.has(status)) {
|
|
391
|
+
return "AUTH_FAILED";
|
|
392
|
+
}
|
|
393
|
+
if (status === TIMEOUT_STATUS) {
|
|
394
|
+
return "TIMEOUT";
|
|
395
|
+
}
|
|
396
|
+
const className = readClassName(error);
|
|
397
|
+
if (className !== void 0) {
|
|
398
|
+
if (RATE_LIMITED_CLASS_NAMES.has(className)) {
|
|
399
|
+
return "RATE_LIMITED";
|
|
400
|
+
}
|
|
401
|
+
if (AUTH_FAILED_CLASS_NAMES.has(className)) {
|
|
402
|
+
return "AUTH_FAILED";
|
|
403
|
+
}
|
|
404
|
+
if (TIMEOUT_CLASS_NAMES.has(className)) {
|
|
405
|
+
return "TIMEOUT";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return "PROVIDER_ERROR";
|
|
409
|
+
}
|
|
410
|
+
function isAbortShapedError(error) {
|
|
411
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
const className = readClassName(error);
|
|
415
|
+
return className !== void 0 && ABORT_ERROR_CLASS_NAMES.has(className);
|
|
416
|
+
}
|
|
417
|
+
function isAbortError(error, signal) {
|
|
418
|
+
if (signal !== void 0) {
|
|
419
|
+
return signal.aborted === true && isAbortShapedError(error);
|
|
420
|
+
}
|
|
421
|
+
return isAbortShapedError(error);
|
|
422
|
+
}
|
|
195
423
|
var REDACTED = "[REDACTED]";
|
|
196
424
|
var KEY_PATTERNS = [
|
|
197
425
|
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
@@ -226,17 +454,34 @@ var ProviderError = class extends Error {
|
|
|
226
454
|
}
|
|
227
455
|
};
|
|
228
456
|
var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
|
|
229
|
-
|
|
457
|
+
var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
|
|
458
|
+
var TIMEOUT_MESSAGE = "The translation provider request timed out.";
|
|
459
|
+
var AUTH_FAILED_MESSAGE = "The translation provider rejected the request credentials.";
|
|
460
|
+
var MESSAGE_BY_CODE = {
|
|
461
|
+
RATE_LIMITED: RATE_LIMITED_MESSAGE,
|
|
462
|
+
TIMEOUT: TIMEOUT_MESSAGE,
|
|
463
|
+
AUTH_FAILED: AUTH_FAILED_MESSAGE,
|
|
464
|
+
PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
|
|
465
|
+
};
|
|
466
|
+
async function guardProviderCall(call, signal) {
|
|
467
|
+
signal?.throwIfAborted();
|
|
230
468
|
try {
|
|
231
469
|
return await call();
|
|
232
|
-
} catch {
|
|
233
|
-
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (isAbortError(error, signal)) {
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
const code = classifyProviderError(error);
|
|
475
|
+
throw new ProviderError(code, MESSAGE_BY_CODE[code]);
|
|
234
476
|
}
|
|
235
477
|
}
|
|
236
|
-
function checkBatchIntegrity(inputs, extract) {
|
|
478
|
+
function checkBatchIntegrity(inputs, extract, compare) {
|
|
237
479
|
const integrity = /* @__PURE__ */ new Map();
|
|
238
|
-
for (const { key,
|
|
239
|
-
integrity.set(
|
|
480
|
+
for (const { key, sourceValue, translatedValue } of inputs) {
|
|
481
|
+
integrity.set(
|
|
482
|
+
key,
|
|
483
|
+
compare !== void 0 ? compare(sourceValue, translatedValue) : checkPlaceholders(extract(sourceValue), extract(translatedValue))
|
|
484
|
+
);
|
|
240
485
|
}
|
|
241
486
|
return integrity;
|
|
242
487
|
}
|
|
@@ -266,7 +511,7 @@ function toIntegrityInputs(entries, values) {
|
|
|
266
511
|
"The provider response is missing one or more keys."
|
|
267
512
|
);
|
|
268
513
|
}
|
|
269
|
-
return { key: entry.key,
|
|
514
|
+
return { key: entry.key, sourceValue: entry.value, translatedValue };
|
|
270
515
|
});
|
|
271
516
|
}
|
|
272
517
|
function toItem(entry) {
|
|
@@ -333,11 +578,16 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
333
578
|
const data = validateRequest(request);
|
|
334
579
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
335
580
|
const requestedKeys = data.entries.map((entry) => entry.key);
|
|
336
|
-
const completion = await mechanism.translate({
|
|
581
|
+
const completion = await mechanism.translate({
|
|
582
|
+
payloadJson,
|
|
583
|
+
requestedKeys,
|
|
584
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
585
|
+
});
|
|
337
586
|
const values = reconcileResult(completion.raw, requestedKeys);
|
|
338
587
|
const integrity = checkBatchIntegrity(
|
|
339
588
|
toIntegrityInputs(data.entries, values),
|
|
340
|
-
request.extractPlaceholders
|
|
589
|
+
request.extractPlaceholders,
|
|
590
|
+
request.comparePlaceholders
|
|
341
591
|
);
|
|
342
592
|
return completion.usage === void 0 ? { values, integrity } : { values, integrity, usage: completion.usage };
|
|
343
593
|
}
|
|
@@ -372,12 +622,29 @@ function requireGeminiKey() {
|
|
|
372
622
|
function requireDeepLKey() {
|
|
373
623
|
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
374
624
|
}
|
|
625
|
+
var OPENAI_COMPATIBLE_ENV_VAR = "OPENAI_COMPATIBLE_API_KEY";
|
|
626
|
+
var OPENAI_COMPATIBLE_KEY_PLACEHOLDER = "local";
|
|
627
|
+
function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
628
|
+
const varName = customEnvVar ?? OPENAI_COMPATIBLE_ENV_VAR;
|
|
629
|
+
const value = process.env[varName];
|
|
630
|
+
if (value !== void 0 && value.length > 0) {
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
if (customEnvVar !== void 0) {
|
|
634
|
+
throw new ProviderError(
|
|
635
|
+
"MISSING_API_KEY",
|
|
636
|
+
`The ${customEnvVar} environment variable is not set.`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return OPENAI_COMPATIBLE_KEY_PLACEHOLDER;
|
|
640
|
+
}
|
|
375
641
|
function createDefaultClient() {
|
|
376
642
|
const sdk = new Anthropic__default.default({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
377
643
|
return {
|
|
378
644
|
messages: {
|
|
379
|
-
create: async (body) => await sdk.messages.create(
|
|
380
|
-
body
|
|
645
|
+
create: async (body, options) => await sdk.messages.create(
|
|
646
|
+
body,
|
|
647
|
+
options
|
|
381
648
|
)
|
|
382
649
|
}
|
|
383
650
|
};
|
|
@@ -445,9 +712,9 @@ function createAnthropicProvider(config, deps = {}) {
|
|
|
445
712
|
}
|
|
446
713
|
function createMechanism(client, config) {
|
|
447
714
|
return {
|
|
448
|
-
translate: async ({ payloadJson }) => {
|
|
715
|
+
translate: async ({ payloadJson, signal }) => {
|
|
449
716
|
const body = buildRequest(config, payloadJson);
|
|
450
|
-
const message = await callClient(client, body);
|
|
717
|
+
const message = await callClient(client, body, signal);
|
|
451
718
|
assertNotTruncated(message.stop_reason === "max_tokens");
|
|
452
719
|
const raw = requireToolInput(message.content);
|
|
453
720
|
const usage = toUsage(message.usage);
|
|
@@ -455,8 +722,11 @@ function createMechanism(client, config) {
|
|
|
455
722
|
}
|
|
456
723
|
};
|
|
457
724
|
}
|
|
458
|
-
function callClient(client, body) {
|
|
459
|
-
return guardProviderCall(
|
|
725
|
+
function callClient(client, body, signal) {
|
|
726
|
+
return guardProviderCall(
|
|
727
|
+
() => client.messages.create(body, signal !== void 0 ? { signal } : void 0),
|
|
728
|
+
signal
|
|
729
|
+
);
|
|
460
730
|
}
|
|
461
731
|
function toUsage(usage) {
|
|
462
732
|
if (usage === void 0) {
|
|
@@ -551,7 +821,7 @@ function zipResults(entries, results) {
|
|
|
551
821
|
values.set(entry.key, translatedValue);
|
|
552
822
|
integrityInputs.push({
|
|
553
823
|
key: entry.key,
|
|
554
|
-
|
|
824
|
+
sourceValue: entry.value,
|
|
555
825
|
translatedValue
|
|
556
826
|
});
|
|
557
827
|
}
|
|
@@ -592,36 +862,97 @@ async function translate(bundle, config, request) {
|
|
|
592
862
|
data,
|
|
593
863
|
protectable,
|
|
594
864
|
options,
|
|
595
|
-
request.extractPlaceholders
|
|
865
|
+
request.extractPlaceholders,
|
|
866
|
+
request.comparePlaceholders,
|
|
867
|
+
request.signal
|
|
596
868
|
);
|
|
597
869
|
if (unprotectable.length > 0) {
|
|
598
870
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
599
871
|
}
|
|
600
872
|
return { values, integrity, notices };
|
|
601
873
|
}
|
|
602
|
-
async function translateProtectable(client, data, protectable, options, extract) {
|
|
874
|
+
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
603
875
|
if (protectable.length === 0) {
|
|
604
876
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
605
877
|
}
|
|
606
878
|
const texts = protectable.map((entry) => entry.value);
|
|
607
|
-
const results = await callClient2(
|
|
879
|
+
const results = await callClient2(
|
|
880
|
+
client,
|
|
881
|
+
texts,
|
|
882
|
+
data.sourceLocale,
|
|
883
|
+
data.targetLocale,
|
|
884
|
+
options,
|
|
885
|
+
signal
|
|
886
|
+
);
|
|
608
887
|
const { values, integrityInputs } = zipResults(protectable, results);
|
|
609
|
-
const integrity = checkBatchIntegrity(integrityInputs, extract);
|
|
888
|
+
const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
|
|
610
889
|
return { values, integrity };
|
|
611
890
|
}
|
|
612
|
-
function callClient2(client, texts, sourceLang, targetLang, options) {
|
|
613
|
-
return guardProviderCall(
|
|
891
|
+
function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
892
|
+
return guardProviderCall(
|
|
893
|
+
() => client.translateText(texts, sourceLang, targetLang, options),
|
|
894
|
+
signal
|
|
895
|
+
);
|
|
614
896
|
}
|
|
615
897
|
var geminiConfigSchema = zod.z.object({
|
|
616
898
|
model: zod.z.string().min(1),
|
|
617
899
|
maxOutputTokens: zod.z.number().int().positive()
|
|
618
900
|
});
|
|
901
|
+
var DEFAULT_GEMINI_RETRY = { attempts: 3, baseDelayMs: 250 };
|
|
902
|
+
function isRetryableStatus(error) {
|
|
903
|
+
const status = getErrorStatus(error);
|
|
904
|
+
return status === 429 || status !== void 0 && status >= 500 && status < 600;
|
|
905
|
+
}
|
|
906
|
+
function isAborted(signal) {
|
|
907
|
+
return signal?.aborted === true;
|
|
908
|
+
}
|
|
909
|
+
function throwAbort(signal) {
|
|
910
|
+
signal?.throwIfAborted();
|
|
911
|
+
throw new DOMException("This operation was aborted.", "AbortError");
|
|
912
|
+
}
|
|
913
|
+
function delay(ms, signal) {
|
|
914
|
+
return new Promise((resolve7) => {
|
|
915
|
+
const onAbort = () => {
|
|
916
|
+
clearTimeout(timer);
|
|
917
|
+
resolve7();
|
|
918
|
+
};
|
|
919
|
+
const timer = setTimeout(() => {
|
|
920
|
+
signal?.removeEventListener("abort", onAbort);
|
|
921
|
+
resolve7();
|
|
922
|
+
}, ms);
|
|
923
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
async function withGeminiRetry(call, signal, config = DEFAULT_GEMINI_RETRY) {
|
|
927
|
+
let attempt = 1;
|
|
928
|
+
while (true) {
|
|
929
|
+
try {
|
|
930
|
+
return await call();
|
|
931
|
+
} catch (error) {
|
|
932
|
+
if (isAborted(signal)) {
|
|
933
|
+
throwAbort(signal);
|
|
934
|
+
}
|
|
935
|
+
const exhausted = attempt >= config.attempts;
|
|
936
|
+
if (exhausted || !isRetryableStatus(error)) {
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
939
|
+
await delay(config.baseDelayMs * 2 ** (attempt - 1), signal);
|
|
940
|
+
if (isAborted(signal)) {
|
|
941
|
+
throwAbort(signal);
|
|
942
|
+
}
|
|
943
|
+
attempt += 1;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
619
947
|
function createDefaultClient3() {
|
|
620
948
|
const ai = new genai.GoogleGenAI({ apiKey: requireGeminiKey() });
|
|
621
949
|
return {
|
|
622
950
|
models: {
|
|
623
|
-
generateContent:
|
|
624
|
-
|
|
951
|
+
generateContent: (request) => withGeminiRetry(
|
|
952
|
+
async () => await ai.models.generateContent(
|
|
953
|
+
request
|
|
954
|
+
),
|
|
955
|
+
request.config.abortSignal
|
|
625
956
|
)
|
|
626
957
|
}
|
|
627
958
|
};
|
|
@@ -685,7 +1016,7 @@ var GEMINI_SYSTEM_RULES = [
|
|
|
685
1016
|
"When a tone is provided, honor it.",
|
|
686
1017
|
"Respond only with the structured object: exactly one entry per requested key, no commentary, no extra keys, and no key that was not requested."
|
|
687
1018
|
].join("\n");
|
|
688
|
-
function buildGeminiRequest(config, payloadJson) {
|
|
1019
|
+
function buildGeminiRequest(config, payloadJson, signal) {
|
|
689
1020
|
return {
|
|
690
1021
|
model: config.model,
|
|
691
1022
|
contents: [{ role: "user", parts: [{ text: payloadJson }] }],
|
|
@@ -693,7 +1024,8 @@ function buildGeminiRequest(config, payloadJson) {
|
|
|
693
1024
|
systemInstruction: GEMINI_SYSTEM_RULES,
|
|
694
1025
|
responseMimeType: "application/json",
|
|
695
1026
|
responseSchema: toGeminiSchema(deriveJsonSchema(translationsResultSchema)),
|
|
696
|
-
maxOutputTokens: config.maxOutputTokens
|
|
1027
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
1028
|
+
...signal !== void 0 ? { abortSignal: signal } : {}
|
|
697
1029
|
}
|
|
698
1030
|
};
|
|
699
1031
|
}
|
|
@@ -757,15 +1089,15 @@ function createGeminiProvider(config, deps = {}) {
|
|
|
757
1089
|
}
|
|
758
1090
|
function createMechanism2(client, config) {
|
|
759
1091
|
return {
|
|
760
|
-
translate: async ({ payloadJson }) => {
|
|
761
|
-
const request = buildGeminiRequest(config, payloadJson);
|
|
762
|
-
const response = await callClient3(client, request);
|
|
1092
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1093
|
+
const request = buildGeminiRequest(config, payloadJson, signal);
|
|
1094
|
+
const response = await callClient3(client, request, signal);
|
|
763
1095
|
return extractGeminiResult(response);
|
|
764
1096
|
}
|
|
765
1097
|
};
|
|
766
1098
|
}
|
|
767
|
-
function callClient3(client, request) {
|
|
768
|
-
return guardProviderCall(() => client.models.generateContent(request));
|
|
1099
|
+
function callClient3(client, request, signal) {
|
|
1100
|
+
return guardProviderCall(() => client.models.generateContent(request), signal);
|
|
769
1101
|
}
|
|
770
1102
|
var openAiConfigSchema = zod.z.object({
|
|
771
1103
|
model: zod.z.string().min(1),
|
|
@@ -776,8 +1108,9 @@ function createDefaultClient4() {
|
|
|
776
1108
|
return {
|
|
777
1109
|
chat: {
|
|
778
1110
|
completions: {
|
|
779
|
-
create: async (body) => await sdk.chat.completions.create(
|
|
780
|
-
body
|
|
1111
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1112
|
+
body,
|
|
1113
|
+
options
|
|
781
1114
|
)
|
|
782
1115
|
}
|
|
783
1116
|
}
|
|
@@ -795,7 +1128,20 @@ var OPENAI_SYSTEM_RULES = [
|
|
|
795
1128
|
"When a tone is provided, honor it.",
|
|
796
1129
|
"Respond only with the structured object: exactly one entry per requested key, no commentary, no extra keys, and no key that was not requested."
|
|
797
1130
|
].join("\n");
|
|
798
|
-
function
|
|
1131
|
+
function buildResponseFormat(mode) {
|
|
1132
|
+
if (mode === "json-object") {
|
|
1133
|
+
return { type: "json_object" };
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
type: "json_schema",
|
|
1137
|
+
json_schema: {
|
|
1138
|
+
name: RESULT_SCHEMA_NAME,
|
|
1139
|
+
strict: true,
|
|
1140
|
+
schema: deriveJsonSchema(translationsResultSchema)
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema") {
|
|
799
1145
|
return {
|
|
800
1146
|
model: config.model,
|
|
801
1147
|
max_completion_tokens: config.maxOutputTokens,
|
|
@@ -803,19 +1149,66 @@ function buildOpenAiRequest(config, payloadJson) {
|
|
|
803
1149
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
804
1150
|
{ role: "user", content: payloadJson }
|
|
805
1151
|
],
|
|
806
|
-
response_format:
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1152
|
+
response_format: buildResponseFormat(mode)
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function advanceStringScan(char, escaped) {
|
|
1156
|
+
if (escaped) {
|
|
1157
|
+
return { inString: true, escaped: false };
|
|
1158
|
+
}
|
|
1159
|
+
if (char === "\\") {
|
|
1160
|
+
return { inString: true, escaped: true };
|
|
1161
|
+
}
|
|
1162
|
+
return { inString: char !== '"', escaped: false };
|
|
1163
|
+
}
|
|
1164
|
+
function scanBalancedObjectEnd(content, start) {
|
|
1165
|
+
let depth = 0;
|
|
1166
|
+
let inString = false;
|
|
1167
|
+
let escaped = false;
|
|
1168
|
+
for (let i = start; i < content.length; i += 1) {
|
|
1169
|
+
const char = content[i];
|
|
1170
|
+
if (inString) {
|
|
1171
|
+
({ inString, escaped } = advanceStringScan(char, escaped));
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (char === '"') {
|
|
1175
|
+
inString = true;
|
|
1176
|
+
} else if (char === "{") {
|
|
1177
|
+
depth += 1;
|
|
1178
|
+
} else if (char === "}") {
|
|
1179
|
+
depth -= 1;
|
|
1180
|
+
if (depth === 0) {
|
|
1181
|
+
return i;
|
|
812
1182
|
}
|
|
813
1183
|
}
|
|
814
|
-
}
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
function extractBalancedJson(content) {
|
|
1188
|
+
let searchFrom = 0;
|
|
1189
|
+
while (searchFrom < content.length) {
|
|
1190
|
+
const start = content.indexOf("{", searchFrom);
|
|
1191
|
+
if (start === -1) {
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
const end = scanBalancedObjectEnd(content, start);
|
|
1195
|
+
if (end === null) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
const candidate = content.slice(start, end + 1);
|
|
1199
|
+
try {
|
|
1200
|
+
JSON.parse(candidate);
|
|
1201
|
+
return candidate;
|
|
1202
|
+
} catch {
|
|
1203
|
+
searchFrom = end + 1;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
815
1207
|
}
|
|
816
|
-
function parseContent2(content) {
|
|
1208
|
+
function parseContent2(content, tolerant) {
|
|
1209
|
+
const candidate = tolerant ? extractBalancedJson(content) ?? content : content;
|
|
817
1210
|
try {
|
|
818
|
-
return JSON.parse(
|
|
1211
|
+
return JSON.parse(candidate);
|
|
819
1212
|
} catch {
|
|
820
1213
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned unparseable content.");
|
|
821
1214
|
}
|
|
@@ -830,7 +1223,7 @@ function toUsage3(usage) {
|
|
|
830
1223
|
}
|
|
831
1224
|
return { inputTokens: prompt_tokens, outputTokens: completion_tokens };
|
|
832
1225
|
}
|
|
833
|
-
function extractOpenAiResult(completion) {
|
|
1226
|
+
function extractOpenAiResult(completion, tolerant = false) {
|
|
834
1227
|
const choice = completion.choices[0];
|
|
835
1228
|
if (choice === void 0) {
|
|
836
1229
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no message.");
|
|
@@ -843,7 +1236,7 @@ function extractOpenAiResult(completion) {
|
|
|
843
1236
|
if (message.content === void 0 || message.content === null) {
|
|
844
1237
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no translation content.");
|
|
845
1238
|
}
|
|
846
|
-
const raw = parseContent2(message.content);
|
|
1239
|
+
const raw = parseContent2(message.content, tolerant);
|
|
847
1240
|
const usage = toUsage3(completion.usage);
|
|
848
1241
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
849
1242
|
}
|
|
@@ -861,15 +1254,84 @@ function createOpenAiProvider(config, deps = {}) {
|
|
|
861
1254
|
}
|
|
862
1255
|
function createMechanism3(client, config) {
|
|
863
1256
|
return {
|
|
864
|
-
translate: async ({ payloadJson }) => {
|
|
1257
|
+
translate: async ({ payloadJson, signal }) => {
|
|
865
1258
|
const body = buildOpenAiRequest(config, payloadJson);
|
|
866
|
-
const completion = await callClient4(client, body);
|
|
1259
|
+
const completion = await callClient4(client, body, signal);
|
|
867
1260
|
return extractOpenAiResult(completion);
|
|
868
1261
|
}
|
|
869
1262
|
};
|
|
870
1263
|
}
|
|
871
|
-
function callClient4(client, body) {
|
|
872
|
-
return guardProviderCall(
|
|
1264
|
+
function callClient4(client, body, signal) {
|
|
1265
|
+
return guardProviderCall(
|
|
1266
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1267
|
+
signal
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
var HOSTED_PROVIDER_ENV_VARS = new Set(
|
|
1271
|
+
Object.values(PROVIDER_ENV).map((name) => name.toUpperCase())
|
|
1272
|
+
);
|
|
1273
|
+
function isNotHostedProviderEnvVar(value) {
|
|
1274
|
+
return !HOSTED_PROVIDER_ENV_VARS.has(value.toUpperCase());
|
|
1275
|
+
}
|
|
1276
|
+
function isHttpOrHttpsUrl(value) {
|
|
1277
|
+
try {
|
|
1278
|
+
const { protocol } = new URL(value);
|
|
1279
|
+
return protocol === "http:" || protocol === "https:";
|
|
1280
|
+
} catch {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
var openAiCompatibleConfigSchema = zod.z.object({
|
|
1285
|
+
baseUrl: zod.z.url({ message: "baseUrl must be a valid absolute URL." }).refine(isHttpOrHttpsUrl, { message: "baseUrl must use the http or https scheme." }),
|
|
1286
|
+
model: zod.z.string().min(1),
|
|
1287
|
+
maxOutputTokens: zod.z.number().int().positive(),
|
|
1288
|
+
apiKeyEnvVar: zod.z.string().min(1).refine(isNotHostedProviderEnvVar, {
|
|
1289
|
+
message: "apiKeyEnvVar must not name a hosted provider's environment variable."
|
|
1290
|
+
}).optional()
|
|
1291
|
+
});
|
|
1292
|
+
function createDefaultClient5(config) {
|
|
1293
|
+
const sdk = new OpenAI__default.default({
|
|
1294
|
+
apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
|
|
1295
|
+
baseURL: config.baseUrl,
|
|
1296
|
+
logLevel: "off"
|
|
1297
|
+
});
|
|
1298
|
+
return {
|
|
1299
|
+
chat: {
|
|
1300
|
+
completions: {
|
|
1301
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1302
|
+
body,
|
|
1303
|
+
options
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
var PROVIDER_ID5 = "openai-compatible";
|
|
1310
|
+
function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
1311
|
+
const validConfig = openAiCompatibleConfigSchema.parse(config);
|
|
1312
|
+
const client = deps.client ?? createDefaultClient5(validConfig);
|
|
1313
|
+
const mechanism = createMechanism4(client, validConfig);
|
|
1314
|
+
return {
|
|
1315
|
+
id: PROVIDER_ID5,
|
|
1316
|
+
kind: "llm",
|
|
1317
|
+
supportsGlossary: true,
|
|
1318
|
+
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function createMechanism4(client, config) {
|
|
1322
|
+
return {
|
|
1323
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1324
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema");
|
|
1325
|
+
const completion = await callClient5(client, body, signal);
|
|
1326
|
+
return extractOpenAiResult(completion, true);
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function callClient5(client, body, signal) {
|
|
1331
|
+
return guardProviderCall(
|
|
1332
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1333
|
+
signal
|
|
1334
|
+
);
|
|
873
1335
|
}
|
|
874
1336
|
var SCAFFOLD_MODELS = {
|
|
875
1337
|
anthropic: "claude-sonnet-4-6",
|
|
@@ -880,13 +1342,18 @@ var providerConfigSchema = zod.z.discriminatedUnion("id", [
|
|
|
880
1342
|
zod.z.object({ id: zod.z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
|
|
881
1343
|
zod.z.object({ id: zod.z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
882
1344
|
zod.z.object({ id: zod.z.literal("gemini"), options: geminiConfigSchema.strict() }),
|
|
883
|
-
zod.z.object({ id: zod.z.literal("deepl"), options: deepLConfigSchema.strict() })
|
|
1345
|
+
zod.z.object({ id: zod.z.literal("deepl"), options: deepLConfigSchema.strict() }),
|
|
1346
|
+
zod.z.object({
|
|
1347
|
+
id: zod.z.literal("openai-compatible"),
|
|
1348
|
+
options: openAiCompatibleConfigSchema.strict()
|
|
1349
|
+
})
|
|
884
1350
|
]);
|
|
885
1351
|
var providerFactories = {
|
|
886
1352
|
anthropic: (options) => createAnthropicProvider(options),
|
|
887
1353
|
openai: (options) => createOpenAiProvider(options),
|
|
888
1354
|
gemini: (options) => createGeminiProvider(options),
|
|
889
|
-
deepl: (options) => createDeepLProvider(options)
|
|
1355
|
+
deepl: (options) => createDeepLProvider(options),
|
|
1356
|
+
"openai-compatible": (options) => createOpenAiCompatibleProvider(options)
|
|
890
1357
|
};
|
|
891
1358
|
function buildProvider(config) {
|
|
892
1359
|
const create = providerFactories[config.id];
|
|
@@ -903,7 +1370,13 @@ var verbatraConfigSchema = zod.z.strictObject({
|
|
|
903
1370
|
pattern: zod.z.string().min(1)
|
|
904
1371
|
}),
|
|
905
1372
|
provider: providerConfigSchema,
|
|
906
|
-
|
|
1373
|
+
/**
|
|
1374
|
+
* A glossary of source terms to preferred target terms, either inline or as a path to a JSON file
|
|
1375
|
+
* carrying the same shape. The two forms are mutually exclusive by type; there is no merge or
|
|
1376
|
+
* precedence between them. A path is resolved and validated by the loader, not by this schema; see
|
|
1377
|
+
* `resolve-glossary.ts`.
|
|
1378
|
+
*/
|
|
1379
|
+
glossary: zod.z.union([zod.z.record(zod.z.string(), zod.z.string()), zod.z.string().min(1)]).optional(),
|
|
907
1380
|
tone: zod.z.enum(["formal", "informal", "neutral"]).optional(),
|
|
908
1381
|
/**
|
|
909
1382
|
* Opt-in orphan pruning, off by default. When true, keys present in a target file but absent from
|
|
@@ -957,7 +1430,7 @@ function formatIssues(error) {
|
|
|
957
1430
|
return issue.code === "unrecognized_keys" ? `${base} (API keys are read from the environment, not the config)` : base;
|
|
958
1431
|
}).join("; ");
|
|
959
1432
|
}
|
|
960
|
-
function
|
|
1433
|
+
function parseConfig(input) {
|
|
961
1434
|
const parsed = verbatraConfigSchema.safeParse(input);
|
|
962
1435
|
if (!parsed.success) {
|
|
963
1436
|
throw new SdkError(
|
|
@@ -967,7 +1440,16 @@ function validate(input) {
|
|
|
967
1440
|
}
|
|
968
1441
|
return parsed.data;
|
|
969
1442
|
}
|
|
970
|
-
async function
|
|
1443
|
+
async function finalizeConfig(parsed, baseDir, fs) {
|
|
1444
|
+
const { glossary: glossaryInput, ...rest } = parsed;
|
|
1445
|
+
const resolved = await resolveGlossary(glossaryInput, baseDir, fs);
|
|
1446
|
+
const config = {
|
|
1447
|
+
...rest,
|
|
1448
|
+
...resolved.glossary !== void 0 ? { glossary: resolved.glossary } : {}
|
|
1449
|
+
};
|
|
1450
|
+
return { config, glossary: resolved.provenance };
|
|
1451
|
+
}
|
|
1452
|
+
async function loadExplicitWithMeta(explorer, configPath, cwd, fs$1) {
|
|
971
1453
|
const resolved = path.resolve(cwd ?? process.cwd(), configPath);
|
|
972
1454
|
if (!fs.existsSync(resolved)) {
|
|
973
1455
|
throw new SdkError("CONFIG_NOT_FOUND", `No verbatra configuration file at ${resolved}.`);
|
|
@@ -979,18 +1461,23 @@ async function loadExplicit(explorer, configPath, cwd) {
|
|
|
979
1461
|
const detail = error instanceof Error ? error.message : String(error);
|
|
980
1462
|
throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
|
|
981
1463
|
}
|
|
982
|
-
|
|
1464
|
+
const parsed = parseConfig(result?.config);
|
|
1465
|
+
const { config, glossary } = await finalizeConfig(parsed, path.dirname(resolved), fs$1);
|
|
1466
|
+
return { config, source: { kind: "explicit", filepath: resolved }, glossary };
|
|
983
1467
|
}
|
|
984
|
-
async function
|
|
1468
|
+
async function loadConfigWithMeta(options = {}) {
|
|
1469
|
+
const fs = options.fs ?? defaultFs;
|
|
985
1470
|
if (options.configOverride !== void 0) {
|
|
986
|
-
|
|
1471
|
+
const parsed2 = parseConfig(options.configOverride);
|
|
1472
|
+
const { config: config2, glossary: glossary2 } = await finalizeConfig(parsed2, options.cwd ?? process.cwd(), fs);
|
|
1473
|
+
return { config: config2, source: { kind: "override" }, glossary: glossary2 };
|
|
987
1474
|
}
|
|
988
1475
|
const explorer = cosmiconfig.cosmiconfig(MODULE_NAME, {
|
|
989
1476
|
searchPlaces: SEARCH_PLACES,
|
|
990
1477
|
loaders: { ".ts": cosmiconfigTypescriptLoader.TypeScriptLoader() }
|
|
991
1478
|
});
|
|
992
1479
|
if (options.configPath !== void 0) {
|
|
993
|
-
return
|
|
1480
|
+
return loadExplicitWithMeta(explorer, options.configPath, options.cwd, fs);
|
|
994
1481
|
}
|
|
995
1482
|
let result;
|
|
996
1483
|
try {
|
|
@@ -1005,99 +1492,14 @@ async function loadConfig(options = {}) {
|
|
|
1005
1492
|
"No verbatra configuration found. Create a verbatra.config.ts, a .verbatrarc.json, or a 'verbatra' property in package.json."
|
|
1006
1493
|
);
|
|
1007
1494
|
}
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
const buffer = Buffer.allocUnsafe(size);
|
|
1012
|
-
let offset = 0;
|
|
1013
|
-
while (offset < size) {
|
|
1014
|
-
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
1015
|
-
if (bytesRead === 0) {
|
|
1016
|
-
break;
|
|
1017
|
-
}
|
|
1018
|
-
offset += bytesRead;
|
|
1019
|
-
}
|
|
1020
|
-
return buffer.toString("utf8", 0, offset);
|
|
1021
|
-
}
|
|
1022
|
-
async function readBounded(path, maxBytes) {
|
|
1023
|
-
let handle;
|
|
1024
|
-
try {
|
|
1025
|
-
handle = await promises.open(path, "r");
|
|
1026
|
-
} catch {
|
|
1027
|
-
return { kind: "missing" };
|
|
1028
|
-
}
|
|
1029
|
-
try {
|
|
1030
|
-
const info = await handle.stat();
|
|
1031
|
-
if (!info.isFile()) {
|
|
1032
|
-
return { kind: "missing" };
|
|
1033
|
-
}
|
|
1034
|
-
if (info.size > maxBytes) {
|
|
1035
|
-
return { kind: "too-large" };
|
|
1036
|
-
}
|
|
1037
|
-
return { kind: "ok", content: await readBoundedUtf8(handle, info.size) };
|
|
1038
|
-
} finally {
|
|
1039
|
-
await handle.close();
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
async function readBoundedBytesInto(handle, size) {
|
|
1043
|
-
const buffer = Buffer.allocUnsafeSlow(size);
|
|
1044
|
-
let offset = 0;
|
|
1045
|
-
while (offset < size) {
|
|
1046
|
-
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
1047
|
-
if (bytesRead === 0) {
|
|
1048
|
-
break;
|
|
1049
|
-
}
|
|
1050
|
-
offset += bytesRead;
|
|
1051
|
-
}
|
|
1052
|
-
return new Uint8Array(buffer.buffer, buffer.byteOffset, offset);
|
|
1053
|
-
}
|
|
1054
|
-
async function readBoundedBytes(path, maxBytes) {
|
|
1055
|
-
let handle;
|
|
1056
|
-
try {
|
|
1057
|
-
handle = await promises.open(path, "r");
|
|
1058
|
-
} catch {
|
|
1059
|
-
return { kind: "missing" };
|
|
1060
|
-
}
|
|
1061
|
-
try {
|
|
1062
|
-
const info = await handle.stat();
|
|
1063
|
-
if (!info.isFile()) {
|
|
1064
|
-
return { kind: "missing" };
|
|
1065
|
-
}
|
|
1066
|
-
if (info.size > maxBytes) {
|
|
1067
|
-
return { kind: "too-large" };
|
|
1068
|
-
}
|
|
1069
|
-
return { kind: "ok", bytes: await readBoundedBytesInto(handle, info.size) };
|
|
1070
|
-
} finally {
|
|
1071
|
-
await handle.close();
|
|
1072
|
-
}
|
|
1495
|
+
const parsed = parseConfig(result.config);
|
|
1496
|
+
const { config, glossary } = await finalizeConfig(parsed, path.dirname(result.filepath), fs);
|
|
1497
|
+
return { config, source: { kind: "search", filepath: result.filepath }, glossary };
|
|
1073
1498
|
}
|
|
1074
|
-
function
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
async function atomicWrite(path, data) {
|
|
1078
|
-
const tmp = tempFileName(path);
|
|
1079
|
-
await (typeof data === "string" ? promises.writeFile(tmp, data, "utf8") : promises.writeFile(tmp, data));
|
|
1080
|
-
try {
|
|
1081
|
-
await promises.rename(tmp, path);
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
await promises.rm(tmp, { force: true });
|
|
1084
|
-
throw error;
|
|
1085
|
-
}
|
|
1499
|
+
async function loadConfig(options = {}) {
|
|
1500
|
+
const { config } = await loadConfigWithMeta(options);
|
|
1501
|
+
return config;
|
|
1086
1502
|
}
|
|
1087
|
-
var defaultFs = {
|
|
1088
|
-
async fileExists(path) {
|
|
1089
|
-
try {
|
|
1090
|
-
await promises.access(path);
|
|
1091
|
-
return true;
|
|
1092
|
-
} catch {
|
|
1093
|
-
return false;
|
|
1094
|
-
}
|
|
1095
|
-
},
|
|
1096
|
-
readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
|
|
1097
|
-
readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
|
|
1098
|
-
writeFile: (path, data) => atomicWrite(path, data),
|
|
1099
|
-
writeBytes: (path, data) => atomicWrite(path, data)
|
|
1100
|
-
};
|
|
1101
1503
|
var LOCK_FILE_NAME = "verbatra.lock.json";
|
|
1102
1504
|
var CURRENT_VERSION = 1;
|
|
1103
1505
|
var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
|
|
@@ -1130,6 +1532,12 @@ async function readLockFile(path, fs) {
|
|
|
1130
1532
|
if (!result.success) {
|
|
1131
1533
|
throw new SdkError("LOCK_FILE_INVALID", `The lock-file at ${path} has an unexpected shape.`);
|
|
1132
1534
|
}
|
|
1535
|
+
if (result.data.version !== CURRENT_VERSION) {
|
|
1536
|
+
throw new SdkError(
|
|
1537
|
+
"LOCK_FILE_INVALID",
|
|
1538
|
+
`The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION}.`
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1133
1541
|
return result.data;
|
|
1134
1542
|
}
|
|
1135
1543
|
function baselineFor(lock, locale) {
|
|
@@ -1262,6 +1670,150 @@ function icuInvalidKeys(entries) {
|
|
|
1262
1670
|
}
|
|
1263
1671
|
return invalid;
|
|
1264
1672
|
}
|
|
1673
|
+
function tokenOf2(element) {
|
|
1674
|
+
switch (element.type) {
|
|
1675
|
+
case icuMessageformatParser.TYPE.argument:
|
|
1676
|
+
case icuMessageformatParser.TYPE.number:
|
|
1677
|
+
case icuMessageformatParser.TYPE.date:
|
|
1678
|
+
case icuMessageformatParser.TYPE.time:
|
|
1679
|
+
case icuMessageformatParser.TYPE.select:
|
|
1680
|
+
case icuMessageformatParser.TYPE.plural:
|
|
1681
|
+
return `{${element.value}}`;
|
|
1682
|
+
case icuMessageformatParser.TYPE.tag:
|
|
1683
|
+
return `<${element.value}>`;
|
|
1684
|
+
default:
|
|
1685
|
+
return void 0;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
function isBranching(element) {
|
|
1689
|
+
return element.type === icuMessageformatParser.TYPE.plural || element.type === icuMessageformatParser.TYPE.select;
|
|
1690
|
+
}
|
|
1691
|
+
function isTag(element) {
|
|
1692
|
+
return element.type === icuMessageformatParser.TYPE.tag;
|
|
1693
|
+
}
|
|
1694
|
+
function layerTokens(elements) {
|
|
1695
|
+
const tokens = [];
|
|
1696
|
+
for (const element of elements) {
|
|
1697
|
+
const token = tokenOf2(element);
|
|
1698
|
+
if (token !== void 0) {
|
|
1699
|
+
tokens.push(token);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return tokens;
|
|
1703
|
+
}
|
|
1704
|
+
function deepTokens(elements) {
|
|
1705
|
+
const tokens = [];
|
|
1706
|
+
for (const element of elements) {
|
|
1707
|
+
const token = tokenOf2(element);
|
|
1708
|
+
if (token !== void 0) {
|
|
1709
|
+
tokens.push(token);
|
|
1710
|
+
}
|
|
1711
|
+
if (isTag(element)) {
|
|
1712
|
+
tokens.push(...deepTokens(element.children));
|
|
1713
|
+
} else if (isBranching(element)) {
|
|
1714
|
+
for (const option of Object.values(element.options)) {
|
|
1715
|
+
tokens.push(...deepTokens(option.value));
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return tokens;
|
|
1720
|
+
}
|
|
1721
|
+
function branchesByCategory(element) {
|
|
1722
|
+
return new Map(
|
|
1723
|
+
Object.entries(element.options).map(([category, option]) => [category, option.value])
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
function findUnconsumed(target, consumed, isMatch) {
|
|
1727
|
+
for (let index = 0; index < target.length; index += 1) {
|
|
1728
|
+
if (consumed.has(index)) {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
const candidate = target[index];
|
|
1732
|
+
if (candidate !== void 0 && isMatch(candidate)) {
|
|
1733
|
+
return { element: candidate, index };
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return void 0;
|
|
1737
|
+
}
|
|
1738
|
+
function findMatchingBranching(source, target, consumed) {
|
|
1739
|
+
return findUnconsumed(
|
|
1740
|
+
target,
|
|
1741
|
+
consumed,
|
|
1742
|
+
(candidate) => isBranching(candidate) && candidate.value === source.value
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
function findMatchingTag(source, target, consumed) {
|
|
1746
|
+
return findUnconsumed(
|
|
1747
|
+
target,
|
|
1748
|
+
consumed,
|
|
1749
|
+
(candidate) => isTag(candidate) && candidate.value === source.value
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
function combineResults(results) {
|
|
1753
|
+
const missing = results.flatMap((result) => result.missing).sort();
|
|
1754
|
+
const extra = results.flatMap((result) => result.extra).sort();
|
|
1755
|
+
return { matches: missing.length === 0 && extra.length === 0, missing, extra, reordered: false };
|
|
1756
|
+
}
|
|
1757
|
+
function compareAgainstSourceUnion(sourceBranches, targetBranch) {
|
|
1758
|
+
const union = /* @__PURE__ */ new Set();
|
|
1759
|
+
for (const branch of sourceBranches.values()) {
|
|
1760
|
+
for (const token of deepTokens(branch)) {
|
|
1761
|
+
union.add(token);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const extra = deepTokens(targetBranch).filter((token) => !union.has(token)).sort();
|
|
1765
|
+
return { matches: extra.length === 0, missing: [], extra, reordered: false };
|
|
1766
|
+
}
|
|
1767
|
+
function compareBranching(source, target) {
|
|
1768
|
+
const sourceBranches = branchesByCategory(source);
|
|
1769
|
+
const targetBranches = branchesByCategory(target);
|
|
1770
|
+
const categories = /* @__PURE__ */ new Set([...sourceBranches.keys(), ...targetBranches.keys()]);
|
|
1771
|
+
const results = [];
|
|
1772
|
+
for (const category of categories) {
|
|
1773
|
+
const sourceBranch = sourceBranches.get(category);
|
|
1774
|
+
const targetBranch = targetBranches.get(category);
|
|
1775
|
+
if (sourceBranch !== void 0 && targetBranch !== void 0) {
|
|
1776
|
+
results.push(compareElements(sourceBranch, targetBranch));
|
|
1777
|
+
} else if (targetBranch !== void 0) {
|
|
1778
|
+
results.push(compareAgainstSourceUnion(sourceBranches, targetBranch));
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return combineResults(results);
|
|
1782
|
+
}
|
|
1783
|
+
function nestedResults(source, target) {
|
|
1784
|
+
const results = [];
|
|
1785
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1786
|
+
for (const element of source) {
|
|
1787
|
+
if (isBranching(element)) {
|
|
1788
|
+
const found = findMatchingBranching(element, target, consumed);
|
|
1789
|
+
if (found !== void 0) {
|
|
1790
|
+
consumed.add(found.index);
|
|
1791
|
+
results.push(compareBranching(element, found.element));
|
|
1792
|
+
}
|
|
1793
|
+
} else if (isTag(element)) {
|
|
1794
|
+
const found = findMatchingTag(element, target, consumed);
|
|
1795
|
+
if (found !== void 0) {
|
|
1796
|
+
consumed.add(found.index);
|
|
1797
|
+
results.push(compareElements(element.children, found.element.children));
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return results;
|
|
1802
|
+
}
|
|
1803
|
+
function compareElements(source, target) {
|
|
1804
|
+
const layer = checkPlaceholders(layerTokens(source), layerTokens(target));
|
|
1805
|
+
const nested = nestedResults(source, target);
|
|
1806
|
+
return nested.length === 0 ? layer : combineResults([layer, ...nested]);
|
|
1807
|
+
}
|
|
1808
|
+
function compareIcuPlaceholders(sourceValue, targetValue) {
|
|
1809
|
+
try {
|
|
1810
|
+
const sourceAst = icuMessageformatParser.parse(sourceValue);
|
|
1811
|
+
const targetAst = icuMessageformatParser.parse(targetValue);
|
|
1812
|
+
return compareElements(sourceAst, targetAst);
|
|
1813
|
+
} catch {
|
|
1814
|
+
return checkPlaceholders(icuPlaceholders(sourceValue), icuPlaceholders(targetValue));
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1265
1817
|
var AdapterError = class extends Error {
|
|
1266
1818
|
code;
|
|
1267
1819
|
constructor(code, message) {
|
|
@@ -1563,9 +2115,9 @@ function unflattenEntries(entries) {
|
|
|
1563
2115
|
}
|
|
1564
2116
|
return root;
|
|
1565
2117
|
}
|
|
1566
|
-
function toEntries(content, namespace,
|
|
2118
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree) {
|
|
1567
2119
|
try {
|
|
1568
|
-
const tree =
|
|
2120
|
+
const tree = parse3(content);
|
|
1569
2121
|
validateTree?.(tree);
|
|
1570
2122
|
return flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
1571
2123
|
} catch (error) {
|
|
@@ -1577,7 +2129,7 @@ function createTreeFileAdapter(options) {
|
|
|
1577
2129
|
format,
|
|
1578
2130
|
extensions,
|
|
1579
2131
|
sniff,
|
|
1580
|
-
parse:
|
|
2132
|
+
parse: parse3,
|
|
1581
2133
|
serialize,
|
|
1582
2134
|
deriveEntry,
|
|
1583
2135
|
extractPlaceholders,
|
|
@@ -1585,6 +2137,7 @@ function createTreeFileAdapter(options) {
|
|
|
1585
2137
|
validateMessage,
|
|
1586
2138
|
validateTree,
|
|
1587
2139
|
buildWriteTree,
|
|
2140
|
+
comparePlaceholders,
|
|
1588
2141
|
keyMode = "literal-leaf"
|
|
1589
2142
|
} = options;
|
|
1590
2143
|
return {
|
|
@@ -1592,10 +2145,11 @@ function createTreeFileAdapter(options) {
|
|
|
1592
2145
|
canHandle: buildCanHandle(extensions, sniff),
|
|
1593
2146
|
extractPlaceholders,
|
|
1594
2147
|
validateMessage: validateMessage ?? (() => true),
|
|
2148
|
+
...comparePlaceholders !== void 0 ? { comparePlaceholders } : {},
|
|
1595
2149
|
async read(filePath, locale) {
|
|
1596
2150
|
const content = await readFileContent(filePath);
|
|
1597
2151
|
const namespace = namespaceOf(filePath);
|
|
1598
|
-
const entries = toEntries(content, namespace,
|
|
2152
|
+
const entries = toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree);
|
|
1599
2153
|
const resource = { locale, namespace, format, entries };
|
|
1600
2154
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1601
2155
|
return { resource, invalidIcuKeys };
|
|
@@ -1691,6 +2245,7 @@ function createArbAdapter() {
|
|
|
1691
2245
|
deriveEntry: icuDeriveEntry,
|
|
1692
2246
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1693
2247
|
validateMessage: icuIsValid,
|
|
2248
|
+
comparePlaceholders: compareIcuPlaceholders,
|
|
1694
2249
|
buildWriteTree: buildArbWriteTree
|
|
1695
2250
|
});
|
|
1696
2251
|
}
|
|
@@ -1754,7 +2309,8 @@ function createNextIntlJsonAdapter() {
|
|
|
1754
2309
|
extractPlaceholders: icuPlaceholders,
|
|
1755
2310
|
deriveEntry: icuDeriveEntry,
|
|
1756
2311
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1757
|
-
validateMessage: icuIsValid
|
|
2312
|
+
validateMessage: icuIsValid,
|
|
2313
|
+
comparePlaceholders: compareIcuPlaceholders
|
|
1758
2314
|
});
|
|
1759
2315
|
}
|
|
1760
2316
|
function assertNoDottedNestedKey(tree) {
|
|
@@ -1956,6 +2512,7 @@ function extractXliffPlaceholders(value) {
|
|
|
1956
2512
|
return result;
|
|
1957
2513
|
}
|
|
1958
2514
|
var ELEMENT_NODE = 1;
|
|
2515
|
+
var TEXT_NODE = 3;
|
|
1959
2516
|
function isElement(node) {
|
|
1960
2517
|
return node.nodeType === ELEMENT_NODE;
|
|
1961
2518
|
}
|
|
@@ -2046,6 +2603,12 @@ function parseXliffEntries(content, namespace) {
|
|
|
2046
2603
|
const serializer = new xmldom.XMLSerializer();
|
|
2047
2604
|
const out = /* @__PURE__ */ new Map();
|
|
2048
2605
|
for (const unit of walkUnits(root)) {
|
|
2606
|
+
if (out.has(unit.key)) {
|
|
2607
|
+
throw new AdapterError(
|
|
2608
|
+
"INVALID_STRUCTURE",
|
|
2609
|
+
"The XLIFF file has two trans-units with the same id."
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2049
2612
|
const value = unitValue(serializer, unit);
|
|
2050
2613
|
out.set(unit.key, {
|
|
2051
2614
|
key: unit.key,
|
|
@@ -2072,10 +2635,67 @@ async function readDestination(filePath) {
|
|
|
2072
2635
|
}
|
|
2073
2636
|
return outcome.content;
|
|
2074
2637
|
}
|
|
2638
|
+
var XLIFF_INLINE_ELEMENTS = /* @__PURE__ */ new Set(["x", "g", "bx", "ex", "ph", "it", "mrk"]);
|
|
2639
|
+
var XLIFF_NAMESPACES = /* @__PURE__ */ new Set([
|
|
2640
|
+
"urn:oasis:names:tc:xliff:document:1.2",
|
|
2641
|
+
"urn:oasis:names:tc:xliff:document:2.0"
|
|
2642
|
+
]);
|
|
2643
|
+
var INLINE_ELEMENT_ATTRIBUTES = {
|
|
2644
|
+
x: ["id", "ctype"],
|
|
2645
|
+
ph: ["id", "ctype"],
|
|
2646
|
+
g: ["id", "ctype"],
|
|
2647
|
+
bx: ["id", "rid", "ctype"],
|
|
2648
|
+
ex: ["id", "rid"],
|
|
2649
|
+
it: ["id", "pos", "ctype"],
|
|
2650
|
+
mrk: ["id", "mtype"]
|
|
2651
|
+
};
|
|
2652
|
+
function isAllowedInlineElement(element) {
|
|
2653
|
+
const namespace = element.namespaceURI;
|
|
2654
|
+
const hasAllowedNamespace = namespace === null || XLIFF_NAMESPACES.has(namespace);
|
|
2655
|
+
return hasAllowedNamespace && XLIFF_INLINE_ELEMENTS.has(element.localName ?? "");
|
|
2656
|
+
}
|
|
2657
|
+
function isAllowedFragmentNode(node) {
|
|
2658
|
+
if (node.nodeType === TEXT_NODE) {
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
return isElement(node) && isAllowedInlineElement(node);
|
|
2662
|
+
}
|
|
2663
|
+
function allDescendantNodes(node) {
|
|
2664
|
+
const children = Array.from(node.childNodes);
|
|
2665
|
+
return children.flatMap((child) => [child, ...allDescendantNodes(child)]);
|
|
2666
|
+
}
|
|
2667
|
+
function hasDisallowedNode(root) {
|
|
2668
|
+
return allDescendantNodes(root).some((node) => !isAllowedFragmentNode(node));
|
|
2669
|
+
}
|
|
2670
|
+
function attributeNames(element) {
|
|
2671
|
+
const names = [];
|
|
2672
|
+
for (let i = 0; i < element.attributes.length; i += 1) {
|
|
2673
|
+
const attr = element.attributes.item(i);
|
|
2674
|
+
if (attr !== null) {
|
|
2675
|
+
names.push(attr.name);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
return names;
|
|
2679
|
+
}
|
|
2680
|
+
function sanitizeInlineAttributes(root) {
|
|
2681
|
+
for (const el of collectByTag(root, "*")) {
|
|
2682
|
+
const allowed = INLINE_ELEMENT_ATTRIBUTES[el.localName ?? ""] ?? [];
|
|
2683
|
+
for (const name of attributeNames(el)) {
|
|
2684
|
+
if (!allowed.includes(name)) {
|
|
2685
|
+
el.removeAttribute(name);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2075
2690
|
function fragmentNodes(parser, value) {
|
|
2691
|
+
assertNoDoctype(value);
|
|
2076
2692
|
try {
|
|
2077
2693
|
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
2078
|
-
|
|
2694
|
+
if (root === null || hasDisallowedNode(root)) {
|
|
2695
|
+
return null;
|
|
2696
|
+
}
|
|
2697
|
+
sanitizeInlineAttributes(root);
|
|
2698
|
+
return Array.from(root.childNodes);
|
|
2079
2699
|
} catch {
|
|
2080
2700
|
return null;
|
|
2081
2701
|
}
|
|
@@ -2254,6 +2874,40 @@ async function diff(input, deps = {}) {
|
|
|
2254
2874
|
return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
|
|
2255
2875
|
}
|
|
2256
2876
|
|
|
2877
|
+
// src/flow/lock-state.ts
|
|
2878
|
+
function toLockLocaleState(locale, keyCount, diff2) {
|
|
2879
|
+
return {
|
|
2880
|
+
locale,
|
|
2881
|
+
keyCount,
|
|
2882
|
+
missing: diff2.missing.length,
|
|
2883
|
+
stale: diff2.changed.length,
|
|
2884
|
+
upToDate: diff2.unchanged.length
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
async function lockState(input, deps = {}) {
|
|
2888
|
+
const config = input.config;
|
|
2889
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2890
|
+
const fs = deps.fs ?? defaultFs;
|
|
2891
|
+
const locales = selectLocales(config, input.locales);
|
|
2892
|
+
const path = lockFilePath(cwd);
|
|
2893
|
+
const exists = await fs.fileExists(path);
|
|
2894
|
+
if (!exists) {
|
|
2895
|
+
return { exists: false };
|
|
2896
|
+
}
|
|
2897
|
+
const lock = await readLockFile(path, fs);
|
|
2898
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
2899
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
2900
|
+
const localeStates = await Promise.all(
|
|
2901
|
+
locales.map(async (locale) => {
|
|
2902
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
2903
|
+
const baseline = baselineFor(lock, locale);
|
|
2904
|
+
const diff2 = diffResources(source.resource, target, { baseline });
|
|
2905
|
+
return toLockLocaleState(locale, baseline.size, diff2);
|
|
2906
|
+
})
|
|
2907
|
+
);
|
|
2908
|
+
return { exists: true, version: lock.version, locales: localeStates };
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2257
2911
|
// src/selection/select-provider.ts
|
|
2258
2912
|
function selectProvider(config, createProvider = buildProvider) {
|
|
2259
2913
|
try {
|
|
@@ -2495,7 +3149,8 @@ function buildRequest2(context, entries) {
|
|
|
2495
3149
|
entries,
|
|
2496
3150
|
extractPlaceholders: context.adapter.extractPlaceholders,
|
|
2497
3151
|
...context.glossary !== void 0 ? { glossary: context.glossary } : {},
|
|
2498
|
-
...context.tone !== void 0 ? { tone: context.tone } : {}
|
|
3152
|
+
...context.tone !== void 0 ? { tone: context.tone } : {},
|
|
3153
|
+
...context.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: context.adapter.comparePlaceholders } : {}
|
|
2499
3154
|
};
|
|
2500
3155
|
}
|
|
2501
3156
|
async function generatePluralForms(context) {
|
|
@@ -2565,7 +3220,8 @@ function buildRequest3(params, entries) {
|
|
|
2565
3220
|
entries,
|
|
2566
3221
|
extractPlaceholders: params.adapter.extractPlaceholders,
|
|
2567
3222
|
...params.glossary !== void 0 ? { glossary: params.glossary } : {},
|
|
2568
|
-
...params.tone !== void 0 ? { tone: params.tone } : {}
|
|
3223
|
+
...params.tone !== void 0 ? { tone: params.tone } : {},
|
|
3224
|
+
...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
|
|
2569
3225
|
};
|
|
2570
3226
|
}
|
|
2571
3227
|
async function runLocale(params) {
|
|
@@ -3028,7 +3684,7 @@ async function streamEntryBounded(file, remaining) {
|
|
|
3028
3684
|
"The workbook decompresses to more than the permitted maximum bytes."
|
|
3029
3685
|
);
|
|
3030
3686
|
}
|
|
3031
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
3687
|
+
return { raw, content: Buffer.concat(chunks).toString("utf8") };
|
|
3032
3688
|
}
|
|
3033
3689
|
async function guardWorkbookBytes(bytes, limits) {
|
|
3034
3690
|
let zip;
|
|
@@ -3060,14 +3716,8 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
3060
3716
|
let actualTotal = 0;
|
|
3061
3717
|
for (const file of files) {
|
|
3062
3718
|
const remaining = limits.maxDecompressedBytes - actualTotal;
|
|
3063
|
-
const content = await streamEntryBounded(file, remaining);
|
|
3064
|
-
actualTotal +=
|
|
3065
|
-
if (actualTotal > limits.maxDecompressedBytes) {
|
|
3066
|
-
throw new ExchangeError(
|
|
3067
|
-
"WORKBOOK_INVALID",
|
|
3068
|
-
`The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
|
|
3069
|
-
);
|
|
3070
|
-
}
|
|
3719
|
+
const { raw, content } = await streamEntryBounded(file, remaining);
|
|
3720
|
+
actualTotal += raw;
|
|
3071
3721
|
assertNoDoctype2(file.name, content);
|
|
3072
3722
|
}
|
|
3073
3723
|
}
|
|
@@ -3256,10 +3906,7 @@ function judge(row, sourceEntry, adapter) {
|
|
|
3256
3906
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
3257
3907
|
return "drift";
|
|
3258
3908
|
}
|
|
3259
|
-
const integrity = checkPlaceholders(
|
|
3260
|
-
sourceEntry.placeholders,
|
|
3261
|
-
adapter.extractPlaceholders(row.translation)
|
|
3262
|
-
);
|
|
3909
|
+
const integrity = adapter.comparePlaceholders !== void 0 ? adapter.comparePlaceholders(sourceEntry.value, row.translation) : checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(row.translation));
|
|
3263
3910
|
if (!integrity.matches) {
|
|
3264
3911
|
return "placeholder";
|
|
3265
3912
|
}
|
|
@@ -3459,6 +4106,13 @@ async function importWorkbook(input, deps = {}) {
|
|
|
3459
4106
|
return { dryRun, locales: summaries, succeeded, failed };
|
|
3460
4107
|
}
|
|
3461
4108
|
|
|
4109
|
+
// src/lock/load-lock-file.ts
|
|
4110
|
+
async function loadLockFile(input = {}, deps = {}) {
|
|
4111
|
+
const cwd = input.cwd ?? process.cwd();
|
|
4112
|
+
const fs = deps.fs ?? defaultFs;
|
|
4113
|
+
return readLockFile(lockFilePath(cwd), fs);
|
|
4114
|
+
}
|
|
4115
|
+
|
|
3462
4116
|
// src/scaffolding.ts
|
|
3463
4117
|
var scaffoldingMetadata = {
|
|
3464
4118
|
/** Provider id -> the environment variable its API key is read from. Owned by ai-providers. */
|
|
@@ -3575,6 +4229,7 @@ async function watch(input, deps = {}) {
|
|
|
3575
4229
|
}
|
|
3576
4230
|
|
|
3577
4231
|
exports.DEFAULT_WORKBOOK_PATH = DEFAULT_WORKBOOK_PATH;
|
|
4232
|
+
exports.LOCK_FILE_NAME = LOCK_FILE_NAME;
|
|
3578
4233
|
exports.SdkError = SdkError;
|
|
3579
4234
|
exports.check = check;
|
|
3580
4235
|
exports.defineConfig = defineConfig;
|
|
@@ -3582,6 +4237,9 @@ exports.diff = diff;
|
|
|
3582
4237
|
exports.exportWorkbook = exportWorkbook;
|
|
3583
4238
|
exports.importWorkbook = importWorkbook;
|
|
3584
4239
|
exports.loadConfig = loadConfig;
|
|
4240
|
+
exports.loadConfigWithMeta = loadConfigWithMeta;
|
|
4241
|
+
exports.loadLockFile = loadLockFile;
|
|
4242
|
+
exports.lockState = lockState;
|
|
3585
4243
|
exports.scaffoldingMetadata = scaffoldingMetadata;
|
|
3586
4244
|
exports.translate = translate2;
|
|
3587
4245
|
exports.verbatraConfigSchema = verbatraConfigSchema;
|