@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.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
|
-
import { resolve, join,
|
|
2
|
+
import { dirname, resolve, join, basename, extname } from 'path';
|
|
3
3
|
import { cosmiconfig } from 'cosmiconfig';
|
|
4
4
|
import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import { access, writeFile, rename, rm, open } from 'fs/promises';
|
|
5
7
|
import { z } from 'zod';
|
|
6
8
|
import Anthropic from '@anthropic-ai/sdk';
|
|
7
9
|
import * as deepl from 'deepl-node';
|
|
@@ -9,11 +11,9 @@ import { createRequire } from 'module';
|
|
|
9
11
|
import log from 'loglevel';
|
|
10
12
|
import { GoogleGenAI } from '@google/genai';
|
|
11
13
|
import OpenAI from 'openai';
|
|
12
|
-
import {
|
|
13
|
-
import { access, writeFile, rename, rm, open } from 'fs/promises';
|
|
14
|
-
import { parse as parse$1, TYPE } from '@formatjs/icu-messageformat-parser';
|
|
14
|
+
import { parse, TYPE } from '@formatjs/icu-messageformat-parser';
|
|
15
15
|
import { XMLSerializer, DOMParser } from '@xmldom/xmldom';
|
|
16
|
-
import { stringify, parse } from 'yaml';
|
|
16
|
+
import { stringify, parse as parse$1 } from 'yaml';
|
|
17
17
|
import ExcelJS from 'exceljs';
|
|
18
18
|
import { Readable } from 'stream';
|
|
19
19
|
import JSZip from 'jszip';
|
|
@@ -38,6 +38,155 @@ var SdkError = class extends Error {
|
|
|
38
38
|
this.code = code;
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
|
+
async function readBoundedUtf8(handle, size) {
|
|
42
|
+
const buffer = Buffer.allocUnsafe(size);
|
|
43
|
+
let offset = 0;
|
|
44
|
+
while (offset < size) {
|
|
45
|
+
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
46
|
+
if (bytesRead === 0) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
offset += bytesRead;
|
|
50
|
+
}
|
|
51
|
+
return buffer.toString("utf8", 0, offset);
|
|
52
|
+
}
|
|
53
|
+
async function readBounded(path, maxBytes) {
|
|
54
|
+
let handle;
|
|
55
|
+
try {
|
|
56
|
+
handle = await open(path, "r");
|
|
57
|
+
} catch {
|
|
58
|
+
return { kind: "missing" };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const info = await handle.stat();
|
|
62
|
+
if (!info.isFile()) {
|
|
63
|
+
return { kind: "missing" };
|
|
64
|
+
}
|
|
65
|
+
if (info.size > maxBytes) {
|
|
66
|
+
return { kind: "too-large" };
|
|
67
|
+
}
|
|
68
|
+
return { kind: "ok", content: await readBoundedUtf8(handle, info.size) };
|
|
69
|
+
} finally {
|
|
70
|
+
await handle.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function readBoundedBytesInto(handle, size) {
|
|
74
|
+
const buffer = Buffer.allocUnsafeSlow(size);
|
|
75
|
+
let offset = 0;
|
|
76
|
+
while (offset < size) {
|
|
77
|
+
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
78
|
+
if (bytesRead === 0) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
offset += bytesRead;
|
|
82
|
+
}
|
|
83
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, offset);
|
|
84
|
+
}
|
|
85
|
+
async function readBoundedBytes(path, maxBytes) {
|
|
86
|
+
let handle;
|
|
87
|
+
try {
|
|
88
|
+
handle = await open(path, "r");
|
|
89
|
+
} catch {
|
|
90
|
+
return { kind: "missing" };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const info = await handle.stat();
|
|
94
|
+
if (!info.isFile()) {
|
|
95
|
+
return { kind: "missing" };
|
|
96
|
+
}
|
|
97
|
+
if (info.size > maxBytes) {
|
|
98
|
+
return { kind: "too-large" };
|
|
99
|
+
}
|
|
100
|
+
return { kind: "ok", bytes: await readBoundedBytesInto(handle, info.size) };
|
|
101
|
+
} finally {
|
|
102
|
+
await handle.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function tempFileName(path) {
|
|
106
|
+
return join(dirname(path), `.${basename(path)}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`);
|
|
107
|
+
}
|
|
108
|
+
async function atomicWrite(path, data) {
|
|
109
|
+
const tmp = tempFileName(path);
|
|
110
|
+
await (typeof data === "string" ? writeFile(tmp, data, "utf8") : writeFile(tmp, data));
|
|
111
|
+
try {
|
|
112
|
+
await rename(tmp, path);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
await rm(tmp, { force: true });
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
var defaultFs = {
|
|
119
|
+
async fileExists(path) {
|
|
120
|
+
try {
|
|
121
|
+
await access(path);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
|
|
128
|
+
readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
|
|
129
|
+
writeFile: (path, data) => atomicWrite(path, data),
|
|
130
|
+
writeBytes: (path, data) => atomicWrite(path, data)
|
|
131
|
+
};
|
|
132
|
+
var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
|
|
133
|
+
var glossaryRecordSchema = z.record(z.string(), z.string());
|
|
134
|
+
var BOM = "\uFEFF";
|
|
135
|
+
var REPLACEMENT_CHARACTER = "\uFFFD";
|
|
136
|
+
var NUL = "\0";
|
|
137
|
+
function stripBom(content) {
|
|
138
|
+
return content.startsWith(BOM) ? content.slice(BOM.length) : content;
|
|
139
|
+
}
|
|
140
|
+
function looksLikeInvalidEncoding(content) {
|
|
141
|
+
return content.startsWith(REPLACEMENT_CHARACTER) || content.includes(NUL);
|
|
142
|
+
}
|
|
143
|
+
async function readGlossaryFile(resolvedPath, fs) {
|
|
144
|
+
const read = await fs.readFileBounded(resolvedPath, MAX_GLOSSARY_FILE_BYTES);
|
|
145
|
+
if (read.kind === "missing") {
|
|
146
|
+
throw new SdkError(
|
|
147
|
+
"CONFIG_INVALID",
|
|
148
|
+
`The glossary file at ${resolvedPath} was not found or could not be read.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (read.kind === "too-large") {
|
|
152
|
+
throw new SdkError(
|
|
153
|
+
"CONFIG_INVALID",
|
|
154
|
+
`The glossary file at ${resolvedPath} exceeds the maximum allowed size of ${MAX_GLOSSARY_FILE_BYTES} bytes.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const content = stripBom(read.content);
|
|
158
|
+
if (looksLikeInvalidEncoding(content)) {
|
|
159
|
+
throw new SdkError(
|
|
160
|
+
"CONFIG_INVALID",
|
|
161
|
+
`The glossary file at ${resolvedPath} must be UTF-8 encoded.`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(content);
|
|
167
|
+
} catch {
|
|
168
|
+
throw new SdkError("CONFIG_INVALID", `The glossary file at ${resolvedPath} is not valid JSON.`);
|
|
169
|
+
}
|
|
170
|
+
const result = glossaryRecordSchema.safeParse(parsed);
|
|
171
|
+
if (!result.success) {
|
|
172
|
+
throw new SdkError(
|
|
173
|
+
"CONFIG_INVALID",
|
|
174
|
+
`The glossary file at ${resolvedPath} must contain a flat object of string keys to string values.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return result.data;
|
|
178
|
+
}
|
|
179
|
+
async function resolveGlossary(glossary, baseDir, fs) {
|
|
180
|
+
if (glossary === void 0) {
|
|
181
|
+
return { glossary: void 0, provenance: { source: "none" } };
|
|
182
|
+
}
|
|
183
|
+
if (typeof glossary === "string") {
|
|
184
|
+
const resolvedPath = resolve(baseDir, glossary);
|
|
185
|
+
const record = await readGlossaryFile(resolvedPath, fs);
|
|
186
|
+
return { glossary: record, provenance: { source: "file", path: resolvedPath } };
|
|
187
|
+
}
|
|
188
|
+
return { glossary, provenance: { source: "inline" } };
|
|
189
|
+
}
|
|
41
190
|
var FNV_OFFSET_BASIS = 14695981039346656037n;
|
|
42
191
|
var FNV_PRIME = 1099511628211n;
|
|
43
192
|
var U64_MASK = (1n << 64n) - 1n;
|
|
@@ -162,6 +311,85 @@ var LOCALE_TOKEN = "{locale}";
|
|
|
162
311
|
function localeFilePath(cwd, pattern, locale) {
|
|
163
312
|
return resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
|
|
164
313
|
}
|
|
314
|
+
var RATE_LIMITED_STATUS = 429;
|
|
315
|
+
var TIMEOUT_STATUS = 408;
|
|
316
|
+
var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
317
|
+
var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
318
|
+
"RateLimitError",
|
|
319
|
+
// openai, @anthropic-ai/sdk
|
|
320
|
+
"TooManyRequestsError"
|
|
321
|
+
// deepl-node
|
|
322
|
+
]);
|
|
323
|
+
var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
324
|
+
"AuthenticationError",
|
|
325
|
+
// openai, @anthropic-ai/sdk (401)
|
|
326
|
+
"PermissionDeniedError",
|
|
327
|
+
// openai, @anthropic-ai/sdk (403)
|
|
328
|
+
"AuthorizationError"
|
|
329
|
+
// deepl-node
|
|
330
|
+
]);
|
|
331
|
+
var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
332
|
+
"APIConnectionTimeoutError",
|
|
333
|
+
// openai, @anthropic-ai/sdk
|
|
334
|
+
// deepl-node has no distinct timeout class; ConnectionError is its only network-failure
|
|
335
|
+
// class, so it is folded into TIMEOUT rather than the PROVIDER_ERROR fallback.
|
|
336
|
+
"ConnectionError"
|
|
337
|
+
]);
|
|
338
|
+
var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
339
|
+
"APIUserAbortError"
|
|
340
|
+
// openai, @anthropic-ai/sdk
|
|
341
|
+
]);
|
|
342
|
+
function getErrorStatus(error) {
|
|
343
|
+
if (typeof error !== "object" || error === null || !("status" in error)) {
|
|
344
|
+
return void 0;
|
|
345
|
+
}
|
|
346
|
+
const status = error.status;
|
|
347
|
+
return typeof status === "number" ? status : void 0;
|
|
348
|
+
}
|
|
349
|
+
function readClassName(error) {
|
|
350
|
+
if (typeof error !== "object" || error === null) {
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
return error.constructor?.name;
|
|
354
|
+
}
|
|
355
|
+
function classifyProviderError(error) {
|
|
356
|
+
const status = getErrorStatus(error);
|
|
357
|
+
if (status === RATE_LIMITED_STATUS) {
|
|
358
|
+
return "RATE_LIMITED";
|
|
359
|
+
}
|
|
360
|
+
if (status !== void 0 && AUTH_FAILED_STATUSES.has(status)) {
|
|
361
|
+
return "AUTH_FAILED";
|
|
362
|
+
}
|
|
363
|
+
if (status === TIMEOUT_STATUS) {
|
|
364
|
+
return "TIMEOUT";
|
|
365
|
+
}
|
|
366
|
+
const className = readClassName(error);
|
|
367
|
+
if (className !== void 0) {
|
|
368
|
+
if (RATE_LIMITED_CLASS_NAMES.has(className)) {
|
|
369
|
+
return "RATE_LIMITED";
|
|
370
|
+
}
|
|
371
|
+
if (AUTH_FAILED_CLASS_NAMES.has(className)) {
|
|
372
|
+
return "AUTH_FAILED";
|
|
373
|
+
}
|
|
374
|
+
if (TIMEOUT_CLASS_NAMES.has(className)) {
|
|
375
|
+
return "TIMEOUT";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return "PROVIDER_ERROR";
|
|
379
|
+
}
|
|
380
|
+
function isAbortShapedError(error) {
|
|
381
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
const className = readClassName(error);
|
|
385
|
+
return className !== void 0 && ABORT_ERROR_CLASS_NAMES.has(className);
|
|
386
|
+
}
|
|
387
|
+
function isAbortError(error, signal) {
|
|
388
|
+
if (signal !== void 0) {
|
|
389
|
+
return signal.aborted === true && isAbortShapedError(error);
|
|
390
|
+
}
|
|
391
|
+
return isAbortShapedError(error);
|
|
392
|
+
}
|
|
165
393
|
var REDACTED = "[REDACTED]";
|
|
166
394
|
var KEY_PATTERNS = [
|
|
167
395
|
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
@@ -196,17 +424,34 @@ var ProviderError = class extends Error {
|
|
|
196
424
|
}
|
|
197
425
|
};
|
|
198
426
|
var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
|
|
199
|
-
|
|
427
|
+
var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
|
|
428
|
+
var TIMEOUT_MESSAGE = "The translation provider request timed out.";
|
|
429
|
+
var AUTH_FAILED_MESSAGE = "The translation provider rejected the request credentials.";
|
|
430
|
+
var MESSAGE_BY_CODE = {
|
|
431
|
+
RATE_LIMITED: RATE_LIMITED_MESSAGE,
|
|
432
|
+
TIMEOUT: TIMEOUT_MESSAGE,
|
|
433
|
+
AUTH_FAILED: AUTH_FAILED_MESSAGE,
|
|
434
|
+
PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
|
|
435
|
+
};
|
|
436
|
+
async function guardProviderCall(call, signal) {
|
|
437
|
+
signal?.throwIfAborted();
|
|
200
438
|
try {
|
|
201
439
|
return await call();
|
|
202
|
-
} catch {
|
|
203
|
-
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (isAbortError(error, signal)) {
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
const code = classifyProviderError(error);
|
|
445
|
+
throw new ProviderError(code, MESSAGE_BY_CODE[code]);
|
|
204
446
|
}
|
|
205
447
|
}
|
|
206
|
-
function checkBatchIntegrity(inputs, extract) {
|
|
448
|
+
function checkBatchIntegrity(inputs, extract, compare) {
|
|
207
449
|
const integrity = /* @__PURE__ */ new Map();
|
|
208
|
-
for (const { key,
|
|
209
|
-
integrity.set(
|
|
450
|
+
for (const { key, sourceValue, translatedValue } of inputs) {
|
|
451
|
+
integrity.set(
|
|
452
|
+
key,
|
|
453
|
+
compare !== void 0 ? compare(sourceValue, translatedValue) : checkPlaceholders(extract(sourceValue), extract(translatedValue))
|
|
454
|
+
);
|
|
210
455
|
}
|
|
211
456
|
return integrity;
|
|
212
457
|
}
|
|
@@ -236,7 +481,7 @@ function toIntegrityInputs(entries, values) {
|
|
|
236
481
|
"The provider response is missing one or more keys."
|
|
237
482
|
);
|
|
238
483
|
}
|
|
239
|
-
return { key: entry.key,
|
|
484
|
+
return { key: entry.key, sourceValue: entry.value, translatedValue };
|
|
240
485
|
});
|
|
241
486
|
}
|
|
242
487
|
function toItem(entry) {
|
|
@@ -303,11 +548,16 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
303
548
|
const data = validateRequest(request);
|
|
304
549
|
const payloadJson = JSON.stringify(buildDataPayload(data));
|
|
305
550
|
const requestedKeys = data.entries.map((entry) => entry.key);
|
|
306
|
-
const completion = await mechanism.translate({
|
|
551
|
+
const completion = await mechanism.translate({
|
|
552
|
+
payloadJson,
|
|
553
|
+
requestedKeys,
|
|
554
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
555
|
+
});
|
|
307
556
|
const values = reconcileResult(completion.raw, requestedKeys);
|
|
308
557
|
const integrity = checkBatchIntegrity(
|
|
309
558
|
toIntegrityInputs(data.entries, values),
|
|
310
|
-
request.extractPlaceholders
|
|
559
|
+
request.extractPlaceholders,
|
|
560
|
+
request.comparePlaceholders
|
|
311
561
|
);
|
|
312
562
|
return completion.usage === void 0 ? { values, integrity } : { values, integrity, usage: completion.usage };
|
|
313
563
|
}
|
|
@@ -342,12 +592,29 @@ function requireGeminiKey() {
|
|
|
342
592
|
function requireDeepLKey() {
|
|
343
593
|
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
344
594
|
}
|
|
595
|
+
var OPENAI_COMPATIBLE_ENV_VAR = "OPENAI_COMPATIBLE_API_KEY";
|
|
596
|
+
var OPENAI_COMPATIBLE_KEY_PLACEHOLDER = "local";
|
|
597
|
+
function resolveOpenAiCompatibleKey(customEnvVar) {
|
|
598
|
+
const varName = customEnvVar ?? OPENAI_COMPATIBLE_ENV_VAR;
|
|
599
|
+
const value = process.env[varName];
|
|
600
|
+
if (value !== void 0 && value.length > 0) {
|
|
601
|
+
return value;
|
|
602
|
+
}
|
|
603
|
+
if (customEnvVar !== void 0) {
|
|
604
|
+
throw new ProviderError(
|
|
605
|
+
"MISSING_API_KEY",
|
|
606
|
+
`The ${customEnvVar} environment variable is not set.`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return OPENAI_COMPATIBLE_KEY_PLACEHOLDER;
|
|
610
|
+
}
|
|
345
611
|
function createDefaultClient() {
|
|
346
612
|
const sdk = new Anthropic({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
347
613
|
return {
|
|
348
614
|
messages: {
|
|
349
|
-
create: async (body) => await sdk.messages.create(
|
|
350
|
-
body
|
|
615
|
+
create: async (body, options) => await sdk.messages.create(
|
|
616
|
+
body,
|
|
617
|
+
options
|
|
351
618
|
)
|
|
352
619
|
}
|
|
353
620
|
};
|
|
@@ -415,9 +682,9 @@ function createAnthropicProvider(config, deps = {}) {
|
|
|
415
682
|
}
|
|
416
683
|
function createMechanism(client, config) {
|
|
417
684
|
return {
|
|
418
|
-
translate: async ({ payloadJson }) => {
|
|
685
|
+
translate: async ({ payloadJson, signal }) => {
|
|
419
686
|
const body = buildRequest(config, payloadJson);
|
|
420
|
-
const message = await callClient(client, body);
|
|
687
|
+
const message = await callClient(client, body, signal);
|
|
421
688
|
assertNotTruncated(message.stop_reason === "max_tokens");
|
|
422
689
|
const raw = requireToolInput(message.content);
|
|
423
690
|
const usage = toUsage(message.usage);
|
|
@@ -425,8 +692,11 @@ function createMechanism(client, config) {
|
|
|
425
692
|
}
|
|
426
693
|
};
|
|
427
694
|
}
|
|
428
|
-
function callClient(client, body) {
|
|
429
|
-
return guardProviderCall(
|
|
695
|
+
function callClient(client, body, signal) {
|
|
696
|
+
return guardProviderCall(
|
|
697
|
+
() => client.messages.create(body, signal !== void 0 ? { signal } : void 0),
|
|
698
|
+
signal
|
|
699
|
+
);
|
|
430
700
|
}
|
|
431
701
|
function toUsage(usage) {
|
|
432
702
|
if (usage === void 0) {
|
|
@@ -521,7 +791,7 @@ function zipResults(entries, results) {
|
|
|
521
791
|
values.set(entry.key, translatedValue);
|
|
522
792
|
integrityInputs.push({
|
|
523
793
|
key: entry.key,
|
|
524
|
-
|
|
794
|
+
sourceValue: entry.value,
|
|
525
795
|
translatedValue
|
|
526
796
|
});
|
|
527
797
|
}
|
|
@@ -562,36 +832,97 @@ async function translate(bundle, config, request) {
|
|
|
562
832
|
data,
|
|
563
833
|
protectable,
|
|
564
834
|
options,
|
|
565
|
-
request.extractPlaceholders
|
|
835
|
+
request.extractPlaceholders,
|
|
836
|
+
request.comparePlaceholders,
|
|
837
|
+
request.signal
|
|
566
838
|
);
|
|
567
839
|
if (unprotectable.length > 0) {
|
|
568
840
|
notices.push({ code: "PLACEHOLDER_UNSUPPORTED", message: PLACEHOLDER_UNSUPPORTED_MESSAGE });
|
|
569
841
|
}
|
|
570
842
|
return { values, integrity, notices };
|
|
571
843
|
}
|
|
572
|
-
async function translateProtectable(client, data, protectable, options, extract) {
|
|
844
|
+
async function translateProtectable(client, data, protectable, options, extract, compare, signal) {
|
|
573
845
|
if (protectable.length === 0) {
|
|
574
846
|
return { values: /* @__PURE__ */ new Map(), integrity: /* @__PURE__ */ new Map() };
|
|
575
847
|
}
|
|
576
848
|
const texts = protectable.map((entry) => entry.value);
|
|
577
|
-
const results = await callClient2(
|
|
849
|
+
const results = await callClient2(
|
|
850
|
+
client,
|
|
851
|
+
texts,
|
|
852
|
+
data.sourceLocale,
|
|
853
|
+
data.targetLocale,
|
|
854
|
+
options,
|
|
855
|
+
signal
|
|
856
|
+
);
|
|
578
857
|
const { values, integrityInputs } = zipResults(protectable, results);
|
|
579
|
-
const integrity = checkBatchIntegrity(integrityInputs, extract);
|
|
858
|
+
const integrity = checkBatchIntegrity(integrityInputs, extract, compare);
|
|
580
859
|
return { values, integrity };
|
|
581
860
|
}
|
|
582
|
-
function callClient2(client, texts, sourceLang, targetLang, options) {
|
|
583
|
-
return guardProviderCall(
|
|
861
|
+
function callClient2(client, texts, sourceLang, targetLang, options, signal) {
|
|
862
|
+
return guardProviderCall(
|
|
863
|
+
() => client.translateText(texts, sourceLang, targetLang, options),
|
|
864
|
+
signal
|
|
865
|
+
);
|
|
584
866
|
}
|
|
585
867
|
var geminiConfigSchema = z.object({
|
|
586
868
|
model: z.string().min(1),
|
|
587
869
|
maxOutputTokens: z.number().int().positive()
|
|
588
870
|
});
|
|
871
|
+
var DEFAULT_GEMINI_RETRY = { attempts: 3, baseDelayMs: 250 };
|
|
872
|
+
function isRetryableStatus(error) {
|
|
873
|
+
const status = getErrorStatus(error);
|
|
874
|
+
return status === 429 || status !== void 0 && status >= 500 && status < 600;
|
|
875
|
+
}
|
|
876
|
+
function isAborted(signal) {
|
|
877
|
+
return signal?.aborted === true;
|
|
878
|
+
}
|
|
879
|
+
function throwAbort(signal) {
|
|
880
|
+
signal?.throwIfAborted();
|
|
881
|
+
throw new DOMException("This operation was aborted.", "AbortError");
|
|
882
|
+
}
|
|
883
|
+
function delay(ms, signal) {
|
|
884
|
+
return new Promise((resolve7) => {
|
|
885
|
+
const onAbort = () => {
|
|
886
|
+
clearTimeout(timer);
|
|
887
|
+
resolve7();
|
|
888
|
+
};
|
|
889
|
+
const timer = setTimeout(() => {
|
|
890
|
+
signal?.removeEventListener("abort", onAbort);
|
|
891
|
+
resolve7();
|
|
892
|
+
}, ms);
|
|
893
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
async function withGeminiRetry(call, signal, config = DEFAULT_GEMINI_RETRY) {
|
|
897
|
+
let attempt = 1;
|
|
898
|
+
while (true) {
|
|
899
|
+
try {
|
|
900
|
+
return await call();
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (isAborted(signal)) {
|
|
903
|
+
throwAbort(signal);
|
|
904
|
+
}
|
|
905
|
+
const exhausted = attempt >= config.attempts;
|
|
906
|
+
if (exhausted || !isRetryableStatus(error)) {
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
await delay(config.baseDelayMs * 2 ** (attempt - 1), signal);
|
|
910
|
+
if (isAborted(signal)) {
|
|
911
|
+
throwAbort(signal);
|
|
912
|
+
}
|
|
913
|
+
attempt += 1;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
589
917
|
function createDefaultClient3() {
|
|
590
918
|
const ai = new GoogleGenAI({ apiKey: requireGeminiKey() });
|
|
591
919
|
return {
|
|
592
920
|
models: {
|
|
593
|
-
generateContent:
|
|
594
|
-
|
|
921
|
+
generateContent: (request) => withGeminiRetry(
|
|
922
|
+
async () => await ai.models.generateContent(
|
|
923
|
+
request
|
|
924
|
+
),
|
|
925
|
+
request.config.abortSignal
|
|
595
926
|
)
|
|
596
927
|
}
|
|
597
928
|
};
|
|
@@ -655,7 +986,7 @@ var GEMINI_SYSTEM_RULES = [
|
|
|
655
986
|
"When a tone is provided, honor it.",
|
|
656
987
|
"Respond only with the structured object: exactly one entry per requested key, no commentary, no extra keys, and no key that was not requested."
|
|
657
988
|
].join("\n");
|
|
658
|
-
function buildGeminiRequest(config, payloadJson) {
|
|
989
|
+
function buildGeminiRequest(config, payloadJson, signal) {
|
|
659
990
|
return {
|
|
660
991
|
model: config.model,
|
|
661
992
|
contents: [{ role: "user", parts: [{ text: payloadJson }] }],
|
|
@@ -663,7 +994,8 @@ function buildGeminiRequest(config, payloadJson) {
|
|
|
663
994
|
systemInstruction: GEMINI_SYSTEM_RULES,
|
|
664
995
|
responseMimeType: "application/json",
|
|
665
996
|
responseSchema: toGeminiSchema(deriveJsonSchema(translationsResultSchema)),
|
|
666
|
-
maxOutputTokens: config.maxOutputTokens
|
|
997
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
998
|
+
...signal !== void 0 ? { abortSignal: signal } : {}
|
|
667
999
|
}
|
|
668
1000
|
};
|
|
669
1001
|
}
|
|
@@ -727,15 +1059,15 @@ function createGeminiProvider(config, deps = {}) {
|
|
|
727
1059
|
}
|
|
728
1060
|
function createMechanism2(client, config) {
|
|
729
1061
|
return {
|
|
730
|
-
translate: async ({ payloadJson }) => {
|
|
731
|
-
const request = buildGeminiRequest(config, payloadJson);
|
|
732
|
-
const response = await callClient3(client, request);
|
|
1062
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1063
|
+
const request = buildGeminiRequest(config, payloadJson, signal);
|
|
1064
|
+
const response = await callClient3(client, request, signal);
|
|
733
1065
|
return extractGeminiResult(response);
|
|
734
1066
|
}
|
|
735
1067
|
};
|
|
736
1068
|
}
|
|
737
|
-
function callClient3(client, request) {
|
|
738
|
-
return guardProviderCall(() => client.models.generateContent(request));
|
|
1069
|
+
function callClient3(client, request, signal) {
|
|
1070
|
+
return guardProviderCall(() => client.models.generateContent(request), signal);
|
|
739
1071
|
}
|
|
740
1072
|
var openAiConfigSchema = z.object({
|
|
741
1073
|
model: z.string().min(1),
|
|
@@ -746,8 +1078,9 @@ function createDefaultClient4() {
|
|
|
746
1078
|
return {
|
|
747
1079
|
chat: {
|
|
748
1080
|
completions: {
|
|
749
|
-
create: async (body) => await sdk.chat.completions.create(
|
|
750
|
-
body
|
|
1081
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1082
|
+
body,
|
|
1083
|
+
options
|
|
751
1084
|
)
|
|
752
1085
|
}
|
|
753
1086
|
}
|
|
@@ -765,7 +1098,20 @@ var OPENAI_SYSTEM_RULES = [
|
|
|
765
1098
|
"When a tone is provided, honor it.",
|
|
766
1099
|
"Respond only with the structured object: exactly one entry per requested key, no commentary, no extra keys, and no key that was not requested."
|
|
767
1100
|
].join("\n");
|
|
768
|
-
function
|
|
1101
|
+
function buildResponseFormat(mode) {
|
|
1102
|
+
if (mode === "json-object") {
|
|
1103
|
+
return { type: "json_object" };
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
type: "json_schema",
|
|
1107
|
+
json_schema: {
|
|
1108
|
+
name: RESULT_SCHEMA_NAME,
|
|
1109
|
+
strict: true,
|
|
1110
|
+
schema: deriveJsonSchema(translationsResultSchema)
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function buildOpenAiRequest(config, payloadJson, mode = "strict-schema") {
|
|
769
1115
|
return {
|
|
770
1116
|
model: config.model,
|
|
771
1117
|
max_completion_tokens: config.maxOutputTokens,
|
|
@@ -773,19 +1119,66 @@ function buildOpenAiRequest(config, payloadJson) {
|
|
|
773
1119
|
{ role: "system", content: OPENAI_SYSTEM_RULES },
|
|
774
1120
|
{ role: "user", content: payloadJson }
|
|
775
1121
|
],
|
|
776
|
-
response_format:
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1122
|
+
response_format: buildResponseFormat(mode)
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function advanceStringScan(char, escaped) {
|
|
1126
|
+
if (escaped) {
|
|
1127
|
+
return { inString: true, escaped: false };
|
|
1128
|
+
}
|
|
1129
|
+
if (char === "\\") {
|
|
1130
|
+
return { inString: true, escaped: true };
|
|
1131
|
+
}
|
|
1132
|
+
return { inString: char !== '"', escaped: false };
|
|
1133
|
+
}
|
|
1134
|
+
function scanBalancedObjectEnd(content, start) {
|
|
1135
|
+
let depth = 0;
|
|
1136
|
+
let inString = false;
|
|
1137
|
+
let escaped = false;
|
|
1138
|
+
for (let i = start; i < content.length; i += 1) {
|
|
1139
|
+
const char = content[i];
|
|
1140
|
+
if (inString) {
|
|
1141
|
+
({ inString, escaped } = advanceStringScan(char, escaped));
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
if (char === '"') {
|
|
1145
|
+
inString = true;
|
|
1146
|
+
} else if (char === "{") {
|
|
1147
|
+
depth += 1;
|
|
1148
|
+
} else if (char === "}") {
|
|
1149
|
+
depth -= 1;
|
|
1150
|
+
if (depth === 0) {
|
|
1151
|
+
return i;
|
|
782
1152
|
}
|
|
783
1153
|
}
|
|
784
|
-
}
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
function extractBalancedJson(content) {
|
|
1158
|
+
let searchFrom = 0;
|
|
1159
|
+
while (searchFrom < content.length) {
|
|
1160
|
+
const start = content.indexOf("{", searchFrom);
|
|
1161
|
+
if (start === -1) {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
const end = scanBalancedObjectEnd(content, start);
|
|
1165
|
+
if (end === null) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const candidate = content.slice(start, end + 1);
|
|
1169
|
+
try {
|
|
1170
|
+
JSON.parse(candidate);
|
|
1171
|
+
return candidate;
|
|
1172
|
+
} catch {
|
|
1173
|
+
searchFrom = end + 1;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return null;
|
|
785
1177
|
}
|
|
786
|
-
function parseContent2(content) {
|
|
1178
|
+
function parseContent2(content, tolerant) {
|
|
1179
|
+
const candidate = tolerant ? extractBalancedJson(content) ?? content : content;
|
|
787
1180
|
try {
|
|
788
|
-
return JSON.parse(
|
|
1181
|
+
return JSON.parse(candidate);
|
|
789
1182
|
} catch {
|
|
790
1183
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned unparseable content.");
|
|
791
1184
|
}
|
|
@@ -800,7 +1193,7 @@ function toUsage3(usage) {
|
|
|
800
1193
|
}
|
|
801
1194
|
return { inputTokens: prompt_tokens, outputTokens: completion_tokens };
|
|
802
1195
|
}
|
|
803
|
-
function extractOpenAiResult(completion) {
|
|
1196
|
+
function extractOpenAiResult(completion, tolerant = false) {
|
|
804
1197
|
const choice = completion.choices[0];
|
|
805
1198
|
if (choice === void 0) {
|
|
806
1199
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no message.");
|
|
@@ -813,7 +1206,7 @@ function extractOpenAiResult(completion) {
|
|
|
813
1206
|
if (message.content === void 0 || message.content === null) {
|
|
814
1207
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no translation content.");
|
|
815
1208
|
}
|
|
816
|
-
const raw = parseContent2(message.content);
|
|
1209
|
+
const raw = parseContent2(message.content, tolerant);
|
|
817
1210
|
const usage = toUsage3(completion.usage);
|
|
818
1211
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
819
1212
|
}
|
|
@@ -831,15 +1224,84 @@ function createOpenAiProvider(config, deps = {}) {
|
|
|
831
1224
|
}
|
|
832
1225
|
function createMechanism3(client, config) {
|
|
833
1226
|
return {
|
|
834
|
-
translate: async ({ payloadJson }) => {
|
|
1227
|
+
translate: async ({ payloadJson, signal }) => {
|
|
835
1228
|
const body = buildOpenAiRequest(config, payloadJson);
|
|
836
|
-
const completion = await callClient4(client, body);
|
|
1229
|
+
const completion = await callClient4(client, body, signal);
|
|
837
1230
|
return extractOpenAiResult(completion);
|
|
838
1231
|
}
|
|
839
1232
|
};
|
|
840
1233
|
}
|
|
841
|
-
function callClient4(client, body) {
|
|
842
|
-
return guardProviderCall(
|
|
1234
|
+
function callClient4(client, body, signal) {
|
|
1235
|
+
return guardProviderCall(
|
|
1236
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1237
|
+
signal
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
var HOSTED_PROVIDER_ENV_VARS = new Set(
|
|
1241
|
+
Object.values(PROVIDER_ENV).map((name) => name.toUpperCase())
|
|
1242
|
+
);
|
|
1243
|
+
function isNotHostedProviderEnvVar(value) {
|
|
1244
|
+
return !HOSTED_PROVIDER_ENV_VARS.has(value.toUpperCase());
|
|
1245
|
+
}
|
|
1246
|
+
function isHttpOrHttpsUrl(value) {
|
|
1247
|
+
try {
|
|
1248
|
+
const { protocol } = new URL(value);
|
|
1249
|
+
return protocol === "http:" || protocol === "https:";
|
|
1250
|
+
} catch {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
var openAiCompatibleConfigSchema = z.object({
|
|
1255
|
+
baseUrl: z.url({ message: "baseUrl must be a valid absolute URL." }).refine(isHttpOrHttpsUrl, { message: "baseUrl must use the http or https scheme." }),
|
|
1256
|
+
model: z.string().min(1),
|
|
1257
|
+
maxOutputTokens: z.number().int().positive(),
|
|
1258
|
+
apiKeyEnvVar: z.string().min(1).refine(isNotHostedProviderEnvVar, {
|
|
1259
|
+
message: "apiKeyEnvVar must not name a hosted provider's environment variable."
|
|
1260
|
+
}).optional()
|
|
1261
|
+
});
|
|
1262
|
+
function createDefaultClient5(config) {
|
|
1263
|
+
const sdk = new OpenAI({
|
|
1264
|
+
apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
|
|
1265
|
+
baseURL: config.baseUrl,
|
|
1266
|
+
logLevel: "off"
|
|
1267
|
+
});
|
|
1268
|
+
return {
|
|
1269
|
+
chat: {
|
|
1270
|
+
completions: {
|
|
1271
|
+
create: async (body, options) => await sdk.chat.completions.create(
|
|
1272
|
+
body,
|
|
1273
|
+
options
|
|
1274
|
+
)
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
var PROVIDER_ID5 = "openai-compatible";
|
|
1280
|
+
function createOpenAiCompatibleProvider(config, deps = {}) {
|
|
1281
|
+
const validConfig = openAiCompatibleConfigSchema.parse(config);
|
|
1282
|
+
const client = deps.client ?? createDefaultClient5(validConfig);
|
|
1283
|
+
const mechanism = createMechanism4(client, validConfig);
|
|
1284
|
+
return {
|
|
1285
|
+
id: PROVIDER_ID5,
|
|
1286
|
+
kind: "llm",
|
|
1287
|
+
supportsGlossary: true,
|
|
1288
|
+
translateBatch: (request) => runLlmTranslation(request, mechanism)
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function createMechanism4(client, config) {
|
|
1292
|
+
return {
|
|
1293
|
+
translate: async ({ payloadJson, signal }) => {
|
|
1294
|
+
const body = buildOpenAiRequest(config, payloadJson, "strict-schema");
|
|
1295
|
+
const completion = await callClient5(client, body, signal);
|
|
1296
|
+
return extractOpenAiResult(completion, true);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function callClient5(client, body, signal) {
|
|
1301
|
+
return guardProviderCall(
|
|
1302
|
+
() => client.chat.completions.create(body, signal !== void 0 ? { signal } : void 0),
|
|
1303
|
+
signal
|
|
1304
|
+
);
|
|
843
1305
|
}
|
|
844
1306
|
var SCAFFOLD_MODELS = {
|
|
845
1307
|
anthropic: "claude-sonnet-4-6",
|
|
@@ -850,13 +1312,18 @@ var providerConfigSchema = z.discriminatedUnion("id", [
|
|
|
850
1312
|
z.object({ id: z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
|
|
851
1313
|
z.object({ id: z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
852
1314
|
z.object({ id: z.literal("gemini"), options: geminiConfigSchema.strict() }),
|
|
853
|
-
z.object({ id: z.literal("deepl"), options: deepLConfigSchema.strict() })
|
|
1315
|
+
z.object({ id: z.literal("deepl"), options: deepLConfigSchema.strict() }),
|
|
1316
|
+
z.object({
|
|
1317
|
+
id: z.literal("openai-compatible"),
|
|
1318
|
+
options: openAiCompatibleConfigSchema.strict()
|
|
1319
|
+
})
|
|
854
1320
|
]);
|
|
855
1321
|
var providerFactories = {
|
|
856
1322
|
anthropic: (options) => createAnthropicProvider(options),
|
|
857
1323
|
openai: (options) => createOpenAiProvider(options),
|
|
858
1324
|
gemini: (options) => createGeminiProvider(options),
|
|
859
|
-
deepl: (options) => createDeepLProvider(options)
|
|
1325
|
+
deepl: (options) => createDeepLProvider(options),
|
|
1326
|
+
"openai-compatible": (options) => createOpenAiCompatibleProvider(options)
|
|
860
1327
|
};
|
|
861
1328
|
function buildProvider(config) {
|
|
862
1329
|
const create = providerFactories[config.id];
|
|
@@ -873,7 +1340,13 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
873
1340
|
pattern: z.string().min(1)
|
|
874
1341
|
}),
|
|
875
1342
|
provider: providerConfigSchema,
|
|
876
|
-
|
|
1343
|
+
/**
|
|
1344
|
+
* A glossary of source terms to preferred target terms, either inline or as a path to a JSON file
|
|
1345
|
+
* carrying the same shape. The two forms are mutually exclusive by type; there is no merge or
|
|
1346
|
+
* precedence between them. A path is resolved and validated by the loader, not by this schema; see
|
|
1347
|
+
* `resolve-glossary.ts`.
|
|
1348
|
+
*/
|
|
1349
|
+
glossary: z.union([z.record(z.string(), z.string()), z.string().min(1)]).optional(),
|
|
877
1350
|
tone: z.enum(["formal", "informal", "neutral"]).optional(),
|
|
878
1351
|
/**
|
|
879
1352
|
* Opt-in orphan pruning, off by default. When true, keys present in a target file but absent from
|
|
@@ -927,7 +1400,7 @@ function formatIssues(error) {
|
|
|
927
1400
|
return issue.code === "unrecognized_keys" ? `${base} (API keys are read from the environment, not the config)` : base;
|
|
928
1401
|
}).join("; ");
|
|
929
1402
|
}
|
|
930
|
-
function
|
|
1403
|
+
function parseConfig(input) {
|
|
931
1404
|
const parsed = verbatraConfigSchema.safeParse(input);
|
|
932
1405
|
if (!parsed.success) {
|
|
933
1406
|
throw new SdkError(
|
|
@@ -937,7 +1410,16 @@ function validate(input) {
|
|
|
937
1410
|
}
|
|
938
1411
|
return parsed.data;
|
|
939
1412
|
}
|
|
940
|
-
async function
|
|
1413
|
+
async function finalizeConfig(parsed, baseDir, fs) {
|
|
1414
|
+
const { glossary: glossaryInput, ...rest } = parsed;
|
|
1415
|
+
const resolved = await resolveGlossary(glossaryInput, baseDir, fs);
|
|
1416
|
+
const config = {
|
|
1417
|
+
...rest,
|
|
1418
|
+
...resolved.glossary !== void 0 ? { glossary: resolved.glossary } : {}
|
|
1419
|
+
};
|
|
1420
|
+
return { config, glossary: resolved.provenance };
|
|
1421
|
+
}
|
|
1422
|
+
async function loadExplicitWithMeta(explorer, configPath, cwd, fs) {
|
|
941
1423
|
const resolved = resolve(cwd ?? process.cwd(), configPath);
|
|
942
1424
|
if (!existsSync(resolved)) {
|
|
943
1425
|
throw new SdkError("CONFIG_NOT_FOUND", `No verbatra configuration file at ${resolved}.`);
|
|
@@ -949,18 +1431,23 @@ async function loadExplicit(explorer, configPath, cwd) {
|
|
|
949
1431
|
const detail = error instanceof Error ? error.message : String(error);
|
|
950
1432
|
throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
|
|
951
1433
|
}
|
|
952
|
-
|
|
1434
|
+
const parsed = parseConfig(result?.config);
|
|
1435
|
+
const { config, glossary } = await finalizeConfig(parsed, dirname(resolved), fs);
|
|
1436
|
+
return { config, source: { kind: "explicit", filepath: resolved }, glossary };
|
|
953
1437
|
}
|
|
954
|
-
async function
|
|
1438
|
+
async function loadConfigWithMeta(options = {}) {
|
|
1439
|
+
const fs = options.fs ?? defaultFs;
|
|
955
1440
|
if (options.configOverride !== void 0) {
|
|
956
|
-
|
|
1441
|
+
const parsed2 = parseConfig(options.configOverride);
|
|
1442
|
+
const { config: config2, glossary: glossary2 } = await finalizeConfig(parsed2, options.cwd ?? process.cwd(), fs);
|
|
1443
|
+
return { config: config2, source: { kind: "override" }, glossary: glossary2 };
|
|
957
1444
|
}
|
|
958
1445
|
const explorer = cosmiconfig(MODULE_NAME, {
|
|
959
1446
|
searchPlaces: SEARCH_PLACES,
|
|
960
1447
|
loaders: { ".ts": TypeScriptLoader() }
|
|
961
1448
|
});
|
|
962
1449
|
if (options.configPath !== void 0) {
|
|
963
|
-
return
|
|
1450
|
+
return loadExplicitWithMeta(explorer, options.configPath, options.cwd, fs);
|
|
964
1451
|
}
|
|
965
1452
|
let result;
|
|
966
1453
|
try {
|
|
@@ -975,99 +1462,14 @@ async function loadConfig(options = {}) {
|
|
|
975
1462
|
"No verbatra configuration found. Create a verbatra.config.ts, a .verbatrarc.json, or a 'verbatra' property in package.json."
|
|
976
1463
|
);
|
|
977
1464
|
}
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
const buffer = Buffer.allocUnsafe(size);
|
|
982
|
-
let offset = 0;
|
|
983
|
-
while (offset < size) {
|
|
984
|
-
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
985
|
-
if (bytesRead === 0) {
|
|
986
|
-
break;
|
|
987
|
-
}
|
|
988
|
-
offset += bytesRead;
|
|
989
|
-
}
|
|
990
|
-
return buffer.toString("utf8", 0, offset);
|
|
991
|
-
}
|
|
992
|
-
async function readBounded(path, maxBytes) {
|
|
993
|
-
let handle;
|
|
994
|
-
try {
|
|
995
|
-
handle = await open(path, "r");
|
|
996
|
-
} catch {
|
|
997
|
-
return { kind: "missing" };
|
|
998
|
-
}
|
|
999
|
-
try {
|
|
1000
|
-
const info = await handle.stat();
|
|
1001
|
-
if (!info.isFile()) {
|
|
1002
|
-
return { kind: "missing" };
|
|
1003
|
-
}
|
|
1004
|
-
if (info.size > maxBytes) {
|
|
1005
|
-
return { kind: "too-large" };
|
|
1006
|
-
}
|
|
1007
|
-
return { kind: "ok", content: await readBoundedUtf8(handle, info.size) };
|
|
1008
|
-
} finally {
|
|
1009
|
-
await handle.close();
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
async function readBoundedBytesInto(handle, size) {
|
|
1013
|
-
const buffer = Buffer.allocUnsafeSlow(size);
|
|
1014
|
-
let offset = 0;
|
|
1015
|
-
while (offset < size) {
|
|
1016
|
-
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
1017
|
-
if (bytesRead === 0) {
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
offset += bytesRead;
|
|
1021
|
-
}
|
|
1022
|
-
return new Uint8Array(buffer.buffer, buffer.byteOffset, offset);
|
|
1023
|
-
}
|
|
1024
|
-
async function readBoundedBytes(path, maxBytes) {
|
|
1025
|
-
let handle;
|
|
1026
|
-
try {
|
|
1027
|
-
handle = await open(path, "r");
|
|
1028
|
-
} catch {
|
|
1029
|
-
return { kind: "missing" };
|
|
1030
|
-
}
|
|
1031
|
-
try {
|
|
1032
|
-
const info = await handle.stat();
|
|
1033
|
-
if (!info.isFile()) {
|
|
1034
|
-
return { kind: "missing" };
|
|
1035
|
-
}
|
|
1036
|
-
if (info.size > maxBytes) {
|
|
1037
|
-
return { kind: "too-large" };
|
|
1038
|
-
}
|
|
1039
|
-
return { kind: "ok", bytes: await readBoundedBytesInto(handle, info.size) };
|
|
1040
|
-
} finally {
|
|
1041
|
-
await handle.close();
|
|
1042
|
-
}
|
|
1465
|
+
const parsed = parseConfig(result.config);
|
|
1466
|
+
const { config, glossary } = await finalizeConfig(parsed, dirname(result.filepath), fs);
|
|
1467
|
+
return { config, source: { kind: "search", filepath: result.filepath }, glossary };
|
|
1043
1468
|
}
|
|
1044
|
-
function
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
async function atomicWrite(path, data) {
|
|
1048
|
-
const tmp = tempFileName(path);
|
|
1049
|
-
await (typeof data === "string" ? writeFile(tmp, data, "utf8") : writeFile(tmp, data));
|
|
1050
|
-
try {
|
|
1051
|
-
await rename(tmp, path);
|
|
1052
|
-
} catch (error) {
|
|
1053
|
-
await rm(tmp, { force: true });
|
|
1054
|
-
throw error;
|
|
1055
|
-
}
|
|
1469
|
+
async function loadConfig(options = {}) {
|
|
1470
|
+
const { config } = await loadConfigWithMeta(options);
|
|
1471
|
+
return config;
|
|
1056
1472
|
}
|
|
1057
|
-
var defaultFs = {
|
|
1058
|
-
async fileExists(path) {
|
|
1059
|
-
try {
|
|
1060
|
-
await access(path);
|
|
1061
|
-
return true;
|
|
1062
|
-
} catch {
|
|
1063
|
-
return false;
|
|
1064
|
-
}
|
|
1065
|
-
},
|
|
1066
|
-
readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
|
|
1067
|
-
readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
|
|
1068
|
-
writeFile: (path, data) => atomicWrite(path, data),
|
|
1069
|
-
writeBytes: (path, data) => atomicWrite(path, data)
|
|
1070
|
-
};
|
|
1071
1473
|
var LOCK_FILE_NAME = "verbatra.lock.json";
|
|
1072
1474
|
var CURRENT_VERSION = 1;
|
|
1073
1475
|
var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
|
|
@@ -1100,6 +1502,12 @@ async function readLockFile(path, fs) {
|
|
|
1100
1502
|
if (!result.success) {
|
|
1101
1503
|
throw new SdkError("LOCK_FILE_INVALID", `The lock-file at ${path} has an unexpected shape.`);
|
|
1102
1504
|
}
|
|
1505
|
+
if (result.data.version !== CURRENT_VERSION) {
|
|
1506
|
+
throw new SdkError(
|
|
1507
|
+
"LOCK_FILE_INVALID",
|
|
1508
|
+
`The lock-file at ${path} has version ${result.data.version}, but this version of verbatra supports version ${CURRENT_VERSION}.`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1103
1511
|
return result.data;
|
|
1104
1512
|
}
|
|
1105
1513
|
function baselineFor(lock, locale) {
|
|
@@ -1205,7 +1613,7 @@ function analyzeIcuValue(value) {
|
|
|
1205
1613
|
return VALID_EMPTY;
|
|
1206
1614
|
}
|
|
1207
1615
|
try {
|
|
1208
|
-
const ast = parse
|
|
1616
|
+
const ast = parse(value);
|
|
1209
1617
|
const state = { isPlural: false };
|
|
1210
1618
|
const multiset = collect(ast, state);
|
|
1211
1619
|
return { placeholders: flatten(multiset), isPlural: state.isPlural, valid: true };
|
|
@@ -1232,6 +1640,150 @@ function icuInvalidKeys(entries) {
|
|
|
1232
1640
|
}
|
|
1233
1641
|
return invalid;
|
|
1234
1642
|
}
|
|
1643
|
+
function tokenOf2(element) {
|
|
1644
|
+
switch (element.type) {
|
|
1645
|
+
case TYPE.argument:
|
|
1646
|
+
case TYPE.number:
|
|
1647
|
+
case TYPE.date:
|
|
1648
|
+
case TYPE.time:
|
|
1649
|
+
case TYPE.select:
|
|
1650
|
+
case TYPE.plural:
|
|
1651
|
+
return `{${element.value}}`;
|
|
1652
|
+
case TYPE.tag:
|
|
1653
|
+
return `<${element.value}>`;
|
|
1654
|
+
default:
|
|
1655
|
+
return void 0;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function isBranching(element) {
|
|
1659
|
+
return element.type === TYPE.plural || element.type === TYPE.select;
|
|
1660
|
+
}
|
|
1661
|
+
function isTag(element) {
|
|
1662
|
+
return element.type === TYPE.tag;
|
|
1663
|
+
}
|
|
1664
|
+
function layerTokens(elements) {
|
|
1665
|
+
const tokens = [];
|
|
1666
|
+
for (const element of elements) {
|
|
1667
|
+
const token = tokenOf2(element);
|
|
1668
|
+
if (token !== void 0) {
|
|
1669
|
+
tokens.push(token);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return tokens;
|
|
1673
|
+
}
|
|
1674
|
+
function deepTokens(elements) {
|
|
1675
|
+
const tokens = [];
|
|
1676
|
+
for (const element of elements) {
|
|
1677
|
+
const token = tokenOf2(element);
|
|
1678
|
+
if (token !== void 0) {
|
|
1679
|
+
tokens.push(token);
|
|
1680
|
+
}
|
|
1681
|
+
if (isTag(element)) {
|
|
1682
|
+
tokens.push(...deepTokens(element.children));
|
|
1683
|
+
} else if (isBranching(element)) {
|
|
1684
|
+
for (const option of Object.values(element.options)) {
|
|
1685
|
+
tokens.push(...deepTokens(option.value));
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return tokens;
|
|
1690
|
+
}
|
|
1691
|
+
function branchesByCategory(element) {
|
|
1692
|
+
return new Map(
|
|
1693
|
+
Object.entries(element.options).map(([category, option]) => [category, option.value])
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
function findUnconsumed(target, consumed, isMatch) {
|
|
1697
|
+
for (let index = 0; index < target.length; index += 1) {
|
|
1698
|
+
if (consumed.has(index)) {
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
const candidate = target[index];
|
|
1702
|
+
if (candidate !== void 0 && isMatch(candidate)) {
|
|
1703
|
+
return { element: candidate, index };
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return void 0;
|
|
1707
|
+
}
|
|
1708
|
+
function findMatchingBranching(source, target, consumed) {
|
|
1709
|
+
return findUnconsumed(
|
|
1710
|
+
target,
|
|
1711
|
+
consumed,
|
|
1712
|
+
(candidate) => isBranching(candidate) && candidate.value === source.value
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
function findMatchingTag(source, target, consumed) {
|
|
1716
|
+
return findUnconsumed(
|
|
1717
|
+
target,
|
|
1718
|
+
consumed,
|
|
1719
|
+
(candidate) => isTag(candidate) && candidate.value === source.value
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
function combineResults(results) {
|
|
1723
|
+
const missing = results.flatMap((result) => result.missing).sort();
|
|
1724
|
+
const extra = results.flatMap((result) => result.extra).sort();
|
|
1725
|
+
return { matches: missing.length === 0 && extra.length === 0, missing, extra, reordered: false };
|
|
1726
|
+
}
|
|
1727
|
+
function compareAgainstSourceUnion(sourceBranches, targetBranch) {
|
|
1728
|
+
const union = /* @__PURE__ */ new Set();
|
|
1729
|
+
for (const branch of sourceBranches.values()) {
|
|
1730
|
+
for (const token of deepTokens(branch)) {
|
|
1731
|
+
union.add(token);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const extra = deepTokens(targetBranch).filter((token) => !union.has(token)).sort();
|
|
1735
|
+
return { matches: extra.length === 0, missing: [], extra, reordered: false };
|
|
1736
|
+
}
|
|
1737
|
+
function compareBranching(source, target) {
|
|
1738
|
+
const sourceBranches = branchesByCategory(source);
|
|
1739
|
+
const targetBranches = branchesByCategory(target);
|
|
1740
|
+
const categories = /* @__PURE__ */ new Set([...sourceBranches.keys(), ...targetBranches.keys()]);
|
|
1741
|
+
const results = [];
|
|
1742
|
+
for (const category of categories) {
|
|
1743
|
+
const sourceBranch = sourceBranches.get(category);
|
|
1744
|
+
const targetBranch = targetBranches.get(category);
|
|
1745
|
+
if (sourceBranch !== void 0 && targetBranch !== void 0) {
|
|
1746
|
+
results.push(compareElements(sourceBranch, targetBranch));
|
|
1747
|
+
} else if (targetBranch !== void 0) {
|
|
1748
|
+
results.push(compareAgainstSourceUnion(sourceBranches, targetBranch));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return combineResults(results);
|
|
1752
|
+
}
|
|
1753
|
+
function nestedResults(source, target) {
|
|
1754
|
+
const results = [];
|
|
1755
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1756
|
+
for (const element of source) {
|
|
1757
|
+
if (isBranching(element)) {
|
|
1758
|
+
const found = findMatchingBranching(element, target, consumed);
|
|
1759
|
+
if (found !== void 0) {
|
|
1760
|
+
consumed.add(found.index);
|
|
1761
|
+
results.push(compareBranching(element, found.element));
|
|
1762
|
+
}
|
|
1763
|
+
} else if (isTag(element)) {
|
|
1764
|
+
const found = findMatchingTag(element, target, consumed);
|
|
1765
|
+
if (found !== void 0) {
|
|
1766
|
+
consumed.add(found.index);
|
|
1767
|
+
results.push(compareElements(element.children, found.element.children));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return results;
|
|
1772
|
+
}
|
|
1773
|
+
function compareElements(source, target) {
|
|
1774
|
+
const layer = checkPlaceholders(layerTokens(source), layerTokens(target));
|
|
1775
|
+
const nested = nestedResults(source, target);
|
|
1776
|
+
return nested.length === 0 ? layer : combineResults([layer, ...nested]);
|
|
1777
|
+
}
|
|
1778
|
+
function compareIcuPlaceholders(sourceValue, targetValue) {
|
|
1779
|
+
try {
|
|
1780
|
+
const sourceAst = parse(sourceValue);
|
|
1781
|
+
const targetAst = parse(targetValue);
|
|
1782
|
+
return compareElements(sourceAst, targetAst);
|
|
1783
|
+
} catch {
|
|
1784
|
+
return checkPlaceholders(icuPlaceholders(sourceValue), icuPlaceholders(targetValue));
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1235
1787
|
var AdapterError = class extends Error {
|
|
1236
1788
|
code;
|
|
1237
1789
|
constructor(code, message) {
|
|
@@ -1533,9 +2085,9 @@ function unflattenEntries(entries) {
|
|
|
1533
2085
|
}
|
|
1534
2086
|
return root;
|
|
1535
2087
|
}
|
|
1536
|
-
function toEntries(content, namespace,
|
|
2088
|
+
function toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree) {
|
|
1537
2089
|
try {
|
|
1538
|
-
const tree =
|
|
2090
|
+
const tree = parse3(content);
|
|
1539
2091
|
validateTree?.(tree);
|
|
1540
2092
|
return flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
1541
2093
|
} catch (error) {
|
|
@@ -1547,7 +2099,7 @@ function createTreeFileAdapter(options) {
|
|
|
1547
2099
|
format,
|
|
1548
2100
|
extensions,
|
|
1549
2101
|
sniff,
|
|
1550
|
-
parse:
|
|
2102
|
+
parse: parse3,
|
|
1551
2103
|
serialize,
|
|
1552
2104
|
deriveEntry,
|
|
1553
2105
|
extractPlaceholders,
|
|
@@ -1555,6 +2107,7 @@ function createTreeFileAdapter(options) {
|
|
|
1555
2107
|
validateMessage,
|
|
1556
2108
|
validateTree,
|
|
1557
2109
|
buildWriteTree,
|
|
2110
|
+
comparePlaceholders,
|
|
1558
2111
|
keyMode = "literal-leaf"
|
|
1559
2112
|
} = options;
|
|
1560
2113
|
return {
|
|
@@ -1562,10 +2115,11 @@ function createTreeFileAdapter(options) {
|
|
|
1562
2115
|
canHandle: buildCanHandle(extensions, sniff),
|
|
1563
2116
|
extractPlaceholders,
|
|
1564
2117
|
validateMessage: validateMessage ?? (() => true),
|
|
2118
|
+
...comparePlaceholders !== void 0 ? { comparePlaceholders } : {},
|
|
1565
2119
|
async read(filePath, locale) {
|
|
1566
2120
|
const content = await readFileContent(filePath);
|
|
1567
2121
|
const namespace = namespaceOf(filePath);
|
|
1568
|
-
const entries = toEntries(content, namespace,
|
|
2122
|
+
const entries = toEntries(content, namespace, parse3, deriveEntry, keyMode, validateTree);
|
|
1569
2123
|
const resource = { locale, namespace, format, entries };
|
|
1570
2124
|
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1571
2125
|
return { resource, invalidIcuKeys };
|
|
@@ -1661,6 +2215,7 @@ function createArbAdapter() {
|
|
|
1661
2215
|
deriveEntry: icuDeriveEntry,
|
|
1662
2216
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1663
2217
|
validateMessage: icuIsValid,
|
|
2218
|
+
comparePlaceholders: compareIcuPlaceholders,
|
|
1664
2219
|
buildWriteTree: buildArbWriteTree
|
|
1665
2220
|
});
|
|
1666
2221
|
}
|
|
@@ -1724,7 +2279,8 @@ function createNextIntlJsonAdapter() {
|
|
|
1724
2279
|
extractPlaceholders: icuPlaceholders,
|
|
1725
2280
|
deriveEntry: icuDeriveEntry,
|
|
1726
2281
|
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1727
|
-
validateMessage: icuIsValid
|
|
2282
|
+
validateMessage: icuIsValid,
|
|
2283
|
+
comparePlaceholders: compareIcuPlaceholders
|
|
1728
2284
|
});
|
|
1729
2285
|
}
|
|
1730
2286
|
function assertNoDottedNestedKey(tree) {
|
|
@@ -1926,6 +2482,7 @@ function extractXliffPlaceholders(value) {
|
|
|
1926
2482
|
return result;
|
|
1927
2483
|
}
|
|
1928
2484
|
var ELEMENT_NODE = 1;
|
|
2485
|
+
var TEXT_NODE = 3;
|
|
1929
2486
|
function isElement(node) {
|
|
1930
2487
|
return node.nodeType === ELEMENT_NODE;
|
|
1931
2488
|
}
|
|
@@ -2016,6 +2573,12 @@ function parseXliffEntries(content, namespace) {
|
|
|
2016
2573
|
const serializer = new XMLSerializer();
|
|
2017
2574
|
const out = /* @__PURE__ */ new Map();
|
|
2018
2575
|
for (const unit of walkUnits(root)) {
|
|
2576
|
+
if (out.has(unit.key)) {
|
|
2577
|
+
throw new AdapterError(
|
|
2578
|
+
"INVALID_STRUCTURE",
|
|
2579
|
+
"The XLIFF file has two trans-units with the same id."
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2019
2582
|
const value = unitValue(serializer, unit);
|
|
2020
2583
|
out.set(unit.key, {
|
|
2021
2584
|
key: unit.key,
|
|
@@ -2042,10 +2605,67 @@ async function readDestination(filePath) {
|
|
|
2042
2605
|
}
|
|
2043
2606
|
return outcome.content;
|
|
2044
2607
|
}
|
|
2608
|
+
var XLIFF_INLINE_ELEMENTS = /* @__PURE__ */ new Set(["x", "g", "bx", "ex", "ph", "it", "mrk"]);
|
|
2609
|
+
var XLIFF_NAMESPACES = /* @__PURE__ */ new Set([
|
|
2610
|
+
"urn:oasis:names:tc:xliff:document:1.2",
|
|
2611
|
+
"urn:oasis:names:tc:xliff:document:2.0"
|
|
2612
|
+
]);
|
|
2613
|
+
var INLINE_ELEMENT_ATTRIBUTES = {
|
|
2614
|
+
x: ["id", "ctype"],
|
|
2615
|
+
ph: ["id", "ctype"],
|
|
2616
|
+
g: ["id", "ctype"],
|
|
2617
|
+
bx: ["id", "rid", "ctype"],
|
|
2618
|
+
ex: ["id", "rid"],
|
|
2619
|
+
it: ["id", "pos", "ctype"],
|
|
2620
|
+
mrk: ["id", "mtype"]
|
|
2621
|
+
};
|
|
2622
|
+
function isAllowedInlineElement(element) {
|
|
2623
|
+
const namespace = element.namespaceURI;
|
|
2624
|
+
const hasAllowedNamespace = namespace === null || XLIFF_NAMESPACES.has(namespace);
|
|
2625
|
+
return hasAllowedNamespace && XLIFF_INLINE_ELEMENTS.has(element.localName ?? "");
|
|
2626
|
+
}
|
|
2627
|
+
function isAllowedFragmentNode(node) {
|
|
2628
|
+
if (node.nodeType === TEXT_NODE) {
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
return isElement(node) && isAllowedInlineElement(node);
|
|
2632
|
+
}
|
|
2633
|
+
function allDescendantNodes(node) {
|
|
2634
|
+
const children = Array.from(node.childNodes);
|
|
2635
|
+
return children.flatMap((child) => [child, ...allDescendantNodes(child)]);
|
|
2636
|
+
}
|
|
2637
|
+
function hasDisallowedNode(root) {
|
|
2638
|
+
return allDescendantNodes(root).some((node) => !isAllowedFragmentNode(node));
|
|
2639
|
+
}
|
|
2640
|
+
function attributeNames(element) {
|
|
2641
|
+
const names = [];
|
|
2642
|
+
for (let i = 0; i < element.attributes.length; i += 1) {
|
|
2643
|
+
const attr = element.attributes.item(i);
|
|
2644
|
+
if (attr !== null) {
|
|
2645
|
+
names.push(attr.name);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
return names;
|
|
2649
|
+
}
|
|
2650
|
+
function sanitizeInlineAttributes(root) {
|
|
2651
|
+
for (const el of collectByTag(root, "*")) {
|
|
2652
|
+
const allowed = INLINE_ELEMENT_ATTRIBUTES[el.localName ?? ""] ?? [];
|
|
2653
|
+
for (const name of attributeNames(el)) {
|
|
2654
|
+
if (!allowed.includes(name)) {
|
|
2655
|
+
el.removeAttribute(name);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2045
2660
|
function fragmentNodes(parser, value) {
|
|
2661
|
+
assertNoDoctype(value);
|
|
2046
2662
|
try {
|
|
2047
2663
|
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
2048
|
-
|
|
2664
|
+
if (root === null || hasDisallowedNode(root)) {
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
sanitizeInlineAttributes(root);
|
|
2668
|
+
return Array.from(root.childNodes);
|
|
2049
2669
|
} catch {
|
|
2050
2670
|
return null;
|
|
2051
2671
|
}
|
|
@@ -2095,7 +2715,7 @@ function createXliffAdapter() {
|
|
|
2095
2715
|
function parseYamlObject(content) {
|
|
2096
2716
|
let parsed;
|
|
2097
2717
|
try {
|
|
2098
|
-
parsed = parse(content, { maxAliasCount: 100 });
|
|
2718
|
+
parsed = parse$1(content, { maxAliasCount: 100 });
|
|
2099
2719
|
} catch {
|
|
2100
2720
|
throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
|
|
2101
2721
|
}
|
|
@@ -2224,6 +2844,40 @@ async function diff(input, deps = {}) {
|
|
|
2224
2844
|
return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
|
|
2225
2845
|
}
|
|
2226
2846
|
|
|
2847
|
+
// src/flow/lock-state.ts
|
|
2848
|
+
function toLockLocaleState(locale, keyCount, diff2) {
|
|
2849
|
+
return {
|
|
2850
|
+
locale,
|
|
2851
|
+
keyCount,
|
|
2852
|
+
missing: diff2.missing.length,
|
|
2853
|
+
stale: diff2.changed.length,
|
|
2854
|
+
upToDate: diff2.unchanged.length
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
async function lockState(input, deps = {}) {
|
|
2858
|
+
const config = input.config;
|
|
2859
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2860
|
+
const fs = deps.fs ?? defaultFs;
|
|
2861
|
+
const locales = selectLocales(config, input.locales);
|
|
2862
|
+
const path = lockFilePath(cwd);
|
|
2863
|
+
const exists = await fs.fileExists(path);
|
|
2864
|
+
if (!exists) {
|
|
2865
|
+
return { exists: false };
|
|
2866
|
+
}
|
|
2867
|
+
const lock = await readLockFile(path, fs);
|
|
2868
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
2869
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
2870
|
+
const localeStates = await Promise.all(
|
|
2871
|
+
locales.map(async (locale) => {
|
|
2872
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
2873
|
+
const baseline = baselineFor(lock, locale);
|
|
2874
|
+
const diff2 = diffResources(source.resource, target, { baseline });
|
|
2875
|
+
return toLockLocaleState(locale, baseline.size, diff2);
|
|
2876
|
+
})
|
|
2877
|
+
);
|
|
2878
|
+
return { exists: true, version: lock.version, locales: localeStates };
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2227
2881
|
// src/selection/select-provider.ts
|
|
2228
2882
|
function selectProvider(config, createProvider = buildProvider) {
|
|
2229
2883
|
try {
|
|
@@ -2465,7 +3119,8 @@ function buildRequest2(context, entries) {
|
|
|
2465
3119
|
entries,
|
|
2466
3120
|
extractPlaceholders: context.adapter.extractPlaceholders,
|
|
2467
3121
|
...context.glossary !== void 0 ? { glossary: context.glossary } : {},
|
|
2468
|
-
...context.tone !== void 0 ? { tone: context.tone } : {}
|
|
3122
|
+
...context.tone !== void 0 ? { tone: context.tone } : {},
|
|
3123
|
+
...context.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: context.adapter.comparePlaceholders } : {}
|
|
2469
3124
|
};
|
|
2470
3125
|
}
|
|
2471
3126
|
async function generatePluralForms(context) {
|
|
@@ -2535,7 +3190,8 @@ function buildRequest3(params, entries) {
|
|
|
2535
3190
|
entries,
|
|
2536
3191
|
extractPlaceholders: params.adapter.extractPlaceholders,
|
|
2537
3192
|
...params.glossary !== void 0 ? { glossary: params.glossary } : {},
|
|
2538
|
-
...params.tone !== void 0 ? { tone: params.tone } : {}
|
|
3193
|
+
...params.tone !== void 0 ? { tone: params.tone } : {},
|
|
3194
|
+
...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
|
|
2539
3195
|
};
|
|
2540
3196
|
}
|
|
2541
3197
|
async function runLocale(params) {
|
|
@@ -2998,7 +3654,7 @@ async function streamEntryBounded(file, remaining) {
|
|
|
2998
3654
|
"The workbook decompresses to more than the permitted maximum bytes."
|
|
2999
3655
|
);
|
|
3000
3656
|
}
|
|
3001
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
3657
|
+
return { raw, content: Buffer.concat(chunks).toString("utf8") };
|
|
3002
3658
|
}
|
|
3003
3659
|
async function guardWorkbookBytes(bytes, limits) {
|
|
3004
3660
|
let zip;
|
|
@@ -3030,14 +3686,8 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
3030
3686
|
let actualTotal = 0;
|
|
3031
3687
|
for (const file of files) {
|
|
3032
3688
|
const remaining = limits.maxDecompressedBytes - actualTotal;
|
|
3033
|
-
const content = await streamEntryBounded(file, remaining);
|
|
3034
|
-
actualTotal +=
|
|
3035
|
-
if (actualTotal > limits.maxDecompressedBytes) {
|
|
3036
|
-
throw new ExchangeError(
|
|
3037
|
-
"WORKBOOK_INVALID",
|
|
3038
|
-
`The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
|
|
3039
|
-
);
|
|
3040
|
-
}
|
|
3689
|
+
const { raw, content } = await streamEntryBounded(file, remaining);
|
|
3690
|
+
actualTotal += raw;
|
|
3041
3691
|
assertNoDoctype2(file.name, content);
|
|
3042
3692
|
}
|
|
3043
3693
|
}
|
|
@@ -3226,10 +3876,7 @@ function judge(row, sourceEntry, adapter) {
|
|
|
3226
3876
|
if (contentHash(sourceEntry) !== row.sourceHash) {
|
|
3227
3877
|
return "drift";
|
|
3228
3878
|
}
|
|
3229
|
-
const integrity = checkPlaceholders(
|
|
3230
|
-
sourceEntry.placeholders,
|
|
3231
|
-
adapter.extractPlaceholders(row.translation)
|
|
3232
|
-
);
|
|
3879
|
+
const integrity = adapter.comparePlaceholders !== void 0 ? adapter.comparePlaceholders(sourceEntry.value, row.translation) : checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(row.translation));
|
|
3233
3880
|
if (!integrity.matches) {
|
|
3234
3881
|
return "placeholder";
|
|
3235
3882
|
}
|
|
@@ -3429,6 +4076,13 @@ async function importWorkbook(input, deps = {}) {
|
|
|
3429
4076
|
return { dryRun, locales: summaries, succeeded, failed };
|
|
3430
4077
|
}
|
|
3431
4078
|
|
|
4079
|
+
// src/lock/load-lock-file.ts
|
|
4080
|
+
async function loadLockFile(input = {}, deps = {}) {
|
|
4081
|
+
const cwd = input.cwd ?? process.cwd();
|
|
4082
|
+
const fs = deps.fs ?? defaultFs;
|
|
4083
|
+
return readLockFile(lockFilePath(cwd), fs);
|
|
4084
|
+
}
|
|
4085
|
+
|
|
3432
4086
|
// src/scaffolding.ts
|
|
3433
4087
|
var scaffoldingMetadata = {
|
|
3434
4088
|
/** Provider id -> the environment variable its API key is read from. Owned by ai-providers. */
|
|
@@ -3544,6 +4198,6 @@ async function watch(input, deps = {}) {
|
|
|
3544
4198
|
return { stop };
|
|
3545
4199
|
}
|
|
3546
4200
|
|
|
3547
|
-
export { DEFAULT_WORKBOOK_PATH, SdkError, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
|
|
4201
|
+
export { DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, loadConfigWithMeta, loadLockFile, lockState, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
|
|
3548
4202
|
//# sourceMappingURL=index.js.map
|
|
3549
4203
|
//# sourceMappingURL=index.js.map
|