@verbatra/sdk 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -5
- package/dist/index.cjs +1244 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +283 -135
- package/dist/index.d.ts +283 -135
- package/dist/index.js +1238 -281
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
package/dist/index.cjs
CHANGED
|
@@ -7,15 +7,20 @@ var cosmiconfigTypescriptLoader = require('cosmiconfig-typescript-loader');
|
|
|
7
7
|
var zod = require('zod');
|
|
8
8
|
var Anthropic = require('@anthropic-ai/sdk');
|
|
9
9
|
var deepl = require('deepl-node');
|
|
10
|
+
var module$1 = require('module');
|
|
10
11
|
var log = require('loglevel');
|
|
11
12
|
var genai = require('@google/genai');
|
|
12
13
|
var OpenAI = require('openai');
|
|
14
|
+
var crypto = require('crypto');
|
|
13
15
|
var promises = require('fs/promises');
|
|
14
16
|
var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
|
|
17
|
+
var xmldom = require('@xmldom/xmldom');
|
|
18
|
+
var yaml = require('yaml');
|
|
15
19
|
var ExcelJS = require('exceljs');
|
|
16
20
|
var JSZip = require('jszip');
|
|
17
21
|
var chokidar = require('chokidar');
|
|
18
22
|
|
|
23
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
19
24
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
20
25
|
|
|
21
26
|
function _interopNamespace(e) {
|
|
@@ -73,13 +78,16 @@ function fnv1a64(input) {
|
|
|
73
78
|
}
|
|
74
79
|
return hash.toString(16).padStart(16, "0");
|
|
75
80
|
}
|
|
81
|
+
function normalizeText(text) {
|
|
82
|
+
return text.normalize("NFC").replace(/\r\n?/g, "\n");
|
|
83
|
+
}
|
|
76
84
|
function canonicalize(entry) {
|
|
77
85
|
return JSON.stringify([
|
|
78
|
-
entry.value,
|
|
79
|
-
entry.description
|
|
80
|
-
entry.meaning
|
|
86
|
+
normalizeText(entry.value),
|
|
87
|
+
entry.description == null ? null : normalizeText(entry.description),
|
|
88
|
+
entry.meaning == null ? null : normalizeText(entry.meaning),
|
|
81
89
|
entry.isPlural,
|
|
82
|
-
[...entry.placeholders].sort()
|
|
90
|
+
[...entry.placeholders].map(normalizeText).sort()
|
|
83
91
|
]);
|
|
84
92
|
}
|
|
85
93
|
function contentHash(entry) {
|
|
@@ -125,7 +133,10 @@ var SUPPORTED_FORMATS = [
|
|
|
125
133
|
"i18next-json",
|
|
126
134
|
"vue-i18n-json",
|
|
127
135
|
"next-intl-json",
|
|
128
|
-
"ngx-translate-json"
|
|
136
|
+
"ngx-translate-json",
|
|
137
|
+
"xliff",
|
|
138
|
+
"yaml",
|
|
139
|
+
"arb"
|
|
129
140
|
];
|
|
130
141
|
var supportedFormatSchema = zod.z.enum(SUPPORTED_FORMATS);
|
|
131
142
|
var translationEntrySchema = zod.z.object({
|
|
@@ -143,17 +154,31 @@ zod.z.object({
|
|
|
143
154
|
format: supportedFormatSchema,
|
|
144
155
|
entries: zod.z.map(zod.z.string(), translationEntrySchema)
|
|
145
156
|
});
|
|
146
|
-
function
|
|
147
|
-
|
|
157
|
+
function counts(items) {
|
|
158
|
+
const map = /* @__PURE__ */ new Map();
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
map.set(item, (map.get(item) ?? 0) + 1);
|
|
161
|
+
}
|
|
162
|
+
return map;
|
|
163
|
+
}
|
|
164
|
+
function multisetExcess(a, b) {
|
|
165
|
+
const excess = [];
|
|
166
|
+
for (const [token, count] of a) {
|
|
167
|
+
const surplus = count - (b.get(token) ?? 0);
|
|
168
|
+
for (let i = 0; i < surplus; i += 1) {
|
|
169
|
+
excess.push(token);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return excess.sort();
|
|
148
173
|
}
|
|
149
174
|
function sameOrder(a, b) {
|
|
150
175
|
return a.length === b.length && a.every((item, index) => item === b[index]);
|
|
151
176
|
}
|
|
152
177
|
function checkPlaceholders(source, translated) {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const missing =
|
|
156
|
-
const extra =
|
|
178
|
+
const sourceCounts = counts(source);
|
|
179
|
+
const translatedCounts = counts(translated);
|
|
180
|
+
const missing = multisetExcess(sourceCounts, translatedCounts);
|
|
181
|
+
const extra = multisetExcess(translatedCounts, sourceCounts);
|
|
157
182
|
const reordered = missing.length === 0 && extra.length === 0 && !sameOrder(source, translated);
|
|
158
183
|
return {
|
|
159
184
|
matches: missing.length === 0 && extra.length === 0 && !reordered,
|
|
@@ -166,6 +191,26 @@ var LOCALE_TOKEN = "{locale}";
|
|
|
166
191
|
function localeFilePath(cwd, pattern, locale) {
|
|
167
192
|
return path.resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
|
|
168
193
|
}
|
|
194
|
+
var REDACTED = "[REDACTED]";
|
|
195
|
+
var KEY_PATTERNS = [
|
|
196
|
+
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
197
|
+
/\bsk-[A-Za-z0-9_-]{8,}/g,
|
|
198
|
+
/AIza[0-9A-Za-z_-]{35}/g,
|
|
199
|
+
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(?::fx)?/g
|
|
200
|
+
];
|
|
201
|
+
function escapeForRegExp(value) {
|
|
202
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
203
|
+
}
|
|
204
|
+
function redact(text, secret = process.env.ANTHROPIC_API_KEY) {
|
|
205
|
+
let out = text;
|
|
206
|
+
for (const pattern of KEY_PATTERNS) {
|
|
207
|
+
out = out.replace(pattern, REDACTED);
|
|
208
|
+
}
|
|
209
|
+
if (secret !== void 0 && secret.length > 0) {
|
|
210
|
+
out = out.replace(new RegExp(escapeForRegExp(secret), "g"), REDACTED);
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
169
214
|
var ProviderError = class extends Error {
|
|
170
215
|
/** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
|
|
171
216
|
code;
|
|
@@ -174,7 +219,7 @@ var ProviderError = class extends Error {
|
|
|
174
219
|
* @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
|
|
175
220
|
*/
|
|
176
221
|
constructor(code, message) {
|
|
177
|
-
super(message);
|
|
222
|
+
super(redact(message, ""));
|
|
178
223
|
this.name = "ProviderError";
|
|
179
224
|
this.code = code;
|
|
180
225
|
}
|
|
@@ -295,6 +340,18 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
295
340
|
);
|
|
296
341
|
return completion.usage === void 0 ? { values, integrity } : { values, integrity, usage: completion.usage };
|
|
297
342
|
}
|
|
343
|
+
var OUTPUT_TRUNCATED_MESSAGE = "The provider stopped because the output-token limit was reached. Reduce the batch size or raise the configured max output tokens.";
|
|
344
|
+
function assertNotTruncated(truncated) {
|
|
345
|
+
if (truncated) {
|
|
346
|
+
throw new ProviderError("OUTPUT_TRUNCATED", OUTPUT_TRUNCATED_MESSAGE);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
var PROVIDER_ENV = {
|
|
350
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
351
|
+
openai: "OPENAI_API_KEY",
|
|
352
|
+
gemini: "GEMINI_API_KEY",
|
|
353
|
+
deepl: "DEEPL_API_KEY"
|
|
354
|
+
};
|
|
298
355
|
function readRequiredEnv(name) {
|
|
299
356
|
const value = process.env[name];
|
|
300
357
|
if (value === void 0 || value.length === 0) {
|
|
@@ -303,16 +360,16 @@ function readRequiredEnv(name) {
|
|
|
303
360
|
return value;
|
|
304
361
|
}
|
|
305
362
|
function requireAnthropicKey() {
|
|
306
|
-
return readRequiredEnv(
|
|
363
|
+
return readRequiredEnv(PROVIDER_ENV.anthropic);
|
|
307
364
|
}
|
|
308
365
|
function requireOpenAiKey() {
|
|
309
|
-
return readRequiredEnv(
|
|
366
|
+
return readRequiredEnv(PROVIDER_ENV.openai);
|
|
310
367
|
}
|
|
311
368
|
function requireGeminiKey() {
|
|
312
|
-
return readRequiredEnv(
|
|
369
|
+
return readRequiredEnv(PROVIDER_ENV.gemini);
|
|
313
370
|
}
|
|
314
371
|
function requireDeepLKey() {
|
|
315
|
-
return readRequiredEnv(
|
|
372
|
+
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
316
373
|
}
|
|
317
374
|
function createDefaultClient() {
|
|
318
375
|
const sdk = new Anthropic__default.default({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
@@ -390,6 +447,7 @@ function createMechanism(client, config) {
|
|
|
390
447
|
translate: async ({ payloadJson }) => {
|
|
391
448
|
const body = buildRequest(config, payloadJson);
|
|
392
449
|
const message = await callClient(client, body);
|
|
450
|
+
assertNotTruncated(message.stop_reason === "max_tokens");
|
|
393
451
|
const raw = requireToolInput(message.content);
|
|
394
452
|
const usage = toUsage(message.usage);
|
|
395
453
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
@@ -412,8 +470,22 @@ function toUsage(usage) {
|
|
|
412
470
|
var deepLConfigSchema = zod.z.object({
|
|
413
471
|
glossaryId: zod.z.string().min(1).optional()
|
|
414
472
|
});
|
|
473
|
+
var DEEPL_LOGGER = "deepl";
|
|
474
|
+
function resolveDeeplLoglevel(requireFn = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
475
|
+
try {
|
|
476
|
+
const entry = requireFn.resolve("deepl-node");
|
|
477
|
+
return module$1.createRequire(entry)("loglevel");
|
|
478
|
+
} catch {
|
|
479
|
+
return void 0;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function silenceDeeplLogger(instances) {
|
|
483
|
+
for (const instance of instances) {
|
|
484
|
+
instance?.getLogger(DEEPL_LOGGER).setLevel("silent");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
415
487
|
function silenceSdkLogging() {
|
|
416
|
-
log__default.default
|
|
488
|
+
silenceDeeplLogger([log__default.default, resolveDeeplLoglevel()]);
|
|
417
489
|
}
|
|
418
490
|
function createDefaultClient2() {
|
|
419
491
|
silenceSdkLogging();
|
|
@@ -637,6 +709,7 @@ function extractGeminiResult(response) {
|
|
|
637
709
|
if (candidate.finishReason !== void 0 && BLOCKED_FINISH_REASONS.has(candidate.finishReason)) {
|
|
638
710
|
throw new ProviderError("PROVIDER_BLOCKED", "The provider filtered the translation response.");
|
|
639
711
|
}
|
|
712
|
+
assertNotTruncated(candidate.finishReason === "MAX_TOKENS");
|
|
640
713
|
const text = response.text;
|
|
641
714
|
if (text === void 0 || text === "") {
|
|
642
715
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no translation content.");
|
|
@@ -733,10 +806,12 @@ function toUsage3(usage) {
|
|
|
733
806
|
return { inputTokens: prompt_tokens, outputTokens: completion_tokens };
|
|
734
807
|
}
|
|
735
808
|
function extractOpenAiResult(completion) {
|
|
736
|
-
const
|
|
737
|
-
if (
|
|
809
|
+
const choice = completion.choices[0];
|
|
810
|
+
if (choice === void 0) {
|
|
738
811
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no message.");
|
|
739
812
|
}
|
|
813
|
+
assertNotTruncated(choice.finish_reason === "length");
|
|
814
|
+
const message = choice.message;
|
|
740
815
|
if (message.refusal !== void 0 && message.refusal !== null && message.refusal !== "") {
|
|
741
816
|
throw new ProviderError("PROVIDER_REFUSED", "The provider refused the translation request.");
|
|
742
817
|
}
|
|
@@ -771,6 +846,11 @@ function createMechanism3(client, config) {
|
|
|
771
846
|
function callClient4(client, body) {
|
|
772
847
|
return guardProviderCall(() => client.chat.completions.create(body));
|
|
773
848
|
}
|
|
849
|
+
var SCAFFOLD_MODELS = {
|
|
850
|
+
anthropic: "claude-sonnet-4-6",
|
|
851
|
+
openai: "gpt-5.4-mini",
|
|
852
|
+
gemini: "gemini-2.5-flash"
|
|
853
|
+
};
|
|
774
854
|
var providerConfigSchema = zod.z.discriminatedUnion("id", [
|
|
775
855
|
zod.z.object({ id: zod.z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
|
|
776
856
|
zod.z.object({ id: zod.z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
@@ -789,6 +869,7 @@ function buildProvider(config) {
|
|
|
789
869
|
}
|
|
790
870
|
|
|
791
871
|
// src/config/schema.ts
|
|
872
|
+
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
792
873
|
var verbatraConfigSchema = zod.z.strictObject({
|
|
793
874
|
sourceLocale: zod.z.string().min(1),
|
|
794
875
|
targetLocales: zod.z.array(zod.z.string().min(1)).min(1),
|
|
@@ -798,7 +879,29 @@ var verbatraConfigSchema = zod.z.strictObject({
|
|
|
798
879
|
}),
|
|
799
880
|
provider: providerConfigSchema,
|
|
800
881
|
glossary: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
801
|
-
tone: zod.z.enum(["formal", "informal", "neutral"]).optional()
|
|
882
|
+
tone: zod.z.enum(["formal", "informal", "neutral"]).optional(),
|
|
883
|
+
/**
|
|
884
|
+
* Opt-in orphan pruning, off by default. When true, keys present in a target file but absent from
|
|
885
|
+
* the source are removed from the written file and the lock. The per-run `prune` option on
|
|
886
|
+
* `translate` (the CLI `--prune` flag) overrides this.
|
|
887
|
+
*/
|
|
888
|
+
prune: zod.z.boolean().optional(),
|
|
889
|
+
/**
|
|
890
|
+
* Opt-in plural-category generation, off by default. When true, and only for an i18next-JSON project
|
|
891
|
+
* translated by an LLM provider, verbatra synthesizes the CLDR plural forms a target language
|
|
892
|
+
* requires but the source does not supply (for example Polish few/many). The per-run
|
|
893
|
+
* `generatePlurals` option on `translate` overrides this. Unsupported cases (DeepL, non-i18next, an
|
|
894
|
+
* unknown language) fall back to the per-locale plural warning.
|
|
895
|
+
*/
|
|
896
|
+
generatePlurals: zod.z.boolean().optional(),
|
|
897
|
+
/**
|
|
898
|
+
* Optional maximum number of entries sent in a single provider request. A locale's missing and
|
|
899
|
+
* changed entries are split into sequential sub-batches no larger than this, so one oversized request
|
|
900
|
+
* cannot sink the whole locale; a failed sub-batch is withheld while the others make progress. Must
|
|
901
|
+
* be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
|
|
902
|
+
* {@link DEFAULT_MAX_BATCH_SIZE} applies.
|
|
903
|
+
*/
|
|
904
|
+
maxBatchSize: zod.z.number().int().positive().optional()
|
|
802
905
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
803
906
|
message: "targetLocales must not include the source locale",
|
|
804
907
|
path: ["targetLocales"]
|
|
@@ -943,11 +1046,14 @@ async function readBoundedBytes(path, maxBytes) {
|
|
|
943
1046
|
await handle.close();
|
|
944
1047
|
}
|
|
945
1048
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1049
|
+
function tempFileName(path$1) {
|
|
1050
|
+
return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
|
|
1051
|
+
}
|
|
1052
|
+
async function atomicWrite(path, data) {
|
|
1053
|
+
const tmp = tempFileName(path);
|
|
948
1054
|
await (typeof data === "string" ? promises.writeFile(tmp, data, "utf8") : promises.writeFile(tmp, data));
|
|
949
1055
|
try {
|
|
950
|
-
await promises.rename(tmp, path
|
|
1056
|
+
await promises.rename(tmp, path);
|
|
951
1057
|
} catch (error) {
|
|
952
1058
|
await promises.rm(tmp, { force: true });
|
|
953
1059
|
throw error;
|
|
@@ -1025,6 +1131,85 @@ async function writeLockFile(path, lock, fs) {
|
|
|
1025
1131
|
await fs.writeFile(path, `${JSON.stringify(ordered, null, 2)}
|
|
1026
1132
|
`);
|
|
1027
1133
|
}
|
|
1134
|
+
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
1135
|
+
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
1136
|
+
function tokenOf(element) {
|
|
1137
|
+
switch (element.type) {
|
|
1138
|
+
case icuMessageformatParser.TYPE.argument:
|
|
1139
|
+
case icuMessageformatParser.TYPE.number:
|
|
1140
|
+
case icuMessageformatParser.TYPE.date:
|
|
1141
|
+
case icuMessageformatParser.TYPE.time:
|
|
1142
|
+
case icuMessageformatParser.TYPE.select:
|
|
1143
|
+
case icuMessageformatParser.TYPE.plural:
|
|
1144
|
+
return `{${element.value}}`;
|
|
1145
|
+
case icuMessageformatParser.TYPE.tag:
|
|
1146
|
+
return `<${element.value}>`;
|
|
1147
|
+
default:
|
|
1148
|
+
return void 0;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function childMessages(element) {
|
|
1152
|
+
if (element.type === icuMessageformatParser.TYPE.plural || element.type === icuMessageformatParser.TYPE.select) {
|
|
1153
|
+
return Object.values(element.options).map((option) => option.value);
|
|
1154
|
+
}
|
|
1155
|
+
if (element.type === icuMessageformatParser.TYPE.tag) {
|
|
1156
|
+
return [element.children];
|
|
1157
|
+
}
|
|
1158
|
+
return [];
|
|
1159
|
+
}
|
|
1160
|
+
function collect(elements, add, state) {
|
|
1161
|
+
for (const element of elements) {
|
|
1162
|
+
const token = tokenOf(element);
|
|
1163
|
+
if (token !== void 0) {
|
|
1164
|
+
add(token);
|
|
1165
|
+
}
|
|
1166
|
+
if (element.type === icuMessageformatParser.TYPE.plural) {
|
|
1167
|
+
state.isPlural = true;
|
|
1168
|
+
}
|
|
1169
|
+
for (const child of childMessages(element)) {
|
|
1170
|
+
collect(child, add, state);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function analyzeIcuValue(value) {
|
|
1175
|
+
if (!value.includes("{") && !value.includes("<")) {
|
|
1176
|
+
return VALID_EMPTY;
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
const ast = icuMessageformatParser.parse(value);
|
|
1180
|
+
const placeholders = [];
|
|
1181
|
+
const state = { isPlural: false };
|
|
1182
|
+
collect(
|
|
1183
|
+
ast,
|
|
1184
|
+
(token) => {
|
|
1185
|
+
placeholders.push(token);
|
|
1186
|
+
},
|
|
1187
|
+
state
|
|
1188
|
+
);
|
|
1189
|
+
return { placeholders, isPlural: state.isPlural, valid: true };
|
|
1190
|
+
} catch {
|
|
1191
|
+
return INVALID;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function icuPlaceholders(value) {
|
|
1195
|
+
return analyzeIcuValue(value).placeholders;
|
|
1196
|
+
}
|
|
1197
|
+
function icuIsValid(value) {
|
|
1198
|
+
return analyzeIcuValue(value).valid;
|
|
1199
|
+
}
|
|
1200
|
+
function icuDeriveEntry(_key, value) {
|
|
1201
|
+
const analysis = analyzeIcuValue(value);
|
|
1202
|
+
return { placeholders: analysis.placeholders, isPlural: analysis.isPlural };
|
|
1203
|
+
}
|
|
1204
|
+
function icuInvalidKeys(entries) {
|
|
1205
|
+
const invalid = [];
|
|
1206
|
+
for (const [key, entry] of entries) {
|
|
1207
|
+
if (!icuIsValid(entry.value)) {
|
|
1208
|
+
invalid.push(key);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return invalid;
|
|
1212
|
+
}
|
|
1028
1213
|
var AdapterError = class extends Error {
|
|
1029
1214
|
code;
|
|
1030
1215
|
constructor(code, message) {
|
|
@@ -1033,6 +1218,82 @@ var AdapterError = class extends Error {
|
|
|
1033
1218
|
this.code = code;
|
|
1034
1219
|
}
|
|
1035
1220
|
};
|
|
1221
|
+
var MAX_DEPTH = 100;
|
|
1222
|
+
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1223
|
+
var jsonTreeSchema = zod.z.lazy(
|
|
1224
|
+
() => zod.z.union([zod.z.string(), zod.z.record(zod.z.string(), jsonTreeSchema)])
|
|
1225
|
+
);
|
|
1226
|
+
var rootSchema = zod.z.record(zod.z.string(), jsonTreeSchema);
|
|
1227
|
+
function assertWithinDepth(value, max) {
|
|
1228
|
+
const stack = [{ node: value, depth: 1 }];
|
|
1229
|
+
while (stack.length > 0) {
|
|
1230
|
+
const top = stack.pop();
|
|
1231
|
+
if (top === void 0) {
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
const { node, depth } = top;
|
|
1235
|
+
if (typeof node !== "object" || node === null) {
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (depth > max) {
|
|
1239
|
+
throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
|
|
1240
|
+
}
|
|
1241
|
+
for (const child of Object.values(node)) {
|
|
1242
|
+
stack.push({ node: child, depth: depth + 1 });
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
function assertJsonRecord(value) {
|
|
1247
|
+
assertWithinDepth(value, MAX_DEPTH);
|
|
1248
|
+
const result = rootSchema.safeParse(value);
|
|
1249
|
+
if (!result.success) {
|
|
1250
|
+
throw new AdapterError(
|
|
1251
|
+
"INVALID_STRUCTURE",
|
|
1252
|
+
"The file is not a valid object (expected nested objects of string values)."
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
return result.data;
|
|
1256
|
+
}
|
|
1257
|
+
function parseJsonObject(content) {
|
|
1258
|
+
let parsed;
|
|
1259
|
+
try {
|
|
1260
|
+
parsed = JSON.parse(content);
|
|
1261
|
+
} catch {
|
|
1262
|
+
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
1263
|
+
}
|
|
1264
|
+
return assertJsonRecord(parsed);
|
|
1265
|
+
}
|
|
1266
|
+
function serializeJsonTree(tree) {
|
|
1267
|
+
return `${JSON.stringify(tree, null, 2)}
|
|
1268
|
+
`;
|
|
1269
|
+
}
|
|
1270
|
+
function namespaceOf(filePath) {
|
|
1271
|
+
return path.basename(filePath, path.extname(filePath));
|
|
1272
|
+
}
|
|
1273
|
+
function rethrowStructured(error, message) {
|
|
1274
|
+
if (error instanceof AdapterError) {
|
|
1275
|
+
throw error;
|
|
1276
|
+
}
|
|
1277
|
+
throw new AdapterError("INVALID_STRUCTURE", message);
|
|
1278
|
+
}
|
|
1279
|
+
function computeIcu(entries, compute) {
|
|
1280
|
+
if (!compute) {
|
|
1281
|
+
return [];
|
|
1282
|
+
}
|
|
1283
|
+
try {
|
|
1284
|
+
return compute(entries);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
rethrowStructured(error, "The file could not be analyzed for message validity.");
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function buildCanHandle(extensions, sniff) {
|
|
1290
|
+
return (filePath, sample) => {
|
|
1291
|
+
if (!extensions.includes(path.extname(filePath).toLowerCase())) {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1036
1297
|
var nodeOps = {
|
|
1037
1298
|
writeFile: (path, data) => promises.writeFile(path, data, "utf8"),
|
|
1038
1299
|
rename: (from, to) => promises.rename(from, to),
|
|
@@ -1044,18 +1305,19 @@ async function cleanup(ops, tmp) {
|
|
|
1044
1305
|
} catch {
|
|
1045
1306
|
}
|
|
1046
1307
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1308
|
+
function tempFileName2(path$1) {
|
|
1309
|
+
return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
|
|
1310
|
+
}
|
|
1311
|
+
async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
1312
|
+
const tmp = tempFileName2(path);
|
|
1049
1313
|
try {
|
|
1050
1314
|
await ops.writeFile(tmp, data);
|
|
1051
|
-
await ops.rename(tmp, path
|
|
1315
|
+
await ops.rename(tmp, path);
|
|
1052
1316
|
} catch (error) {
|
|
1053
1317
|
await cleanup(ops, tmp);
|
|
1054
1318
|
throw error;
|
|
1055
1319
|
}
|
|
1056
1320
|
}
|
|
1057
|
-
var MAX_DEPTH = 100;
|
|
1058
|
-
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1059
1321
|
async function readBoundedUtf82(handle, size) {
|
|
1060
1322
|
const buffer = Buffer.allocUnsafe(size);
|
|
1061
1323
|
let offset = 0;
|
|
@@ -1083,61 +1345,128 @@ async function readBounded2(filePath) {
|
|
|
1083
1345
|
await handle.close();
|
|
1084
1346
|
}
|
|
1085
1347
|
}
|
|
1086
|
-
function
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1348
|
+
async function readFileContent(filePath) {
|
|
1349
|
+
const outcome = await readBounded2(filePath);
|
|
1350
|
+
if (outcome.kind === "not-a-file") {
|
|
1351
|
+
throw new AdapterError("INVALID_STRUCTURE", "The path is not a regular file.");
|
|
1352
|
+
}
|
|
1353
|
+
if (outcome.kind === "too-large") {
|
|
1354
|
+
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
1355
|
+
}
|
|
1356
|
+
return outcome.content;
|
|
1357
|
+
}
|
|
1358
|
+
var BACKSLASH = "\\";
|
|
1359
|
+
var DOT = ".";
|
|
1360
|
+
var ESCAPED_BACKSLASH = "\\\\";
|
|
1361
|
+
var ESCAPED_DOT = "\\.";
|
|
1362
|
+
function needsEncoding(segment) {
|
|
1363
|
+
return segment.includes(BACKSLASH) || segment.includes(DOT);
|
|
1364
|
+
}
|
|
1365
|
+
function encodeSegment(segment) {
|
|
1366
|
+
if (!needsEncoding(segment)) {
|
|
1367
|
+
return segment;
|
|
1368
|
+
}
|
|
1369
|
+
let out = "";
|
|
1370
|
+
for (const char of segment) {
|
|
1371
|
+
if (char === BACKSLASH) {
|
|
1372
|
+
out += ESCAPED_BACKSLASH;
|
|
1373
|
+
} else if (char === DOT) {
|
|
1374
|
+
out += ESCAPED_DOT;
|
|
1092
1375
|
} else {
|
|
1093
|
-
|
|
1376
|
+
out += char;
|
|
1094
1377
|
}
|
|
1095
1378
|
}
|
|
1096
|
-
}
|
|
1097
|
-
function flattenTree(tree, namespace, derive) {
|
|
1098
|
-
const out = /* @__PURE__ */ new Map();
|
|
1099
|
-
addEntries(tree, "", namespace, derive, out);
|
|
1100
1379
|
return out;
|
|
1101
1380
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
if (depth > max) {
|
|
1118
|
-
throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
|
|
1119
|
-
}
|
|
1120
|
-
for (const child of Object.values(node)) {
|
|
1121
|
-
stack.push({ node: child, depth: depth + 1 });
|
|
1381
|
+
function decodeSegment(segment) {
|
|
1382
|
+
if (!segment.includes(BACKSLASH)) {
|
|
1383
|
+
return segment;
|
|
1384
|
+
}
|
|
1385
|
+
let out = "";
|
|
1386
|
+
let escaping = false;
|
|
1387
|
+
for (const char of segment) {
|
|
1388
|
+
if (escaping) {
|
|
1389
|
+
out += char;
|
|
1390
|
+
escaping = false;
|
|
1391
|
+
} else if (char === BACKSLASH) {
|
|
1392
|
+
escaping = true;
|
|
1393
|
+
} else {
|
|
1394
|
+
out += char;
|
|
1122
1395
|
}
|
|
1123
1396
|
}
|
|
1397
|
+
return out;
|
|
1124
1398
|
}
|
|
1125
|
-
function
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1399
|
+
function joinEncodedSegments(segments) {
|
|
1400
|
+
return segments.join(DOT);
|
|
1401
|
+
}
|
|
1402
|
+
function decodeKeyToSegments(key) {
|
|
1403
|
+
if (!key.includes(BACKSLASH)) {
|
|
1404
|
+
return key.split(DOT);
|
|
1405
|
+
}
|
|
1406
|
+
const segments = [];
|
|
1407
|
+
let current = "";
|
|
1408
|
+
let escaping = false;
|
|
1409
|
+
for (const char of key) {
|
|
1410
|
+
if (escaping) {
|
|
1411
|
+
current += BACKSLASH + char;
|
|
1412
|
+
escaping = false;
|
|
1413
|
+
} else if (char === BACKSLASH) {
|
|
1414
|
+
escaping = true;
|
|
1415
|
+
} else if (char === DOT) {
|
|
1416
|
+
segments.push(current);
|
|
1417
|
+
current = "";
|
|
1418
|
+
} else {
|
|
1419
|
+
current += char;
|
|
1420
|
+
}
|
|
1131
1421
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1422
|
+
if (escaping) {
|
|
1423
|
+
current += BACKSLASH;
|
|
1424
|
+
}
|
|
1425
|
+
segments.push(current);
|
|
1426
|
+
return segments.map(decodeSegment);
|
|
1427
|
+
}
|
|
1428
|
+
function addLeaf(ctx, segments, key, value) {
|
|
1429
|
+
const effectivePath = segments.join(".");
|
|
1430
|
+
const mapKey = joinEncodedSegments(segments.map(encodeSegment));
|
|
1431
|
+
if (ctx.claimed.has(effectivePath) && ctx.claimed.get(effectivePath) !== mapKey) {
|
|
1135
1432
|
throw new AdapterError(
|
|
1136
1433
|
"INVALID_STRUCTURE",
|
|
1137
|
-
"
|
|
1434
|
+
"A literal dotted leaf key and a nested key path resolve to the same path."
|
|
1138
1435
|
);
|
|
1139
1436
|
}
|
|
1140
|
-
|
|
1437
|
+
ctx.claimed.set(effectivePath, mapKey);
|
|
1438
|
+
const { placeholders, isPlural } = ctx.derive(key, value);
|
|
1439
|
+
ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
|
|
1440
|
+
}
|
|
1441
|
+
function addEntries(ctx, prefix, node) {
|
|
1442
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1443
|
+
const segments = [...prefix, key];
|
|
1444
|
+
if (typeof value === "string") {
|
|
1445
|
+
addLeaf(ctx, segments, key, value);
|
|
1446
|
+
} else {
|
|
1447
|
+
addEntries(ctx, segments, value);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function addPathEntries(node, prefix, namespace, derive, out) {
|
|
1452
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1453
|
+
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
1454
|
+
if (typeof value === "string") {
|
|
1455
|
+
const { placeholders, isPlural } = derive(key, value);
|
|
1456
|
+
out.set(path, { key: path, namespace, value, placeholders, isPlural });
|
|
1457
|
+
} else {
|
|
1458
|
+
addPathEntries(value, path, namespace, derive, out);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
|
|
1463
|
+
const out = /* @__PURE__ */ new Map();
|
|
1464
|
+
if (keyMode === "path-notation") {
|
|
1465
|
+
addPathEntries(tree, "", namespace, derive, out);
|
|
1466
|
+
return out;
|
|
1467
|
+
}
|
|
1468
|
+
addEntries({ namespace, derive, out, claimed: /* @__PURE__ */ new Map() }, [], tree);
|
|
1469
|
+
return out;
|
|
1141
1470
|
}
|
|
1142
1471
|
function emptyNode() {
|
|
1143
1472
|
return /* @__PURE__ */ Object.create(null);
|
|
@@ -1163,103 +1492,194 @@ function setPath(root, segments, value) {
|
|
|
1163
1492
|
for (const segment of segments.slice(0, -1)) {
|
|
1164
1493
|
node = descend(node, segment);
|
|
1165
1494
|
}
|
|
1495
|
+
if (typeof node[leaf] === "object") {
|
|
1496
|
+
throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
|
|
1497
|
+
}
|
|
1166
1498
|
node[leaf] = value;
|
|
1167
1499
|
}
|
|
1168
1500
|
function unflattenEntries(entries) {
|
|
1169
1501
|
const root = emptyNode();
|
|
1170
1502
|
for (const [key, entry] of entries) {
|
|
1171
|
-
setPath(root, key
|
|
1503
|
+
setPath(root, decodeKeyToSegments(key), entry.value);
|
|
1172
1504
|
}
|
|
1173
1505
|
return root;
|
|
1174
1506
|
}
|
|
1175
|
-
function
|
|
1176
|
-
return path.basename(filePath, path.extname(filePath));
|
|
1177
|
-
}
|
|
1178
|
-
function canHandle(filePath, sample) {
|
|
1179
|
-
if (path.extname(filePath).toLowerCase() !== ".json") {
|
|
1180
|
-
return false;
|
|
1181
|
-
}
|
|
1182
|
-
return sample === void 0 || sample.trimStart().startsWith("{");
|
|
1183
|
-
}
|
|
1184
|
-
function rethrowStructured(error, message) {
|
|
1185
|
-
if (error instanceof AdapterError) {
|
|
1186
|
-
throw error;
|
|
1187
|
-
}
|
|
1188
|
-
throw new AdapterError("INVALID_STRUCTURE", message);
|
|
1189
|
-
}
|
|
1190
|
-
function toEntries(content, namespace, deriveEntry, validateTree) {
|
|
1507
|
+
function toEntries(content, namespace, parse2, deriveEntry, keyMode, validateTree) {
|
|
1191
1508
|
try {
|
|
1192
|
-
const tree =
|
|
1509
|
+
const tree = parse2(content);
|
|
1193
1510
|
validateTree?.(tree);
|
|
1194
|
-
return flattenTree(tree, namespace, deriveEntry);
|
|
1511
|
+
return flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
1195
1512
|
} catch (error) {
|
|
1196
|
-
rethrowStructured(error, "The file could not be
|
|
1513
|
+
rethrowStructured(error, "The file could not be parsed.");
|
|
1197
1514
|
}
|
|
1198
1515
|
}
|
|
1199
|
-
function
|
|
1200
|
-
if (!compute) {
|
|
1201
|
-
return [];
|
|
1202
|
-
}
|
|
1203
|
-
try {
|
|
1204
|
-
return compute(entries);
|
|
1205
|
-
} catch (error) {
|
|
1206
|
-
rethrowStructured(error, "The file could not be analyzed for message validity.");
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
function createJsonFileAdapter(options) {
|
|
1516
|
+
function createTreeFileAdapter(options) {
|
|
1210
1517
|
const {
|
|
1211
1518
|
format,
|
|
1519
|
+
extensions,
|
|
1520
|
+
sniff,
|
|
1521
|
+
parse: parse2,
|
|
1522
|
+
serialize,
|
|
1212
1523
|
deriveEntry,
|
|
1213
|
-
extractPlaceholders
|
|
1214
|
-
computeInvalidIcuKeys
|
|
1215
|
-
validateMessage
|
|
1524
|
+
extractPlaceholders,
|
|
1525
|
+
computeInvalidIcuKeys,
|
|
1526
|
+
validateMessage,
|
|
1216
1527
|
validateTree,
|
|
1217
|
-
buildWriteTree
|
|
1528
|
+
buildWriteTree,
|
|
1529
|
+
keyMode = "literal-leaf"
|
|
1218
1530
|
} = options;
|
|
1219
1531
|
return {
|
|
1220
1532
|
format,
|
|
1221
|
-
canHandle,
|
|
1222
|
-
extractPlaceholders
|
|
1223
|
-
|
|
1224
|
-
validateMessage: validateMessage2 ?? (() => true),
|
|
1533
|
+
canHandle: buildCanHandle(extensions, sniff),
|
|
1534
|
+
extractPlaceholders,
|
|
1535
|
+
validateMessage: validateMessage ?? (() => true),
|
|
1225
1536
|
async read(filePath, locale) {
|
|
1226
|
-
const
|
|
1227
|
-
if (outcome.kind === "not-a-file") {
|
|
1228
|
-
throw new AdapterError("INVALID_STRUCTURE", "The path is not a regular file.");
|
|
1229
|
-
}
|
|
1230
|
-
if (outcome.kind === "too-large") {
|
|
1231
|
-
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
1232
|
-
}
|
|
1537
|
+
const content = await readFileContent(filePath);
|
|
1233
1538
|
const namespace = namespaceOf(filePath);
|
|
1234
|
-
const entries = toEntries(
|
|
1539
|
+
const entries = toEntries(content, namespace, parse2, deriveEntry, keyMode, validateTree);
|
|
1235
1540
|
const resource = { locale, namespace, format, entries };
|
|
1236
|
-
const invalidIcuKeys = computeIcu(entries,
|
|
1541
|
+
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1237
1542
|
return { resource, invalidIcuKeys };
|
|
1238
1543
|
},
|
|
1239
1544
|
async write(resource, filePath) {
|
|
1240
1545
|
const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
|
|
1241
|
-
await atomicWriteFile(filePath,
|
|
1242
|
-
`);
|
|
1546
|
+
await atomicWriteFile(filePath, serialize(tree));
|
|
1243
1547
|
}
|
|
1244
1548
|
};
|
|
1245
1549
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1550
|
+
function isMetadataKey(key) {
|
|
1551
|
+
return key.startsWith("@");
|
|
1552
|
+
}
|
|
1553
|
+
function parseArbObject(content) {
|
|
1554
|
+
let parsed;
|
|
1555
|
+
try {
|
|
1556
|
+
parsed = JSON.parse(content);
|
|
1557
|
+
} catch {
|
|
1558
|
+
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
1559
|
+
}
|
|
1560
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1561
|
+
throw new AdapterError(
|
|
1562
|
+
"INVALID_STRUCTURE",
|
|
1563
|
+
"The file is not a valid object (expected nested objects of string values)."
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
return parsed;
|
|
1567
|
+
}
|
|
1568
|
+
function stripArbMetadata(tree) {
|
|
1569
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1570
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
1571
|
+
if (!isMetadataKey(key)) {
|
|
1572
|
+
out[key] = value;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return out;
|
|
1576
|
+
}
|
|
1577
|
+
function originalKey(encoded) {
|
|
1578
|
+
return decodeKeyToSegments(encoded).join(".");
|
|
1579
|
+
}
|
|
1580
|
+
function messagesFromEntries(entries) {
|
|
1581
|
+
const out = /* @__PURE__ */ new Map();
|
|
1582
|
+
for (const [key, entry] of entries) {
|
|
1583
|
+
out.set(originalKey(key), entry.value);
|
|
1584
|
+
}
|
|
1585
|
+
return out;
|
|
1586
|
+
}
|
|
1587
|
+
async function readDestinationPairs(filePath) {
|
|
1588
|
+
let parsed;
|
|
1589
|
+
try {
|
|
1590
|
+
const outcome = await readBounded2(filePath);
|
|
1591
|
+
if (outcome.kind !== "ok") {
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
parsed = JSON.parse(outcome.content);
|
|
1595
|
+
} catch {
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
return Object.entries(parsed);
|
|
1602
|
+
}
|
|
1603
|
+
async function buildArbWriteTree(entries, filePath) {
|
|
1604
|
+
const messages = messagesFromEntries(entries);
|
|
1605
|
+
const pairs = await readDestinationPairs(filePath);
|
|
1606
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1607
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1608
|
+
for (const [key, value] of pairs ?? []) {
|
|
1609
|
+
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
1610
|
+
if (translated !== void 0) {
|
|
1611
|
+
consumed.add(key);
|
|
1612
|
+
}
|
|
1613
|
+
out[key] = translated ?? value;
|
|
1614
|
+
}
|
|
1615
|
+
for (const [key, value] of messages) {
|
|
1616
|
+
if (!consumed.has(key)) {
|
|
1617
|
+
out[key] = value;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return out;
|
|
1621
|
+
}
|
|
1622
|
+
function parseArb(content) {
|
|
1623
|
+
return assertJsonRecord(stripArbMetadata(parseArbObject(content)));
|
|
1624
|
+
}
|
|
1625
|
+
function createArbAdapter() {
|
|
1626
|
+
return createTreeFileAdapter({
|
|
1627
|
+
format: "arb",
|
|
1628
|
+
extensions: [".arb"],
|
|
1629
|
+
sniff: (sample) => sample.trimStart().startsWith("{"),
|
|
1630
|
+
parse: parseArb,
|
|
1631
|
+
serialize: serializeJsonTree,
|
|
1632
|
+
extractPlaceholders: icuPlaceholders,
|
|
1633
|
+
deriveEntry: icuDeriveEntry,
|
|
1634
|
+
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1635
|
+
validateMessage: icuIsValid,
|
|
1636
|
+
buildWriteTree: buildArbWriteTree
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
function createJsonFileAdapter(options) {
|
|
1640
|
+
return createTreeFileAdapter({
|
|
1641
|
+
...options,
|
|
1642
|
+
extensions: [".json"],
|
|
1643
|
+
sniff: (sample) => sample.trimStart().startsWith("{"),
|
|
1644
|
+
parse: parseJsonObject,
|
|
1645
|
+
serialize: serializeJsonTree
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
var DOUBLE_BRACE_PATTERN = /\{\{[^{}]*\}\}/g;
|
|
1649
|
+
var I18NEXT_PATTERN = /\{\{[^{}]*\}\}|\$t\([^()]*\)/g;
|
|
1650
|
+
function scanTokens(value, pattern) {
|
|
1249
1651
|
const result = [];
|
|
1250
|
-
for (const match of value.matchAll(
|
|
1652
|
+
for (const match of value.matchAll(pattern)) {
|
|
1251
1653
|
const token = match[0];
|
|
1252
|
-
if (token !== void 0
|
|
1253
|
-
seen.add(token);
|
|
1654
|
+
if (token !== void 0) {
|
|
1254
1655
|
result.push(token);
|
|
1255
1656
|
}
|
|
1256
1657
|
}
|
|
1257
1658
|
return result;
|
|
1258
1659
|
}
|
|
1660
|
+
function extractDoubleBracePlaceholders(value) {
|
|
1661
|
+
return scanTokens(value, DOUBLE_BRACE_PATTERN);
|
|
1662
|
+
}
|
|
1663
|
+
function extractI18nextPlaceholders(value) {
|
|
1664
|
+
return scanTokens(value, I18NEXT_PATTERN);
|
|
1665
|
+
}
|
|
1259
1666
|
var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
|
|
1260
1667
|
function isPluralKey(key) {
|
|
1261
1668
|
return PLURAL_SUFFIX.test(key);
|
|
1262
1669
|
}
|
|
1670
|
+
function pluralCategoryOf(key) {
|
|
1671
|
+
const match = PLURAL_SUFFIX.exec(key);
|
|
1672
|
+
return match?.[1];
|
|
1673
|
+
}
|
|
1674
|
+
function pluralBaseKey(key) {
|
|
1675
|
+
if (!isPluralKey(key)) {
|
|
1676
|
+
return void 0;
|
|
1677
|
+
}
|
|
1678
|
+
return key.replace(PLURAL_SUFFIX, "");
|
|
1679
|
+
}
|
|
1680
|
+
function makePluralKey(baseKey, category) {
|
|
1681
|
+
return `${baseKey}_${category}`;
|
|
1682
|
+
}
|
|
1263
1683
|
function createI18nextJsonAdapter() {
|
|
1264
1684
|
return createJsonFileAdapter({
|
|
1265
1685
|
format: "i18next-json",
|
|
@@ -1270,95 +1690,13 @@ function createI18nextJsonAdapter() {
|
|
|
1270
1690
|
})
|
|
1271
1691
|
});
|
|
1272
1692
|
}
|
|
1273
|
-
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
1274
|
-
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
1275
|
-
function tokenOf(element) {
|
|
1276
|
-
switch (element.type) {
|
|
1277
|
-
case icuMessageformatParser.TYPE.argument:
|
|
1278
|
-
case icuMessageformatParser.TYPE.number:
|
|
1279
|
-
case icuMessageformatParser.TYPE.date:
|
|
1280
|
-
case icuMessageformatParser.TYPE.time:
|
|
1281
|
-
case icuMessageformatParser.TYPE.select:
|
|
1282
|
-
case icuMessageformatParser.TYPE.plural:
|
|
1283
|
-
return `{${element.value}}`;
|
|
1284
|
-
case icuMessageformatParser.TYPE.tag:
|
|
1285
|
-
return `<${element.value}>`;
|
|
1286
|
-
default:
|
|
1287
|
-
return void 0;
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
function childMessages(element) {
|
|
1291
|
-
if (element.type === icuMessageformatParser.TYPE.plural || element.type === icuMessageformatParser.TYPE.select) {
|
|
1292
|
-
return Object.values(element.options).map((option) => option.value);
|
|
1293
|
-
}
|
|
1294
|
-
if (element.type === icuMessageformatParser.TYPE.tag) {
|
|
1295
|
-
return [element.children];
|
|
1296
|
-
}
|
|
1297
|
-
return [];
|
|
1298
|
-
}
|
|
1299
|
-
function collect(elements, add, state) {
|
|
1300
|
-
for (const element of elements) {
|
|
1301
|
-
const token = tokenOf(element);
|
|
1302
|
-
if (token !== void 0) {
|
|
1303
|
-
add(token);
|
|
1304
|
-
}
|
|
1305
|
-
if (element.type === icuMessageformatParser.TYPE.plural) {
|
|
1306
|
-
state.isPlural = true;
|
|
1307
|
-
}
|
|
1308
|
-
for (const child of childMessages(element)) {
|
|
1309
|
-
collect(child, add, state);
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
function analyzeIcuValue(value) {
|
|
1314
|
-
if (!value.includes("{") && !value.includes("<")) {
|
|
1315
|
-
return VALID_EMPTY;
|
|
1316
|
-
}
|
|
1317
|
-
try {
|
|
1318
|
-
const ast = icuMessageformatParser.parse(value);
|
|
1319
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1320
|
-
const placeholders = [];
|
|
1321
|
-
const state = { isPlural: false };
|
|
1322
|
-
collect(
|
|
1323
|
-
ast,
|
|
1324
|
-
(token) => {
|
|
1325
|
-
if (!seen.has(token)) {
|
|
1326
|
-
seen.add(token);
|
|
1327
|
-
placeholders.push(token);
|
|
1328
|
-
}
|
|
1329
|
-
},
|
|
1330
|
-
state
|
|
1331
|
-
);
|
|
1332
|
-
return { placeholders, isPlural: state.isPlural, valid: true };
|
|
1333
|
-
} catch {
|
|
1334
|
-
return INVALID;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
function extractPlaceholders(value) {
|
|
1338
|
-
return analyzeIcuValue(value).placeholders;
|
|
1339
|
-
}
|
|
1340
|
-
function validateMessage(value) {
|
|
1341
|
-
return analyzeIcuValue(value).valid;
|
|
1342
|
-
}
|
|
1343
|
-
function computeInvalidIcuKeys(entries) {
|
|
1344
|
-
const invalid = [];
|
|
1345
|
-
for (const [key, entry] of entries) {
|
|
1346
|
-
if (!validateMessage(entry.value)) {
|
|
1347
|
-
invalid.push(key);
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
return invalid;
|
|
1351
|
-
}
|
|
1352
1693
|
function createNextIntlJsonAdapter() {
|
|
1353
1694
|
return createJsonFileAdapter({
|
|
1354
1695
|
format: "next-intl-json",
|
|
1355
|
-
extractPlaceholders,
|
|
1356
|
-
deriveEntry:
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
},
|
|
1360
|
-
computeInvalidIcuKeys,
|
|
1361
|
-
validateMessage
|
|
1696
|
+
extractPlaceholders: icuPlaceholders,
|
|
1697
|
+
deriveEntry: icuDeriveEntry,
|
|
1698
|
+
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1699
|
+
validateMessage: icuIsValid
|
|
1362
1700
|
});
|
|
1363
1701
|
}
|
|
1364
1702
|
function assertNotMixed(tree) {
|
|
@@ -1413,13 +1751,15 @@ async function buildNgxWriteTree(entries, filePath) {
|
|
|
1413
1751
|
function createNgxTranslateJsonAdapter() {
|
|
1414
1752
|
return createJsonFileAdapter({
|
|
1415
1753
|
format: "ngx-translate-json",
|
|
1416
|
-
extractPlaceholders:
|
|
1754
|
+
extractPlaceholders: extractDoubleBracePlaceholders,
|
|
1417
1755
|
deriveEntry: (_key, value) => ({
|
|
1418
|
-
placeholders:
|
|
1756
|
+
placeholders: extractDoubleBracePlaceholders(value),
|
|
1419
1757
|
isPlural: false
|
|
1420
1758
|
}),
|
|
1421
1759
|
validateTree: assertNotMixed,
|
|
1422
|
-
buildWriteTree: buildNgxWriteTree
|
|
1760
|
+
buildWriteTree: buildNgxWriteTree,
|
|
1761
|
+
// ngx-translate flat style uses dotted keys as path notation, not literal leaves.
|
|
1762
|
+
keyMode: "path-notation"
|
|
1423
1763
|
});
|
|
1424
1764
|
}
|
|
1425
1765
|
var AdapterRegistry = class {
|
|
@@ -1470,15 +1810,13 @@ var AdapterRegistry = class {
|
|
|
1470
1810
|
return this.resolveByDetection(filePath, options.sample);
|
|
1471
1811
|
}
|
|
1472
1812
|
};
|
|
1473
|
-
var
|
|
1813
|
+
var PLACEHOLDER_PATTERN = /(?<!\{)\{\s*([A-Za-z_][\w$-]*|\d+)\s*\}(?!\})/g;
|
|
1474
1814
|
function extractVueI18nPlaceholders(value) {
|
|
1475
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1476
1815
|
const result = [];
|
|
1477
|
-
for (const match of value.matchAll(
|
|
1478
|
-
const
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
result.push(token);
|
|
1816
|
+
for (const match of value.matchAll(PLACEHOLDER_PATTERN)) {
|
|
1817
|
+
const key = match[1];
|
|
1818
|
+
if (key !== void 0) {
|
|
1819
|
+
result.push(`{${key}}`);
|
|
1482
1820
|
}
|
|
1483
1821
|
}
|
|
1484
1822
|
return result;
|
|
@@ -1496,8 +1834,245 @@ function createVueI18nJsonAdapter() {
|
|
|
1496
1834
|
})
|
|
1497
1835
|
});
|
|
1498
1836
|
}
|
|
1837
|
+
function toEntries2(content, namespace, parseEntries) {
|
|
1838
|
+
try {
|
|
1839
|
+
return parseEntries(content, namespace);
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
rethrowStructured(error, "The file could not be parsed.");
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function createFlatFileAdapter(options) {
|
|
1845
|
+
const {
|
|
1846
|
+
format,
|
|
1847
|
+
extensions,
|
|
1848
|
+
sniff,
|
|
1849
|
+
parseEntries,
|
|
1850
|
+
serializeEntries,
|
|
1851
|
+
extractPlaceholders,
|
|
1852
|
+
validateMessage,
|
|
1853
|
+
computeInvalidIcuKeys
|
|
1854
|
+
} = options;
|
|
1855
|
+
return {
|
|
1856
|
+
format,
|
|
1857
|
+
canHandle: buildCanHandle(extensions, sniff),
|
|
1858
|
+
extractPlaceholders,
|
|
1859
|
+
validateMessage: validateMessage ?? (() => true),
|
|
1860
|
+
async read(filePath, locale) {
|
|
1861
|
+
const content = await readFileContent(filePath);
|
|
1862
|
+
const namespace = namespaceOf(filePath);
|
|
1863
|
+
const entries = toEntries2(content, namespace, parseEntries);
|
|
1864
|
+
const resource = { locale, namespace, format, entries };
|
|
1865
|
+
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1866
|
+
return { resource, invalidIcuKeys };
|
|
1867
|
+
},
|
|
1868
|
+
async write(resource, filePath) {
|
|
1869
|
+
const data = await serializeEntries(resource.entries, filePath);
|
|
1870
|
+
await atomicWriteFile(filePath, data);
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
var XLIFF_PATTERN = /<(?:x|g|bx|ex|ph|it|mrk)\b[^>]*>|\{[^{}]+\}/g;
|
|
1875
|
+
function extractXliffPlaceholders(value) {
|
|
1876
|
+
const result = [];
|
|
1877
|
+
for (const match of value.matchAll(XLIFF_PATTERN)) {
|
|
1878
|
+
const token = match[0];
|
|
1879
|
+
if (token !== void 0) {
|
|
1880
|
+
result.push(token);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return result;
|
|
1884
|
+
}
|
|
1885
|
+
var ELEMENT_NODE = 1;
|
|
1886
|
+
function isElement(node) {
|
|
1887
|
+
return node.nodeType === ELEMENT_NODE;
|
|
1888
|
+
}
|
|
1889
|
+
function elementChildren(parent) {
|
|
1890
|
+
return Array.from(parent.childNodes).filter(isElement);
|
|
1891
|
+
}
|
|
1892
|
+
function childByName(parent, name) {
|
|
1893
|
+
return elementChildren(parent).find((el) => el.localName === name) ?? null;
|
|
1894
|
+
}
|
|
1895
|
+
function collectByTag(root, name) {
|
|
1896
|
+
return Array.from(root.getElementsByTagName(name));
|
|
1897
|
+
}
|
|
1898
|
+
function unitKey(element, index) {
|
|
1899
|
+
return element.getAttribute("id") ?? element.getAttribute("resname") ?? `unit-${index}`;
|
|
1900
|
+
}
|
|
1901
|
+
function onFatal(level) {
|
|
1902
|
+
if (level === "fatalError") {
|
|
1903
|
+
throw new Error("malformed XML");
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
function assertNoDoctype(content) {
|
|
1907
|
+
if (/<!DOCTYPE/i.test(content) || /<!ENTITY/i.test(content)) {
|
|
1908
|
+
throw new AdapterError("INVALID_XML", "XLIFF with a DTD or entity declaration is rejected.");
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
function parseXml(content) {
|
|
1912
|
+
assertNoDoctype(content);
|
|
1913
|
+
let doc;
|
|
1914
|
+
try {
|
|
1915
|
+
doc = new xmldom.DOMParser({ onError: onFatal }).parseFromString(content, "text/xml");
|
|
1916
|
+
} catch {
|
|
1917
|
+
throw new AdapterError("INVALID_XML", "The file is not valid XML.");
|
|
1918
|
+
}
|
|
1919
|
+
const root = doc.documentElement;
|
|
1920
|
+
if (root === null || root.localName !== "xliff") {
|
|
1921
|
+
throw new AdapterError("INVALID_STRUCTURE", "The file is not an XLIFF document.");
|
|
1922
|
+
}
|
|
1923
|
+
return { doc, root };
|
|
1924
|
+
}
|
|
1925
|
+
function walkXliff12(root) {
|
|
1926
|
+
const units = [];
|
|
1927
|
+
collectByTag(root, "trans-unit").forEach((tu, index) => {
|
|
1928
|
+
const source = childByName(tu, "source");
|
|
1929
|
+
if (source !== null) {
|
|
1930
|
+
units.push({
|
|
1931
|
+
key: unitKey(tu, index),
|
|
1932
|
+
source,
|
|
1933
|
+
target: childByName(tu, "target"),
|
|
1934
|
+
container: tu
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
return units;
|
|
1939
|
+
}
|
|
1940
|
+
function walkXliff20(root) {
|
|
1941
|
+
const units = [];
|
|
1942
|
+
collectByTag(root, "unit").forEach((unit, index) => {
|
|
1943
|
+
const baseKey = unitKey(unit, index);
|
|
1944
|
+
const segments = elementChildren(unit).filter((el) => el.localName === "segment");
|
|
1945
|
+
segments.forEach((segment, segIndex) => {
|
|
1946
|
+
const source = childByName(segment, "source");
|
|
1947
|
+
if (source !== null) {
|
|
1948
|
+
const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
|
|
1949
|
+
units.push({ key, source, target: childByName(segment, "target"), container: segment });
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1953
|
+
return units;
|
|
1954
|
+
}
|
|
1955
|
+
function walkUnits(root) {
|
|
1956
|
+
const version = root.getAttribute("version") ?? "1.2";
|
|
1957
|
+
return version.startsWith("2") ? walkXliff20(root) : walkXliff12(root);
|
|
1958
|
+
}
|
|
1959
|
+
function innerXml(serializer, element) {
|
|
1960
|
+
return Array.from(element.childNodes).map((node) => serializer.serializeToString(node)).join("");
|
|
1961
|
+
}
|
|
1962
|
+
function unitValue(serializer, unit) {
|
|
1963
|
+
if (unit.target !== null) {
|
|
1964
|
+
const targetXml = innerXml(serializer, unit.target);
|
|
1965
|
+
if (targetXml.trim() !== "") {
|
|
1966
|
+
return targetXml;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return innerXml(serializer, unit.source);
|
|
1970
|
+
}
|
|
1971
|
+
function parseXliffEntries(content, namespace) {
|
|
1972
|
+
const { root } = parseXml(content);
|
|
1973
|
+
const serializer = new xmldom.XMLSerializer();
|
|
1974
|
+
const out = /* @__PURE__ */ new Map();
|
|
1975
|
+
for (const unit of walkUnits(root)) {
|
|
1976
|
+
const value = unitValue(serializer, unit);
|
|
1977
|
+
out.set(unit.key, {
|
|
1978
|
+
key: unit.key,
|
|
1979
|
+
namespace,
|
|
1980
|
+
value,
|
|
1981
|
+
placeholders: extractXliffPlaceholders(value),
|
|
1982
|
+
isPlural: false
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
return out;
|
|
1986
|
+
}
|
|
1987
|
+
async function readDestination(filePath) {
|
|
1988
|
+
let outcome;
|
|
1989
|
+
try {
|
|
1990
|
+
outcome = await readBounded2(filePath);
|
|
1991
|
+
} catch {
|
|
1992
|
+
throw new AdapterError("INVALID_STRUCTURE", "The destination XLIFF file does not exist.");
|
|
1993
|
+
}
|
|
1994
|
+
if (outcome.kind === "not-a-file") {
|
|
1995
|
+
throw new AdapterError("INVALID_STRUCTURE", "The destination path is not a regular file.");
|
|
1996
|
+
}
|
|
1997
|
+
if (outcome.kind === "too-large") {
|
|
1998
|
+
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
1999
|
+
}
|
|
2000
|
+
return outcome.content;
|
|
2001
|
+
}
|
|
2002
|
+
function fragmentNodes(parser, value) {
|
|
2003
|
+
try {
|
|
2004
|
+
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
2005
|
+
return root === null ? null : Array.from(root.childNodes);
|
|
2006
|
+
} catch {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
function setTargetValue(doc, parser, element, value) {
|
|
2011
|
+
while (element.firstChild !== null) {
|
|
2012
|
+
element.removeChild(element.firstChild);
|
|
2013
|
+
}
|
|
2014
|
+
const nodes = fragmentNodes(parser, value);
|
|
2015
|
+
if (nodes === null) {
|
|
2016
|
+
element.textContent = value;
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
for (const node of nodes) {
|
|
2020
|
+
element.appendChild(doc.importNode(node, true));
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
async function serializeXliffEntries(entries, filePath) {
|
|
2024
|
+
const { doc, root } = parseXml(await readDestination(filePath));
|
|
2025
|
+
const parser = new xmldom.DOMParser({ onError: onFatal });
|
|
2026
|
+
for (const unit of walkUnits(root)) {
|
|
2027
|
+
const entry = entries.get(unit.key);
|
|
2028
|
+
if (entry !== void 0) {
|
|
2029
|
+
const target = unit.target ?? doc.createElement("target");
|
|
2030
|
+
if (unit.target === null) {
|
|
2031
|
+
unit.container.appendChild(target);
|
|
2032
|
+
}
|
|
2033
|
+
setTargetValue(doc, parser, target, entry.value);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
return new xmldom.XMLSerializer().serializeToString(doc);
|
|
2037
|
+
}
|
|
2038
|
+
function sniffXliff(sample) {
|
|
2039
|
+
const head = sample.trimStart();
|
|
2040
|
+
return head.startsWith("<xliff") || head.startsWith("<?xml");
|
|
2041
|
+
}
|
|
2042
|
+
function createXliffAdapter() {
|
|
2043
|
+
return createFlatFileAdapter({
|
|
2044
|
+
format: "xliff",
|
|
2045
|
+
extensions: [".xlf", ".xliff"],
|
|
2046
|
+
sniff: sniffXliff,
|
|
2047
|
+
parseEntries: parseXliffEntries,
|
|
2048
|
+
serializeEntries: serializeXliffEntries,
|
|
2049
|
+
extractPlaceholders: extractXliffPlaceholders
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
function parseYamlObject(content) {
|
|
2053
|
+
let parsed;
|
|
2054
|
+
try {
|
|
2055
|
+
parsed = yaml.parse(content, { maxAliasCount: 100 });
|
|
2056
|
+
} catch {
|
|
2057
|
+
throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
|
|
2058
|
+
}
|
|
2059
|
+
return assertJsonRecord(parsed);
|
|
2060
|
+
}
|
|
2061
|
+
function createYamlAdapter() {
|
|
2062
|
+
return createTreeFileAdapter({
|
|
2063
|
+
format: "yaml",
|
|
2064
|
+
extensions: [".yml", ".yaml"],
|
|
2065
|
+
parse: parseYamlObject,
|
|
2066
|
+
serialize: (tree) => yaml.stringify(tree),
|
|
2067
|
+
extractPlaceholders: extractDoubleBracePlaceholders,
|
|
2068
|
+
deriveEntry: (_key, value) => ({
|
|
2069
|
+
placeholders: extractDoubleBracePlaceholders(value),
|
|
2070
|
+
isPlural: false
|
|
2071
|
+
})
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
1499
2074
|
function createDefaultRegistry() {
|
|
1500
|
-
return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter());
|
|
2075
|
+
return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter());
|
|
1501
2076
|
}
|
|
1502
2077
|
|
|
1503
2078
|
// src/selection/select-adapter.ts
|
|
@@ -1512,6 +2087,89 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
|
|
|
1512
2087
|
);
|
|
1513
2088
|
}
|
|
1514
2089
|
|
|
2090
|
+
// src/flow/source.ts
|
|
2091
|
+
async function readSource(config, cwd, fs, adapter) {
|
|
2092
|
+
const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
|
|
2093
|
+
if (!await fs.fileExists(sourcePath)) {
|
|
2094
|
+
throw new SdkError(
|
|
2095
|
+
"SOURCE_UNREADABLE",
|
|
2096
|
+
`The source locale file was not found at ${sourcePath}.`
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
try {
|
|
2100
|
+
return await adapter.read(sourcePath, config.sourceLocale);
|
|
2101
|
+
} catch (error) {
|
|
2102
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2103
|
+
throw new SdkError(
|
|
2104
|
+
"SOURCE_INVALID",
|
|
2105
|
+
`The source locale file at ${sourcePath} could not be read: ${detail}`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// src/flow/diff-locales.ts
|
|
2111
|
+
async function readTarget(cwd, config, adapter, fs, locale) {
|
|
2112
|
+
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
2113
|
+
if (!await fs.fileExists(path)) {
|
|
2114
|
+
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
2115
|
+
}
|
|
2116
|
+
return (await adapter.read(path, locale)).resource;
|
|
2117
|
+
}
|
|
2118
|
+
function selectedLocales(config, requested) {
|
|
2119
|
+
if (requested === void 0) {
|
|
2120
|
+
return config.targetLocales;
|
|
2121
|
+
}
|
|
2122
|
+
const wanted = new Set(requested);
|
|
2123
|
+
return config.targetLocales.filter((locale) => wanted.has(locale));
|
|
2124
|
+
}
|
|
2125
|
+
async function diffLocales(input, deps = {}) {
|
|
2126
|
+
const config = input.config;
|
|
2127
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2128
|
+
const fs = deps.fs ?? defaultFs;
|
|
2129
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
2130
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
2131
|
+
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
2132
|
+
return Promise.all(
|
|
2133
|
+
selectedLocales(config, input.locales).map(async (locale) => {
|
|
2134
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
2135
|
+
const diff2 = diffResources(source.resource, target, { baseline: baselineFor(lock, locale) });
|
|
2136
|
+
return { locale, diff: diff2 };
|
|
2137
|
+
})
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// src/flow/check.ts
|
|
2142
|
+
function toCheckSummary(locale, diff2) {
|
|
2143
|
+
return {
|
|
2144
|
+
locale,
|
|
2145
|
+
missing: diff2.missing.length,
|
|
2146
|
+
stale: diff2.changed.length,
|
|
2147
|
+
upToDate: diff2.unchanged.length,
|
|
2148
|
+
inSync: diff2.missing.length === 0 && diff2.changed.length === 0
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
async function check(input, deps = {}) {
|
|
2152
|
+
const results = await diffLocales(input, deps);
|
|
2153
|
+
const locales = results.map(({ locale, diff: diff2 }) => toCheckSummary(locale, diff2));
|
|
2154
|
+
return { inSync: locales.every((entry) => entry.inSync), locales };
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/flow/diff.ts
|
|
2158
|
+
function toLocaleDiff(locale, diff2) {
|
|
2159
|
+
return {
|
|
2160
|
+
locale,
|
|
2161
|
+
missing: diff2.missing,
|
|
2162
|
+
changed: diff2.changed,
|
|
2163
|
+
orphaned: diff2.orphaned,
|
|
2164
|
+
hasPendingChanges: diff2.missing.length > 0 || diff2.changed.length > 0
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
async function diff(input, deps = {}) {
|
|
2168
|
+
const results = await diffLocales(input, deps);
|
|
2169
|
+
const locales = results.map(({ locale, diff: result }) => toLocaleDiff(locale, result));
|
|
2170
|
+
return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1515
2173
|
// src/selection/select-provider.ts
|
|
1516
2174
|
function selectProvider(config, createProvider = buildProvider) {
|
|
1517
2175
|
try {
|
|
@@ -1540,8 +2198,10 @@ function failureSummary(locale, error) {
|
|
|
1540
2198
|
translated: [],
|
|
1541
2199
|
unchanged: [],
|
|
1542
2200
|
orphaned: [],
|
|
2201
|
+
pruned: [],
|
|
1543
2202
|
invalidIcuSource: [],
|
|
1544
2203
|
integrityMismatches: [],
|
|
2204
|
+
generated: [],
|
|
1545
2205
|
notices: [],
|
|
1546
2206
|
error: describeError(error)
|
|
1547
2207
|
};
|
|
@@ -1564,18 +2224,210 @@ function readNotices(result) {
|
|
|
1564
2224
|
return candidate.filter(isNotice);
|
|
1565
2225
|
}
|
|
1566
2226
|
|
|
2227
|
+
// src/flow/plural-categories.ts
|
|
2228
|
+
var LANGUAGE_CATEGORIES = {
|
|
2229
|
+
ar: ["zero", "one", "two", "few", "many", "other"],
|
|
2230
|
+
cy: ["zero", "one", "two", "few", "many", "other"],
|
|
2231
|
+
ga: ["one", "two", "few", "many", "other"],
|
|
2232
|
+
pl: ["one", "few", "many", "other"],
|
|
2233
|
+
ru: ["one", "few", "many", "other"],
|
|
2234
|
+
uk: ["one", "few", "many", "other"],
|
|
2235
|
+
be: ["one", "few", "many", "other"],
|
|
2236
|
+
lt: ["one", "few", "many", "other"],
|
|
2237
|
+
sl: ["one", "two", "few", "other"]
|
|
2238
|
+
};
|
|
2239
|
+
function isKnownRicherLanguage(locale) {
|
|
2240
|
+
const subtag = locale.toLowerCase().split(/[-_]/)[0] ?? "";
|
|
2241
|
+
return LANGUAGE_CATEGORIES[subtag] !== void 0;
|
|
2242
|
+
}
|
|
2243
|
+
function requiredCategories(locale) {
|
|
2244
|
+
const subtag = locale.toLowerCase().split(/[-_]/)[0] ?? "";
|
|
2245
|
+
return LANGUAGE_CATEGORIES[subtag] ?? ["one", "other"];
|
|
2246
|
+
}
|
|
2247
|
+
function groupPluralSources(source) {
|
|
2248
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2249
|
+
for (const [key, entry] of source.entries) {
|
|
2250
|
+
const baseKey = pluralBaseKey(key);
|
|
2251
|
+
const category = pluralCategoryOf(key);
|
|
2252
|
+
if (baseKey === void 0 || category === void 0) {
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
const group = groups.get(baseKey) ?? /* @__PURE__ */ new Map();
|
|
2256
|
+
group.set(category, entry);
|
|
2257
|
+
groups.set(baseKey, group);
|
|
2258
|
+
}
|
|
2259
|
+
return groups;
|
|
2260
|
+
}
|
|
2261
|
+
function suppliedCategories(groups) {
|
|
2262
|
+
const supplied = /* @__PURE__ */ new Set();
|
|
2263
|
+
for (const group of groups.values()) {
|
|
2264
|
+
for (const category of group.keys()) {
|
|
2265
|
+
supplied.add(category);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return supplied;
|
|
2269
|
+
}
|
|
2270
|
+
function detectMissingPluralCategories(source, targetLocale, format) {
|
|
2271
|
+
if (format !== "i18next-json") {
|
|
2272
|
+
return void 0;
|
|
2273
|
+
}
|
|
2274
|
+
const groups = groupPluralSources(source);
|
|
2275
|
+
const supplied = suppliedCategories(groups);
|
|
2276
|
+
if (supplied.size === 0) {
|
|
2277
|
+
return void 0;
|
|
2278
|
+
}
|
|
2279
|
+
const missing = requiredCategories(targetLocale).filter((category) => !supplied.has(category));
|
|
2280
|
+
if (missing.length === 0) {
|
|
2281
|
+
return void 0;
|
|
2282
|
+
}
|
|
2283
|
+
return {
|
|
2284
|
+
code: "PLURAL_CATEGORIES_INCOMPLETE",
|
|
2285
|
+
message: `The source does not supply all CLDR plural categories the target language "${targetLocale}" requires (missing: ${missing.join(", ")}); verbatra translates only the source's plural forms and does not synthesize the others. Add the missing forms manually.`
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
function targetPluralSetIncomplete(targetKeys, targetLocale) {
|
|
2289
|
+
const required = requiredCategories(targetLocale);
|
|
2290
|
+
const present = /* @__PURE__ */ new Map();
|
|
2291
|
+
for (const key of targetKeys) {
|
|
2292
|
+
const baseKey = pluralBaseKey(key);
|
|
2293
|
+
const category = pluralCategoryOf(key);
|
|
2294
|
+
if (baseKey === void 0 || category === void 0) {
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
2297
|
+
const set = present.get(baseKey) ?? /* @__PURE__ */ new Set();
|
|
2298
|
+
set.add(category);
|
|
2299
|
+
present.set(baseKey, set);
|
|
2300
|
+
}
|
|
2301
|
+
for (const categories of present.values()) {
|
|
2302
|
+
if (required.some((category) => !categories.has(category))) {
|
|
2303
|
+
return true;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return false;
|
|
2307
|
+
}
|
|
2308
|
+
function sourcePluralBaseKeys(source) {
|
|
2309
|
+
const bases = /* @__PURE__ */ new Set();
|
|
2310
|
+
for (const key of source.entries.keys()) {
|
|
2311
|
+
const baseKey = pluralBaseKey(key);
|
|
2312
|
+
if (baseKey !== void 0) {
|
|
2313
|
+
bases.add(baseKey);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return bases;
|
|
2317
|
+
}
|
|
2318
|
+
function isGeneratedPluralKey(key, sourceBaseKeys) {
|
|
2319
|
+
const baseKey = pluralBaseKey(key);
|
|
2320
|
+
return baseKey !== void 0 && sourceBaseKeys.has(baseKey);
|
|
2321
|
+
}
|
|
2322
|
+
function pluralIncompleteNotice(targetLocale) {
|
|
2323
|
+
return {
|
|
2324
|
+
code: "PLURAL_CATEGORIES_INCOMPLETE",
|
|
2325
|
+
message: `The plural set for the target language "${targetLocale}" is still incomplete: verbatra could not generate every required CLDR plural form (an unsupported case, or a generated form was withheld for a placeholder mismatch). Add the remaining forms manually.`
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function representativeEntry(group) {
|
|
2329
|
+
return group.get("other") ?? group.get("one") ?? [...group.values()][0];
|
|
2330
|
+
}
|
|
2331
|
+
function planPluralGeneration(source, targetLocale, format) {
|
|
2332
|
+
if (format !== "i18next-json" || !isKnownRicherLanguage(targetLocale)) {
|
|
2333
|
+
return { items: [] };
|
|
2334
|
+
}
|
|
2335
|
+
const required = requiredCategories(targetLocale);
|
|
2336
|
+
const groups = groupPluralSources(source);
|
|
2337
|
+
const items = [];
|
|
2338
|
+
for (const [baseKey, group] of groups) {
|
|
2339
|
+
const representative = representativeEntry(group);
|
|
2340
|
+
if (representative === void 0) {
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
const governingEntries = [...group.values()];
|
|
2344
|
+
for (const category of required) {
|
|
2345
|
+
if (group.has(category)) {
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
items.push({
|
|
2349
|
+
targetKey: makePluralKey(baseKey, category),
|
|
2350
|
+
category,
|
|
2351
|
+
sourceEntry: representative,
|
|
2352
|
+
governingEntries
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return { items };
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/flow/plural-generation.ts
|
|
2360
|
+
function generatedLockHash(governingEntries, category) {
|
|
2361
|
+
const governingHashes = governingEntries.map(contentHash).sort();
|
|
2362
|
+
return contentHash({
|
|
2363
|
+
value: `${category}:${governingHashes.join("|")}`,
|
|
2364
|
+
placeholders: [],
|
|
2365
|
+
isPlural: true
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
function syntheticEntry(item) {
|
|
2369
|
+
return {
|
|
2370
|
+
...item.sourceEntry,
|
|
2371
|
+
key: item.targetKey,
|
|
2372
|
+
isPlural: true,
|
|
2373
|
+
// The CLDR category travels as data context (the meaning field), never the instruction channel.
|
|
2374
|
+
meaning: `CLDR plural category "${item.category}"`
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
function staleItems(items, baseline) {
|
|
2378
|
+
return items.filter((item) => {
|
|
2379
|
+
const hash = generatedLockHash(item.governingEntries, item.category);
|
|
2380
|
+
return baseline.get(item.targetKey) !== hash;
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
function buildRequest2(context, entries) {
|
|
2384
|
+
return {
|
|
2385
|
+
sourceLocale: context.sourceLocale,
|
|
2386
|
+
targetLocale: context.targetLocale,
|
|
2387
|
+
entries,
|
|
2388
|
+
extractPlaceholders: context.adapter.extractPlaceholders,
|
|
2389
|
+
...context.glossary !== void 0 ? { glossary: context.glossary } : {},
|
|
2390
|
+
...context.tone !== void 0 ? { tone: context.tone } : {}
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
async function generatePluralForms(context) {
|
|
2394
|
+
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
2395
|
+
const stale = staleItems(plan.items, context.baseline);
|
|
2396
|
+
if (stale.length === 0) {
|
|
2397
|
+
return { accepted: [], withheld: [] };
|
|
2398
|
+
}
|
|
2399
|
+
const entries = stale.map(syntheticEntry);
|
|
2400
|
+
const result = await context.provider.translateBatch(buildRequest2(context, entries));
|
|
2401
|
+
const accepted = [];
|
|
2402
|
+
const withheld = [];
|
|
2403
|
+
for (const item of stale) {
|
|
2404
|
+
const value = result.values.get(item.targetKey);
|
|
2405
|
+
const integrity = result.integrity.get(item.targetKey);
|
|
2406
|
+
if (value !== void 0 && integrity?.matches === true) {
|
|
2407
|
+
accepted.push({
|
|
2408
|
+
targetKey: item.targetKey,
|
|
2409
|
+
entry: { ...syntheticEntry(item), value },
|
|
2410
|
+
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
2411
|
+
});
|
|
2412
|
+
} else {
|
|
2413
|
+
withheld.push(item.targetKey);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
return { accepted, withheld };
|
|
2417
|
+
}
|
|
2418
|
+
|
|
1567
2419
|
// src/flow/locale-run.ts
|
|
1568
2420
|
function emptyResource(locale, format) {
|
|
1569
2421
|
return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
|
|
1570
2422
|
}
|
|
1571
|
-
async function
|
|
2423
|
+
async function readTarget2(params) {
|
|
1572
2424
|
const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
|
|
1573
2425
|
if (!await params.fs.fileExists(path)) {
|
|
1574
2426
|
return emptyResource(params.targetLocale, params.format);
|
|
1575
2427
|
}
|
|
1576
2428
|
return (await params.adapter.read(path, params.targetLocale)).resource;
|
|
1577
2429
|
}
|
|
1578
|
-
function
|
|
2430
|
+
function buildRequest3(params, entries) {
|
|
1579
2431
|
return {
|
|
1580
2432
|
sourceLocale: params.sourceLocale,
|
|
1581
2433
|
targetLocale: params.targetLocale,
|
|
@@ -1586,27 +2438,58 @@ function buildRequest2(params, entries) {
|
|
|
1586
2438
|
};
|
|
1587
2439
|
}
|
|
1588
2440
|
async function runLocale(params) {
|
|
1589
|
-
const target = await
|
|
1590
|
-
const
|
|
2441
|
+
const target = await readTarget2(params);
|
|
2442
|
+
const diff2 = diffResources(params.source, target, { baseline: params.baseline });
|
|
2443
|
+
const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
|
|
2444
|
+
const pruned = params.prune ? orphaned : [];
|
|
1591
2445
|
const invalidIcu = new Set(params.sourceInvalidIcuKeys);
|
|
1592
|
-
const candidates = [...
|
|
2446
|
+
const candidates = [...diff2.missing, ...diff2.changed];
|
|
1593
2447
|
const toTranslate = candidates.filter((key) => !invalidIcu.has(key));
|
|
1594
2448
|
const invalidIcuSource = candidates.filter((key) => invalidIcu.has(key));
|
|
2449
|
+
const pluralNotice = detectMissingPluralCategories(
|
|
2450
|
+
params.source,
|
|
2451
|
+
params.targetLocale,
|
|
2452
|
+
params.format
|
|
2453
|
+
);
|
|
2454
|
+
const sdkNotices = pluralNotice ? [pluralNotice] : [];
|
|
1595
2455
|
const provider = params.provider;
|
|
1596
2456
|
if (provider === void 0) {
|
|
1597
2457
|
return {
|
|
1598
|
-
summary: baseSummary(
|
|
2458
|
+
summary: baseSummary({
|
|
2459
|
+
locale: params.targetLocale,
|
|
2460
|
+
unchanged: diff2.unchanged,
|
|
2461
|
+
orphaned,
|
|
2462
|
+
invalidIcuSource,
|
|
2463
|
+
translated: toTranslate,
|
|
2464
|
+
generated: [],
|
|
2465
|
+
integrityMismatches: [],
|
|
2466
|
+
pruned,
|
|
2467
|
+
notices: sdkNotices
|
|
2468
|
+
}),
|
|
1599
2469
|
lockEntries: {}
|
|
1600
2470
|
};
|
|
1601
2471
|
}
|
|
1602
2472
|
const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
|
|
1603
2473
|
const accepted = /* @__PURE__ */ new Map();
|
|
1604
2474
|
const integrityMismatches = [];
|
|
1605
|
-
const
|
|
2475
|
+
const subBatchNotices = await translateAndCheck(
|
|
2476
|
+
provider,
|
|
2477
|
+
params,
|
|
2478
|
+
entries,
|
|
2479
|
+
accepted,
|
|
2480
|
+
integrityMismatches
|
|
2481
|
+
);
|
|
1606
2482
|
const merged = new Map(target.entries);
|
|
2483
|
+
for (const key of pruned) {
|
|
2484
|
+
merged.delete(key);
|
|
2485
|
+
}
|
|
1607
2486
|
for (const [key, { value, source }] of accepted) {
|
|
1608
2487
|
merged.set(key, { ...source, value, namespace: target.namespace });
|
|
1609
2488
|
}
|
|
2489
|
+
const generation = await runGeneration(params, provider);
|
|
2490
|
+
for (const form of generation.accepted) {
|
|
2491
|
+
merged.set(form.targetKey, { ...form.entry, namespace: target.namespace });
|
|
2492
|
+
}
|
|
1610
2493
|
const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
|
|
1611
2494
|
await params.adapter.write(
|
|
1612
2495
|
{
|
|
@@ -1617,37 +2500,83 @@ async function runLocale(params) {
|
|
|
1617
2500
|
},
|
|
1618
2501
|
path
|
|
1619
2502
|
);
|
|
1620
|
-
const
|
|
2503
|
+
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
2504
|
+
const notices = [...pluralNotices, ...subBatchNotices];
|
|
2505
|
+
const withheld = /* @__PURE__ */ new Set([...integrityMismatches, ...invalidIcuSource, ...generation.withheld]);
|
|
1621
2506
|
return {
|
|
1622
|
-
summary: baseSummary(
|
|
1623
|
-
params.targetLocale,
|
|
1624
|
-
|
|
2507
|
+
summary: baseSummary({
|
|
2508
|
+
locale: params.targetLocale,
|
|
2509
|
+
unchanged: diff2.unchanged,
|
|
2510
|
+
orphaned,
|
|
1625
2511
|
invalidIcuSource,
|
|
1626
|
-
[...accepted.keys()],
|
|
1627
|
-
|
|
2512
|
+
translated: [...accepted.keys()],
|
|
2513
|
+
generated: generation.accepted.map((form) => form.targetKey).sort(),
|
|
2514
|
+
// Withheld generated forms surface alongside withheld translations: both failed integrity.
|
|
2515
|
+
integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
|
|
2516
|
+
pruned,
|
|
1628
2517
|
notices
|
|
1629
|
-
),
|
|
1630
|
-
lockEntries: computeLockEntries(params, merged, withheld)
|
|
2518
|
+
}),
|
|
2519
|
+
lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
|
|
1631
2520
|
};
|
|
1632
2521
|
}
|
|
1633
|
-
function
|
|
2522
|
+
async function runGeneration(params, provider) {
|
|
2523
|
+
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
2524
|
+
return { accepted: [], withheld: [] };
|
|
2525
|
+
}
|
|
2526
|
+
return generatePluralForms({
|
|
2527
|
+
source: params.source,
|
|
2528
|
+
sourceLocale: params.sourceLocale,
|
|
2529
|
+
targetLocale: params.targetLocale,
|
|
2530
|
+
format: params.format,
|
|
2531
|
+
adapter: params.adapter,
|
|
2532
|
+
provider,
|
|
2533
|
+
glossary: params.glossary,
|
|
2534
|
+
tone: params.tone,
|
|
2535
|
+
baseline: params.baseline
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
function pluralNoticeFor(params, merged) {
|
|
2539
|
+
if (params.format !== "i18next-json") {
|
|
2540
|
+
return [];
|
|
2541
|
+
}
|
|
2542
|
+
if (!targetPluralSetIncomplete(merged.keys(), params.targetLocale)) {
|
|
2543
|
+
return [];
|
|
2544
|
+
}
|
|
2545
|
+
return [pluralIncompleteNotice(params.targetLocale)];
|
|
2546
|
+
}
|
|
2547
|
+
function baseSummary(parts) {
|
|
1634
2548
|
return {
|
|
1635
|
-
locale,
|
|
2549
|
+
locale: parts.locale,
|
|
1636
2550
|
status: "succeeded",
|
|
1637
|
-
translated,
|
|
1638
|
-
unchanged:
|
|
1639
|
-
orphaned:
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
2551
|
+
translated: parts.translated,
|
|
2552
|
+
unchanged: parts.unchanged,
|
|
2553
|
+
orphaned: parts.orphaned,
|
|
2554
|
+
pruned: parts.pruned,
|
|
2555
|
+
invalidIcuSource: parts.invalidIcuSource,
|
|
2556
|
+
integrityMismatches: parts.integrityMismatches,
|
|
2557
|
+
generated: parts.generated,
|
|
2558
|
+
notices: parts.notices
|
|
1643
2559
|
};
|
|
1644
2560
|
}
|
|
1645
2561
|
async function translateAndCheck(provider, params, entries, accepted, integrityMismatches) {
|
|
1646
|
-
|
|
1647
|
-
|
|
2562
|
+
const notices = [];
|
|
2563
|
+
for (const batch of chunk(entries, params.maxBatchSize)) {
|
|
2564
|
+
const subNotices = await runSubBatch(provider, params, batch, accepted, integrityMismatches);
|
|
2565
|
+
notices.push(...subNotices);
|
|
1648
2566
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
2567
|
+
return notices;
|
|
2568
|
+
}
|
|
2569
|
+
async function runSubBatch(provider, params, batch, accepted, integrityMismatches) {
|
|
2570
|
+
let result;
|
|
2571
|
+
try {
|
|
2572
|
+
result = await provider.translateBatch(buildRequest3(params, batch));
|
|
2573
|
+
} catch {
|
|
2574
|
+
for (const entry of batch) {
|
|
2575
|
+
integrityMismatches.push(entry.key);
|
|
2576
|
+
}
|
|
2577
|
+
return [subBatchFailedNotice(batch.length)];
|
|
2578
|
+
}
|
|
2579
|
+
for (const entry of batch) {
|
|
1651
2580
|
const value = result.values.get(entry.key);
|
|
1652
2581
|
const integrity = result.integrity.get(entry.key);
|
|
1653
2582
|
if (value !== void 0 && integrity?.matches === true) {
|
|
@@ -1658,11 +2587,28 @@ async function translateAndCheck(provider, params, entries, accepted, integrityM
|
|
|
1658
2587
|
}
|
|
1659
2588
|
return readNotices(result);
|
|
1660
2589
|
}
|
|
1661
|
-
function
|
|
2590
|
+
function subBatchFailedNotice(count) {
|
|
2591
|
+
return {
|
|
2592
|
+
code: "SUB_BATCH_FAILED",
|
|
2593
|
+
message: `A sub-batch of ${count} entries failed and was withheld; it will be retried next run.`
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
function chunk(items, size) {
|
|
2597
|
+
const chunks = [];
|
|
2598
|
+
for (let index = 0; index < items.length; index += size) {
|
|
2599
|
+
chunks.push(items.slice(index, index + size));
|
|
2600
|
+
}
|
|
2601
|
+
return chunks;
|
|
2602
|
+
}
|
|
2603
|
+
function computeLockEntries(params, merged, withheld, generated) {
|
|
1662
2604
|
const lockEntries = {};
|
|
2605
|
+
const sourceBaseKeys = sourcePluralBaseKeys(params.source);
|
|
1663
2606
|
for (const key of merged.keys()) {
|
|
1664
2607
|
const sourceEntry = params.source.entries.get(key);
|
|
1665
2608
|
if (sourceEntry === void 0) {
|
|
2609
|
+
if (params.generatePlurals) {
|
|
2610
|
+
carryGeneratedLock(lockEntries, params.baseline, key, sourceBaseKeys);
|
|
2611
|
+
}
|
|
1666
2612
|
continue;
|
|
1667
2613
|
}
|
|
1668
2614
|
if (withheld.has(key)) {
|
|
@@ -1674,26 +2620,18 @@ function computeLockEntries(params, merged, withheld) {
|
|
|
1674
2620
|
}
|
|
1675
2621
|
lockEntries[key] = contentHash(sourceEntry);
|
|
1676
2622
|
}
|
|
2623
|
+
for (const form of generated) {
|
|
2624
|
+
lockEntries[form.targetKey] = form.lockHash;
|
|
2625
|
+
}
|
|
1677
2626
|
return lockEntries;
|
|
1678
2627
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
|
|
1683
|
-
if (!await fs.fileExists(sourcePath)) {
|
|
1684
|
-
throw new SdkError(
|
|
1685
|
-
"SOURCE_UNREADABLE",
|
|
1686
|
-
`The source locale file was not found at ${sourcePath}.`
|
|
1687
|
-
);
|
|
2628
|
+
function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
|
|
2629
|
+
if (!isGeneratedPluralKey(key, sourceBaseKeys)) {
|
|
2630
|
+
return;
|
|
1688
2631
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1693
|
-
throw new SdkError(
|
|
1694
|
-
"SOURCE_INVALID",
|
|
1695
|
-
`The source locale file at ${sourcePath} could not be read: ${detail}`
|
|
1696
|
-
);
|
|
2632
|
+
const prior = baseline.get(key);
|
|
2633
|
+
if (prior !== void 0) {
|
|
2634
|
+
lockEntries[key] = prior;
|
|
1697
2635
|
}
|
|
1698
2636
|
}
|
|
1699
2637
|
|
|
@@ -1702,6 +2640,9 @@ async function translate2(input, deps = {}) {
|
|
|
1702
2640
|
const config = input.config;
|
|
1703
2641
|
const cwd = input.cwd ?? process.cwd();
|
|
1704
2642
|
const dryRun = input.dryRun ?? false;
|
|
2643
|
+
const prune = input.prune ?? config.prune ?? false;
|
|
2644
|
+
const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
|
|
2645
|
+
const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
1705
2646
|
const fs = deps.fs ?? defaultFs;
|
|
1706
2647
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
1707
2648
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
@@ -1724,6 +2665,9 @@ async function translate2(input, deps = {}) {
|
|
|
1724
2665
|
format: config.format,
|
|
1725
2666
|
glossary: config.glossary,
|
|
1726
2667
|
tone: config.tone,
|
|
2668
|
+
prune,
|
|
2669
|
+
generatePlurals,
|
|
2670
|
+
maxBatchSize,
|
|
1727
2671
|
fs
|
|
1728
2672
|
};
|
|
1729
2673
|
const { summary, lockEntries } = await runLocale(params);
|
|
@@ -1899,7 +2843,7 @@ var DEFAULT_WORKBOOK_LIMITS = {
|
|
|
1899
2843
|
maxRowsPerSheet: 1e5,
|
|
1900
2844
|
maxCellsPerRow: 64
|
|
1901
2845
|
};
|
|
1902
|
-
function
|
|
2846
|
+
function assertNoDoctype2(name, xml) {
|
|
1903
2847
|
if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
|
|
1904
2848
|
throw new ExchangeError(
|
|
1905
2849
|
"WORKBOOK_INVALID",
|
|
@@ -1954,7 +2898,7 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
1954
2898
|
`The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
|
|
1955
2899
|
);
|
|
1956
2900
|
}
|
|
1957
|
-
|
|
2901
|
+
assertNoDoctype2(file.name, content);
|
|
1958
2902
|
}
|
|
1959
2903
|
}
|
|
1960
2904
|
var rowSchema = zod.z.object({
|
|
@@ -2063,7 +3007,7 @@ async function readWorkbook(bytes, options = {}) {
|
|
|
2063
3007
|
|
|
2064
3008
|
// src/flow/workbook/export-workbook.ts
|
|
2065
3009
|
var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
|
|
2066
|
-
async function
|
|
3010
|
+
async function readTarget3(cwd, config, adapter, fs, locale) {
|
|
2067
3011
|
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
2068
3012
|
if (!await fs.fileExists(path)) {
|
|
2069
3013
|
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
@@ -2071,7 +3015,7 @@ async function readTarget2(cwd, config, adapter, fs, locale) {
|
|
|
2071
3015
|
return (await adapter.read(path, locale)).resource;
|
|
2072
3016
|
}
|
|
2073
3017
|
function buildRows(source, target, baseline, includeUnchanged) {
|
|
2074
|
-
const
|
|
3018
|
+
const diff2 = diffResources(source, target, { baseline });
|
|
2075
3019
|
const rows = [];
|
|
2076
3020
|
const add = (keys, status) => {
|
|
2077
3021
|
for (const key of keys) {
|
|
@@ -2089,14 +3033,14 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
2089
3033
|
});
|
|
2090
3034
|
}
|
|
2091
3035
|
};
|
|
2092
|
-
add(
|
|
2093
|
-
add(
|
|
3036
|
+
add(diff2.missing, "new");
|
|
3037
|
+
add(diff2.changed, "changed");
|
|
2094
3038
|
if (includeUnchanged) {
|
|
2095
|
-
add(
|
|
3039
|
+
add(diff2.unchanged, "changed");
|
|
2096
3040
|
}
|
|
2097
3041
|
return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
2098
3042
|
}
|
|
2099
|
-
function
|
|
3043
|
+
function selectedLocales2(config, requested) {
|
|
2100
3044
|
if (requested === void 0) {
|
|
2101
3045
|
return config.targetLocales;
|
|
2102
3046
|
}
|
|
@@ -2110,10 +3054,10 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
2110
3054
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
2111
3055
|
const source = await readSource(config, cwd, fs, adapter);
|
|
2112
3056
|
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
2113
|
-
const locales =
|
|
3057
|
+
const locales = selectedLocales2(config, input.locales);
|
|
2114
3058
|
const sheets = await Promise.all(
|
|
2115
3059
|
locales.map(async (locale) => {
|
|
2116
|
-
const target = await
|
|
3060
|
+
const target = await readTarget3(cwd, config, adapter, fs, locale);
|
|
2117
3061
|
const rows = buildRows(
|
|
2118
3062
|
source.resource,
|
|
2119
3063
|
target,
|
|
@@ -2183,7 +3127,7 @@ function classifyRows(params, buckets) {
|
|
|
2183
3127
|
}
|
|
2184
3128
|
}
|
|
2185
3129
|
function importLocale(params) {
|
|
2186
|
-
const
|
|
3130
|
+
const diff2 = diffResources(params.source, params.target, { baseline: params.baseline });
|
|
2187
3131
|
const buckets = { accepted: /* @__PURE__ */ new Map(), mismatches: [], withheld: /* @__PURE__ */ new Set() };
|
|
2188
3132
|
classifyRows(params, buckets);
|
|
2189
3133
|
const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
|
|
@@ -2192,10 +3136,14 @@ function importLocale(params) {
|
|
|
2192
3136
|
locale: params.sheet.locale,
|
|
2193
3137
|
status: "succeeded",
|
|
2194
3138
|
translated: [...buckets.accepted.keys()].sort(),
|
|
2195
|
-
unchanged:
|
|
2196
|
-
orphaned:
|
|
3139
|
+
unchanged: diff2.unchanged,
|
|
3140
|
+
orphaned: diff2.orphaned,
|
|
3141
|
+
// Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
|
|
3142
|
+
pruned: [],
|
|
2197
3143
|
invalidIcuSource,
|
|
2198
3144
|
integrityMismatches: [...buckets.mismatches].sort(),
|
|
3145
|
+
// Plural generation is a translate-flow concern; the manual workbook import never generates forms.
|
|
3146
|
+
generated: [],
|
|
2199
3147
|
notices: []
|
|
2200
3148
|
};
|
|
2201
3149
|
return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
|
|
@@ -2216,7 +3164,7 @@ async function readWorkbookBytes(path, fs) {
|
|
|
2216
3164
|
}
|
|
2217
3165
|
return read.bytes;
|
|
2218
3166
|
}
|
|
2219
|
-
async function
|
|
3167
|
+
async function readTarget4(cwd, config, adapter, fs, locale) {
|
|
2220
3168
|
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
2221
3169
|
if (!await fs.fileExists(path)) {
|
|
2222
3170
|
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
@@ -2255,7 +3203,7 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
2255
3203
|
`The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
|
|
2256
3204
|
);
|
|
2257
3205
|
}
|
|
2258
|
-
const target = await
|
|
3206
|
+
const target = await readTarget4(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
|
|
2259
3207
|
const baseline = baselineFor(lock, sheet.locale);
|
|
2260
3208
|
const { summary, accepted, withheld } = importLocale({
|
|
2261
3209
|
sheet,
|
|
@@ -2325,6 +3273,16 @@ async function importWorkbook(input, deps = {}) {
|
|
|
2325
3273
|
const { succeeded, failed } = partition(summaries);
|
|
2326
3274
|
return { dryRun, locales: summaries, succeeded, failed };
|
|
2327
3275
|
}
|
|
3276
|
+
|
|
3277
|
+
// src/scaffolding.ts
|
|
3278
|
+
var scaffoldingMetadata = {
|
|
3279
|
+
/** Provider id -> the environment variable its API key is read from. Owned by ai-providers. */
|
|
3280
|
+
providerEnv: PROVIDER_ENV,
|
|
3281
|
+
/** LLM provider id -> a cosmetic default scaffold model. Owned by ai-providers. DeepL has none. */
|
|
3282
|
+
scaffoldModels: SCAFFOLD_MODELS,
|
|
3283
|
+
/** The closed set of source format ids. Owned by core. */
|
|
3284
|
+
supportedFormats: SUPPORTED_FORMATS
|
|
3285
|
+
};
|
|
2328
3286
|
var defaultCreateWatcher = (paths) => {
|
|
2329
3287
|
const fsWatcher = chokidar.watch([...paths], { persistent: true, ignoreInitial: true });
|
|
2330
3288
|
return {
|
|
@@ -2433,10 +3391,13 @@ async function watch(input, deps = {}) {
|
|
|
2433
3391
|
|
|
2434
3392
|
exports.DEFAULT_WORKBOOK_PATH = DEFAULT_WORKBOOK_PATH;
|
|
2435
3393
|
exports.SdkError = SdkError;
|
|
3394
|
+
exports.check = check;
|
|
2436
3395
|
exports.defineConfig = defineConfig;
|
|
3396
|
+
exports.diff = diff;
|
|
2437
3397
|
exports.exportWorkbook = exportWorkbook;
|
|
2438
3398
|
exports.importWorkbook = importWorkbook;
|
|
2439
3399
|
exports.loadConfig = loadConfig;
|
|
3400
|
+
exports.scaffoldingMetadata = scaffoldingMetadata;
|
|
2440
3401
|
exports.translate = translate2;
|
|
2441
3402
|
exports.verbatraConfigSchema = verbatraConfigSchema;
|
|
2442
3403
|
exports.watch = watch;
|