@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.js
CHANGED
|
@@ -5,11 +5,15 @@ import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import Anthropic from '@anthropic-ai/sdk';
|
|
7
7
|
import * as deepl from 'deepl-node';
|
|
8
|
+
import { createRequire } from 'module';
|
|
8
9
|
import log from 'loglevel';
|
|
9
10
|
import { GoogleGenAI } from '@google/genai';
|
|
10
11
|
import OpenAI from 'openai';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
11
13
|
import { access, writeFile, rename, rm, open } from 'fs/promises';
|
|
12
|
-
import { parse, TYPE } from '@formatjs/icu-messageformat-parser';
|
|
14
|
+
import { parse as parse$1, TYPE } from '@formatjs/icu-messageformat-parser';
|
|
15
|
+
import { XMLSerializer, DOMParser } from '@xmldom/xmldom';
|
|
16
|
+
import { stringify, parse } from 'yaml';
|
|
13
17
|
import ExcelJS from 'exceljs';
|
|
14
18
|
import JSZip from 'jszip';
|
|
15
19
|
import { watch as watch$1 } from 'chokidar';
|
|
@@ -44,13 +48,16 @@ function fnv1a64(input) {
|
|
|
44
48
|
}
|
|
45
49
|
return hash.toString(16).padStart(16, "0");
|
|
46
50
|
}
|
|
51
|
+
function normalizeText(text) {
|
|
52
|
+
return text.normalize("NFC").replace(/\r\n?/g, "\n");
|
|
53
|
+
}
|
|
47
54
|
function canonicalize(entry) {
|
|
48
55
|
return JSON.stringify([
|
|
49
|
-
entry.value,
|
|
50
|
-
entry.description
|
|
51
|
-
entry.meaning
|
|
56
|
+
normalizeText(entry.value),
|
|
57
|
+
entry.description == null ? null : normalizeText(entry.description),
|
|
58
|
+
entry.meaning == null ? null : normalizeText(entry.meaning),
|
|
52
59
|
entry.isPlural,
|
|
53
|
-
[...entry.placeholders].sort()
|
|
60
|
+
[...entry.placeholders].map(normalizeText).sort()
|
|
54
61
|
]);
|
|
55
62
|
}
|
|
56
63
|
function contentHash(entry) {
|
|
@@ -96,7 +103,10 @@ var SUPPORTED_FORMATS = [
|
|
|
96
103
|
"i18next-json",
|
|
97
104
|
"vue-i18n-json",
|
|
98
105
|
"next-intl-json",
|
|
99
|
-
"ngx-translate-json"
|
|
106
|
+
"ngx-translate-json",
|
|
107
|
+
"xliff",
|
|
108
|
+
"yaml",
|
|
109
|
+
"arb"
|
|
100
110
|
];
|
|
101
111
|
var supportedFormatSchema = z.enum(SUPPORTED_FORMATS);
|
|
102
112
|
var translationEntrySchema = z.object({
|
|
@@ -114,17 +124,31 @@ z.object({
|
|
|
114
124
|
format: supportedFormatSchema,
|
|
115
125
|
entries: z.map(z.string(), translationEntrySchema)
|
|
116
126
|
});
|
|
117
|
-
function
|
|
118
|
-
|
|
127
|
+
function counts(items) {
|
|
128
|
+
const map = /* @__PURE__ */ new Map();
|
|
129
|
+
for (const item of items) {
|
|
130
|
+
map.set(item, (map.get(item) ?? 0) + 1);
|
|
131
|
+
}
|
|
132
|
+
return map;
|
|
133
|
+
}
|
|
134
|
+
function multisetExcess(a, b) {
|
|
135
|
+
const excess = [];
|
|
136
|
+
for (const [token, count] of a) {
|
|
137
|
+
const surplus = count - (b.get(token) ?? 0);
|
|
138
|
+
for (let i = 0; i < surplus; i += 1) {
|
|
139
|
+
excess.push(token);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return excess.sort();
|
|
119
143
|
}
|
|
120
144
|
function sameOrder(a, b) {
|
|
121
145
|
return a.length === b.length && a.every((item, index) => item === b[index]);
|
|
122
146
|
}
|
|
123
147
|
function checkPlaceholders(source, translated) {
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const missing =
|
|
127
|
-
const extra =
|
|
148
|
+
const sourceCounts = counts(source);
|
|
149
|
+
const translatedCounts = counts(translated);
|
|
150
|
+
const missing = multisetExcess(sourceCounts, translatedCounts);
|
|
151
|
+
const extra = multisetExcess(translatedCounts, sourceCounts);
|
|
128
152
|
const reordered = missing.length === 0 && extra.length === 0 && !sameOrder(source, translated);
|
|
129
153
|
return {
|
|
130
154
|
matches: missing.length === 0 && extra.length === 0 && !reordered,
|
|
@@ -137,6 +161,26 @@ var LOCALE_TOKEN = "{locale}";
|
|
|
137
161
|
function localeFilePath(cwd, pattern, locale) {
|
|
138
162
|
return resolve(cwd, pattern.replaceAll(LOCALE_TOKEN, locale));
|
|
139
163
|
}
|
|
164
|
+
var REDACTED = "[REDACTED]";
|
|
165
|
+
var KEY_PATTERNS = [
|
|
166
|
+
// The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
|
|
167
|
+
/\bsk-[A-Za-z0-9_-]{8,}/g,
|
|
168
|
+
/AIza[0-9A-Za-z_-]{35}/g,
|
|
169
|
+
/[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
|
|
170
|
+
];
|
|
171
|
+
function escapeForRegExp(value) {
|
|
172
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
173
|
+
}
|
|
174
|
+
function redact(text, secret = process.env.ANTHROPIC_API_KEY) {
|
|
175
|
+
let out = text;
|
|
176
|
+
for (const pattern of KEY_PATTERNS) {
|
|
177
|
+
out = out.replace(pattern, REDACTED);
|
|
178
|
+
}
|
|
179
|
+
if (secret !== void 0 && secret.length > 0) {
|
|
180
|
+
out = out.replace(new RegExp(escapeForRegExp(secret), "g"), REDACTED);
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
140
184
|
var ProviderError = class extends Error {
|
|
141
185
|
/** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
|
|
142
186
|
code;
|
|
@@ -145,7 +189,7 @@ var ProviderError = class extends Error {
|
|
|
145
189
|
* @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
|
|
146
190
|
*/
|
|
147
191
|
constructor(code, message) {
|
|
148
|
-
super(message);
|
|
192
|
+
super(redact(message, ""));
|
|
149
193
|
this.name = "ProviderError";
|
|
150
194
|
this.code = code;
|
|
151
195
|
}
|
|
@@ -266,6 +310,18 @@ async function runLlmTranslation(request, mechanism) {
|
|
|
266
310
|
);
|
|
267
311
|
return completion.usage === void 0 ? { values, integrity } : { values, integrity, usage: completion.usage };
|
|
268
312
|
}
|
|
313
|
+
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.";
|
|
314
|
+
function assertNotTruncated(truncated) {
|
|
315
|
+
if (truncated) {
|
|
316
|
+
throw new ProviderError("OUTPUT_TRUNCATED", OUTPUT_TRUNCATED_MESSAGE);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
var PROVIDER_ENV = {
|
|
320
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
321
|
+
openai: "OPENAI_API_KEY",
|
|
322
|
+
gemini: "GEMINI_API_KEY",
|
|
323
|
+
deepl: "DEEPL_API_KEY"
|
|
324
|
+
};
|
|
269
325
|
function readRequiredEnv(name) {
|
|
270
326
|
const value = process.env[name];
|
|
271
327
|
if (value === void 0 || value.length === 0) {
|
|
@@ -274,16 +330,16 @@ function readRequiredEnv(name) {
|
|
|
274
330
|
return value;
|
|
275
331
|
}
|
|
276
332
|
function requireAnthropicKey() {
|
|
277
|
-
return readRequiredEnv(
|
|
333
|
+
return readRequiredEnv(PROVIDER_ENV.anthropic);
|
|
278
334
|
}
|
|
279
335
|
function requireOpenAiKey() {
|
|
280
|
-
return readRequiredEnv(
|
|
336
|
+
return readRequiredEnv(PROVIDER_ENV.openai);
|
|
281
337
|
}
|
|
282
338
|
function requireGeminiKey() {
|
|
283
|
-
return readRequiredEnv(
|
|
339
|
+
return readRequiredEnv(PROVIDER_ENV.gemini);
|
|
284
340
|
}
|
|
285
341
|
function requireDeepLKey() {
|
|
286
|
-
return readRequiredEnv(
|
|
342
|
+
return readRequiredEnv(PROVIDER_ENV.deepl);
|
|
287
343
|
}
|
|
288
344
|
function createDefaultClient() {
|
|
289
345
|
const sdk = new Anthropic({ apiKey: requireAnthropicKey(), logLevel: "off" });
|
|
@@ -361,6 +417,7 @@ function createMechanism(client, config) {
|
|
|
361
417
|
translate: async ({ payloadJson }) => {
|
|
362
418
|
const body = buildRequest(config, payloadJson);
|
|
363
419
|
const message = await callClient(client, body);
|
|
420
|
+
assertNotTruncated(message.stop_reason === "max_tokens");
|
|
364
421
|
const raw = requireToolInput(message.content);
|
|
365
422
|
const usage = toUsage(message.usage);
|
|
366
423
|
return usage === void 0 ? { raw } : { raw, usage };
|
|
@@ -383,8 +440,22 @@ function toUsage(usage) {
|
|
|
383
440
|
var deepLConfigSchema = z.object({
|
|
384
441
|
glossaryId: z.string().min(1).optional()
|
|
385
442
|
});
|
|
443
|
+
var DEEPL_LOGGER = "deepl";
|
|
444
|
+
function resolveDeeplLoglevel(requireFn = createRequire(import.meta.url)) {
|
|
445
|
+
try {
|
|
446
|
+
const entry = requireFn.resolve("deepl-node");
|
|
447
|
+
return createRequire(entry)("loglevel");
|
|
448
|
+
} catch {
|
|
449
|
+
return void 0;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function silenceDeeplLogger(instances) {
|
|
453
|
+
for (const instance of instances) {
|
|
454
|
+
instance?.getLogger(DEEPL_LOGGER).setLevel("silent");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
386
457
|
function silenceSdkLogging() {
|
|
387
|
-
log
|
|
458
|
+
silenceDeeplLogger([log, resolveDeeplLoglevel()]);
|
|
388
459
|
}
|
|
389
460
|
function createDefaultClient2() {
|
|
390
461
|
silenceSdkLogging();
|
|
@@ -608,6 +679,7 @@ function extractGeminiResult(response) {
|
|
|
608
679
|
if (candidate.finishReason !== void 0 && BLOCKED_FINISH_REASONS.has(candidate.finishReason)) {
|
|
609
680
|
throw new ProviderError("PROVIDER_BLOCKED", "The provider filtered the translation response.");
|
|
610
681
|
}
|
|
682
|
+
assertNotTruncated(candidate.finishReason === "MAX_TOKENS");
|
|
611
683
|
const text = response.text;
|
|
612
684
|
if (text === void 0 || text === "") {
|
|
613
685
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no translation content.");
|
|
@@ -704,10 +776,12 @@ function toUsage3(usage) {
|
|
|
704
776
|
return { inputTokens: prompt_tokens, outputTokens: completion_tokens };
|
|
705
777
|
}
|
|
706
778
|
function extractOpenAiResult(completion) {
|
|
707
|
-
const
|
|
708
|
-
if (
|
|
779
|
+
const choice = completion.choices[0];
|
|
780
|
+
if (choice === void 0) {
|
|
709
781
|
throw new ProviderError("INVALID_RESPONSE", "The provider returned no message.");
|
|
710
782
|
}
|
|
783
|
+
assertNotTruncated(choice.finish_reason === "length");
|
|
784
|
+
const message = choice.message;
|
|
711
785
|
if (message.refusal !== void 0 && message.refusal !== null && message.refusal !== "") {
|
|
712
786
|
throw new ProviderError("PROVIDER_REFUSED", "The provider refused the translation request.");
|
|
713
787
|
}
|
|
@@ -742,6 +816,11 @@ function createMechanism3(client, config) {
|
|
|
742
816
|
function callClient4(client, body) {
|
|
743
817
|
return guardProviderCall(() => client.chat.completions.create(body));
|
|
744
818
|
}
|
|
819
|
+
var SCAFFOLD_MODELS = {
|
|
820
|
+
anthropic: "claude-sonnet-4-6",
|
|
821
|
+
openai: "gpt-5.4-mini",
|
|
822
|
+
gemini: "gemini-2.5-flash"
|
|
823
|
+
};
|
|
745
824
|
var providerConfigSchema = z.discriminatedUnion("id", [
|
|
746
825
|
z.object({ id: z.literal("anthropic"), options: anthropicConfigSchema.strict() }),
|
|
747
826
|
z.object({ id: z.literal("openai"), options: openAiConfigSchema.strict() }),
|
|
@@ -760,6 +839,7 @@ function buildProvider(config) {
|
|
|
760
839
|
}
|
|
761
840
|
|
|
762
841
|
// src/config/schema.ts
|
|
842
|
+
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
763
843
|
var verbatraConfigSchema = z.strictObject({
|
|
764
844
|
sourceLocale: z.string().min(1),
|
|
765
845
|
targetLocales: z.array(z.string().min(1)).min(1),
|
|
@@ -769,7 +849,29 @@ var verbatraConfigSchema = z.strictObject({
|
|
|
769
849
|
}),
|
|
770
850
|
provider: providerConfigSchema,
|
|
771
851
|
glossary: z.record(z.string(), z.string()).optional(),
|
|
772
|
-
tone: z.enum(["formal", "informal", "neutral"]).optional()
|
|
852
|
+
tone: z.enum(["formal", "informal", "neutral"]).optional(),
|
|
853
|
+
/**
|
|
854
|
+
* Opt-in orphan pruning, off by default. When true, keys present in a target file but absent from
|
|
855
|
+
* the source are removed from the written file and the lock. The per-run `prune` option on
|
|
856
|
+
* `translate` (the CLI `--prune` flag) overrides this.
|
|
857
|
+
*/
|
|
858
|
+
prune: z.boolean().optional(),
|
|
859
|
+
/**
|
|
860
|
+
* Opt-in plural-category generation, off by default. When true, and only for an i18next-JSON project
|
|
861
|
+
* translated by an LLM provider, verbatra synthesizes the CLDR plural forms a target language
|
|
862
|
+
* requires but the source does not supply (for example Polish few/many). The per-run
|
|
863
|
+
* `generatePlurals` option on `translate` overrides this. Unsupported cases (DeepL, non-i18next, an
|
|
864
|
+
* unknown language) fall back to the per-locale plural warning.
|
|
865
|
+
*/
|
|
866
|
+
generatePlurals: z.boolean().optional(),
|
|
867
|
+
/**
|
|
868
|
+
* Optional maximum number of entries sent in a single provider request. A locale's missing and
|
|
869
|
+
* changed entries are split into sequential sub-batches no larger than this, so one oversized request
|
|
870
|
+
* cannot sink the whole locale; a failed sub-batch is withheld while the others make progress. Must
|
|
871
|
+
* be a positive integer (zero, negative, or non-integer is rejected, never coerced). When absent,
|
|
872
|
+
* {@link DEFAULT_MAX_BATCH_SIZE} applies.
|
|
873
|
+
*/
|
|
874
|
+
maxBatchSize: z.number().int().positive().optional()
|
|
773
875
|
}).refine((config) => !config.targetLocales.includes(config.sourceLocale), {
|
|
774
876
|
message: "targetLocales must not include the source locale",
|
|
775
877
|
path: ["targetLocales"]
|
|
@@ -914,8 +1016,11 @@ async function readBoundedBytes(path, maxBytes) {
|
|
|
914
1016
|
await handle.close();
|
|
915
1017
|
}
|
|
916
1018
|
}
|
|
1019
|
+
function tempFileName(path) {
|
|
1020
|
+
return join(dirname(path), `.${basename(path)}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`);
|
|
1021
|
+
}
|
|
917
1022
|
async function atomicWrite(path, data) {
|
|
918
|
-
const tmp =
|
|
1023
|
+
const tmp = tempFileName(path);
|
|
919
1024
|
await (typeof data === "string" ? writeFile(tmp, data, "utf8") : writeFile(tmp, data));
|
|
920
1025
|
try {
|
|
921
1026
|
await rename(tmp, path);
|
|
@@ -996,6 +1101,85 @@ async function writeLockFile(path, lock, fs) {
|
|
|
996
1101
|
await fs.writeFile(path, `${JSON.stringify(ordered, null, 2)}
|
|
997
1102
|
`);
|
|
998
1103
|
}
|
|
1104
|
+
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
1105
|
+
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
1106
|
+
function tokenOf(element) {
|
|
1107
|
+
switch (element.type) {
|
|
1108
|
+
case TYPE.argument:
|
|
1109
|
+
case TYPE.number:
|
|
1110
|
+
case TYPE.date:
|
|
1111
|
+
case TYPE.time:
|
|
1112
|
+
case TYPE.select:
|
|
1113
|
+
case TYPE.plural:
|
|
1114
|
+
return `{${element.value}}`;
|
|
1115
|
+
case TYPE.tag:
|
|
1116
|
+
return `<${element.value}>`;
|
|
1117
|
+
default:
|
|
1118
|
+
return void 0;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function childMessages(element) {
|
|
1122
|
+
if (element.type === TYPE.plural || element.type === TYPE.select) {
|
|
1123
|
+
return Object.values(element.options).map((option) => option.value);
|
|
1124
|
+
}
|
|
1125
|
+
if (element.type === TYPE.tag) {
|
|
1126
|
+
return [element.children];
|
|
1127
|
+
}
|
|
1128
|
+
return [];
|
|
1129
|
+
}
|
|
1130
|
+
function collect(elements, add, state) {
|
|
1131
|
+
for (const element of elements) {
|
|
1132
|
+
const token = tokenOf(element);
|
|
1133
|
+
if (token !== void 0) {
|
|
1134
|
+
add(token);
|
|
1135
|
+
}
|
|
1136
|
+
if (element.type === TYPE.plural) {
|
|
1137
|
+
state.isPlural = true;
|
|
1138
|
+
}
|
|
1139
|
+
for (const child of childMessages(element)) {
|
|
1140
|
+
collect(child, add, state);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function analyzeIcuValue(value) {
|
|
1145
|
+
if (!value.includes("{") && !value.includes("<")) {
|
|
1146
|
+
return VALID_EMPTY;
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
const ast = parse$1(value);
|
|
1150
|
+
const placeholders = [];
|
|
1151
|
+
const state = { isPlural: false };
|
|
1152
|
+
collect(
|
|
1153
|
+
ast,
|
|
1154
|
+
(token) => {
|
|
1155
|
+
placeholders.push(token);
|
|
1156
|
+
},
|
|
1157
|
+
state
|
|
1158
|
+
);
|
|
1159
|
+
return { placeholders, isPlural: state.isPlural, valid: true };
|
|
1160
|
+
} catch {
|
|
1161
|
+
return INVALID;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function icuPlaceholders(value) {
|
|
1165
|
+
return analyzeIcuValue(value).placeholders;
|
|
1166
|
+
}
|
|
1167
|
+
function icuIsValid(value) {
|
|
1168
|
+
return analyzeIcuValue(value).valid;
|
|
1169
|
+
}
|
|
1170
|
+
function icuDeriveEntry(_key, value) {
|
|
1171
|
+
const analysis = analyzeIcuValue(value);
|
|
1172
|
+
return { placeholders: analysis.placeholders, isPlural: analysis.isPlural };
|
|
1173
|
+
}
|
|
1174
|
+
function icuInvalidKeys(entries) {
|
|
1175
|
+
const invalid = [];
|
|
1176
|
+
for (const [key, entry] of entries) {
|
|
1177
|
+
if (!icuIsValid(entry.value)) {
|
|
1178
|
+
invalid.push(key);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return invalid;
|
|
1182
|
+
}
|
|
999
1183
|
var AdapterError = class extends Error {
|
|
1000
1184
|
code;
|
|
1001
1185
|
constructor(code, message) {
|
|
@@ -1004,6 +1188,82 @@ var AdapterError = class extends Error {
|
|
|
1004
1188
|
this.code = code;
|
|
1005
1189
|
}
|
|
1006
1190
|
};
|
|
1191
|
+
var MAX_DEPTH = 100;
|
|
1192
|
+
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1193
|
+
var jsonTreeSchema = z.lazy(
|
|
1194
|
+
() => z.union([z.string(), z.record(z.string(), jsonTreeSchema)])
|
|
1195
|
+
);
|
|
1196
|
+
var rootSchema = z.record(z.string(), jsonTreeSchema);
|
|
1197
|
+
function assertWithinDepth(value, max) {
|
|
1198
|
+
const stack = [{ node: value, depth: 1 }];
|
|
1199
|
+
while (stack.length > 0) {
|
|
1200
|
+
const top = stack.pop();
|
|
1201
|
+
if (top === void 0) {
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
const { node, depth } = top;
|
|
1205
|
+
if (typeof node !== "object" || node === null) {
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
if (depth > max) {
|
|
1209
|
+
throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
|
|
1210
|
+
}
|
|
1211
|
+
for (const child of Object.values(node)) {
|
|
1212
|
+
stack.push({ node: child, depth: depth + 1 });
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function assertJsonRecord(value) {
|
|
1217
|
+
assertWithinDepth(value, MAX_DEPTH);
|
|
1218
|
+
const result = rootSchema.safeParse(value);
|
|
1219
|
+
if (!result.success) {
|
|
1220
|
+
throw new AdapterError(
|
|
1221
|
+
"INVALID_STRUCTURE",
|
|
1222
|
+
"The file is not a valid object (expected nested objects of string values)."
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
return result.data;
|
|
1226
|
+
}
|
|
1227
|
+
function parseJsonObject(content) {
|
|
1228
|
+
let parsed;
|
|
1229
|
+
try {
|
|
1230
|
+
parsed = JSON.parse(content);
|
|
1231
|
+
} catch {
|
|
1232
|
+
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
1233
|
+
}
|
|
1234
|
+
return assertJsonRecord(parsed);
|
|
1235
|
+
}
|
|
1236
|
+
function serializeJsonTree(tree) {
|
|
1237
|
+
return `${JSON.stringify(tree, null, 2)}
|
|
1238
|
+
`;
|
|
1239
|
+
}
|
|
1240
|
+
function namespaceOf(filePath) {
|
|
1241
|
+
return basename(filePath, extname(filePath));
|
|
1242
|
+
}
|
|
1243
|
+
function rethrowStructured(error, message) {
|
|
1244
|
+
if (error instanceof AdapterError) {
|
|
1245
|
+
throw error;
|
|
1246
|
+
}
|
|
1247
|
+
throw new AdapterError("INVALID_STRUCTURE", message);
|
|
1248
|
+
}
|
|
1249
|
+
function computeIcu(entries, compute) {
|
|
1250
|
+
if (!compute) {
|
|
1251
|
+
return [];
|
|
1252
|
+
}
|
|
1253
|
+
try {
|
|
1254
|
+
return compute(entries);
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
rethrowStructured(error, "The file could not be analyzed for message validity.");
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function buildCanHandle(extensions, sniff) {
|
|
1260
|
+
return (filePath, sample) => {
|
|
1261
|
+
if (!extensions.includes(extname(filePath).toLowerCase())) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
return sample === void 0 || sniff === void 0 || sniff(sample);
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1007
1267
|
var nodeOps = {
|
|
1008
1268
|
writeFile: (path, data) => writeFile(path, data, "utf8"),
|
|
1009
1269
|
rename: (from, to) => rename(from, to),
|
|
@@ -1015,8 +1275,11 @@ async function cleanup(ops, tmp) {
|
|
|
1015
1275
|
} catch {
|
|
1016
1276
|
}
|
|
1017
1277
|
}
|
|
1278
|
+
function tempFileName2(path) {
|
|
1279
|
+
return join(dirname(path), `.${basename(path)}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`);
|
|
1280
|
+
}
|
|
1018
1281
|
async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
1019
|
-
const tmp =
|
|
1282
|
+
const tmp = tempFileName2(path);
|
|
1020
1283
|
try {
|
|
1021
1284
|
await ops.writeFile(tmp, data);
|
|
1022
1285
|
await ops.rename(tmp, path);
|
|
@@ -1025,8 +1288,6 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
|
|
|
1025
1288
|
throw error;
|
|
1026
1289
|
}
|
|
1027
1290
|
}
|
|
1028
|
-
var MAX_DEPTH = 100;
|
|
1029
|
-
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1030
1291
|
async function readBoundedUtf82(handle, size) {
|
|
1031
1292
|
const buffer = Buffer.allocUnsafe(size);
|
|
1032
1293
|
let offset = 0;
|
|
@@ -1054,61 +1315,128 @@ async function readBounded2(filePath) {
|
|
|
1054
1315
|
await handle.close();
|
|
1055
1316
|
}
|
|
1056
1317
|
}
|
|
1057
|
-
function
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1318
|
+
async function readFileContent(filePath) {
|
|
1319
|
+
const outcome = await readBounded2(filePath);
|
|
1320
|
+
if (outcome.kind === "not-a-file") {
|
|
1321
|
+
throw new AdapterError("INVALID_STRUCTURE", "The path is not a regular file.");
|
|
1322
|
+
}
|
|
1323
|
+
if (outcome.kind === "too-large") {
|
|
1324
|
+
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
1325
|
+
}
|
|
1326
|
+
return outcome.content;
|
|
1327
|
+
}
|
|
1328
|
+
var BACKSLASH = "\\";
|
|
1329
|
+
var DOT = ".";
|
|
1330
|
+
var ESCAPED_BACKSLASH = "\\\\";
|
|
1331
|
+
var ESCAPED_DOT = "\\.";
|
|
1332
|
+
function needsEncoding(segment) {
|
|
1333
|
+
return segment.includes(BACKSLASH) || segment.includes(DOT);
|
|
1334
|
+
}
|
|
1335
|
+
function encodeSegment(segment) {
|
|
1336
|
+
if (!needsEncoding(segment)) {
|
|
1337
|
+
return segment;
|
|
1338
|
+
}
|
|
1339
|
+
let out = "";
|
|
1340
|
+
for (const char of segment) {
|
|
1341
|
+
if (char === BACKSLASH) {
|
|
1342
|
+
out += ESCAPED_BACKSLASH;
|
|
1343
|
+
} else if (char === DOT) {
|
|
1344
|
+
out += ESCAPED_DOT;
|
|
1063
1345
|
} else {
|
|
1064
|
-
|
|
1346
|
+
out += char;
|
|
1065
1347
|
}
|
|
1066
1348
|
}
|
|
1067
|
-
}
|
|
1068
|
-
function flattenTree(tree, namespace, derive) {
|
|
1069
|
-
const out = /* @__PURE__ */ new Map();
|
|
1070
|
-
addEntries(tree, "", namespace, derive, out);
|
|
1071
1349
|
return out;
|
|
1072
1350
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
if (depth > max) {
|
|
1089
|
-
throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
|
|
1090
|
-
}
|
|
1091
|
-
for (const child of Object.values(node)) {
|
|
1092
|
-
stack.push({ node: child, depth: depth + 1 });
|
|
1351
|
+
function decodeSegment(segment) {
|
|
1352
|
+
if (!segment.includes(BACKSLASH)) {
|
|
1353
|
+
return segment;
|
|
1354
|
+
}
|
|
1355
|
+
let out = "";
|
|
1356
|
+
let escaping = false;
|
|
1357
|
+
for (const char of segment) {
|
|
1358
|
+
if (escaping) {
|
|
1359
|
+
out += char;
|
|
1360
|
+
escaping = false;
|
|
1361
|
+
} else if (char === BACKSLASH) {
|
|
1362
|
+
escaping = true;
|
|
1363
|
+
} else {
|
|
1364
|
+
out += char;
|
|
1093
1365
|
}
|
|
1094
1366
|
}
|
|
1367
|
+
return out;
|
|
1095
1368
|
}
|
|
1096
|
-
function
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1369
|
+
function joinEncodedSegments(segments) {
|
|
1370
|
+
return segments.join(DOT);
|
|
1371
|
+
}
|
|
1372
|
+
function decodeKeyToSegments(key) {
|
|
1373
|
+
if (!key.includes(BACKSLASH)) {
|
|
1374
|
+
return key.split(DOT);
|
|
1375
|
+
}
|
|
1376
|
+
const segments = [];
|
|
1377
|
+
let current = "";
|
|
1378
|
+
let escaping = false;
|
|
1379
|
+
for (const char of key) {
|
|
1380
|
+
if (escaping) {
|
|
1381
|
+
current += BACKSLASH + char;
|
|
1382
|
+
escaping = false;
|
|
1383
|
+
} else if (char === BACKSLASH) {
|
|
1384
|
+
escaping = true;
|
|
1385
|
+
} else if (char === DOT) {
|
|
1386
|
+
segments.push(current);
|
|
1387
|
+
current = "";
|
|
1388
|
+
} else {
|
|
1389
|
+
current += char;
|
|
1390
|
+
}
|
|
1102
1391
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1392
|
+
if (escaping) {
|
|
1393
|
+
current += BACKSLASH;
|
|
1394
|
+
}
|
|
1395
|
+
segments.push(current);
|
|
1396
|
+
return segments.map(decodeSegment);
|
|
1397
|
+
}
|
|
1398
|
+
function addLeaf(ctx, segments, key, value) {
|
|
1399
|
+
const effectivePath = segments.join(".");
|
|
1400
|
+
const mapKey = joinEncodedSegments(segments.map(encodeSegment));
|
|
1401
|
+
if (ctx.claimed.has(effectivePath) && ctx.claimed.get(effectivePath) !== mapKey) {
|
|
1106
1402
|
throw new AdapterError(
|
|
1107
1403
|
"INVALID_STRUCTURE",
|
|
1108
|
-
"
|
|
1404
|
+
"A literal dotted leaf key and a nested key path resolve to the same path."
|
|
1109
1405
|
);
|
|
1110
1406
|
}
|
|
1111
|
-
|
|
1407
|
+
ctx.claimed.set(effectivePath, mapKey);
|
|
1408
|
+
const { placeholders, isPlural } = ctx.derive(key, value);
|
|
1409
|
+
ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
|
|
1410
|
+
}
|
|
1411
|
+
function addEntries(ctx, prefix, node) {
|
|
1412
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1413
|
+
const segments = [...prefix, key];
|
|
1414
|
+
if (typeof value === "string") {
|
|
1415
|
+
addLeaf(ctx, segments, key, value);
|
|
1416
|
+
} else {
|
|
1417
|
+
addEntries(ctx, segments, value);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function addPathEntries(node, prefix, namespace, derive, out) {
|
|
1422
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1423
|
+
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
1424
|
+
if (typeof value === "string") {
|
|
1425
|
+
const { placeholders, isPlural } = derive(key, value);
|
|
1426
|
+
out.set(path, { key: path, namespace, value, placeholders, isPlural });
|
|
1427
|
+
} else {
|
|
1428
|
+
addPathEntries(value, path, namespace, derive, out);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
|
|
1433
|
+
const out = /* @__PURE__ */ new Map();
|
|
1434
|
+
if (keyMode === "path-notation") {
|
|
1435
|
+
addPathEntries(tree, "", namespace, derive, out);
|
|
1436
|
+
return out;
|
|
1437
|
+
}
|
|
1438
|
+
addEntries({ namespace, derive, out, claimed: /* @__PURE__ */ new Map() }, [], tree);
|
|
1439
|
+
return out;
|
|
1112
1440
|
}
|
|
1113
1441
|
function emptyNode() {
|
|
1114
1442
|
return /* @__PURE__ */ Object.create(null);
|
|
@@ -1134,103 +1462,194 @@ function setPath(root, segments, value) {
|
|
|
1134
1462
|
for (const segment of segments.slice(0, -1)) {
|
|
1135
1463
|
node = descend(node, segment);
|
|
1136
1464
|
}
|
|
1465
|
+
if (typeof node[leaf] === "object") {
|
|
1466
|
+
throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
|
|
1467
|
+
}
|
|
1137
1468
|
node[leaf] = value;
|
|
1138
1469
|
}
|
|
1139
1470
|
function unflattenEntries(entries) {
|
|
1140
1471
|
const root = emptyNode();
|
|
1141
1472
|
for (const [key, entry] of entries) {
|
|
1142
|
-
setPath(root, key
|
|
1473
|
+
setPath(root, decodeKeyToSegments(key), entry.value);
|
|
1143
1474
|
}
|
|
1144
1475
|
return root;
|
|
1145
1476
|
}
|
|
1146
|
-
function
|
|
1147
|
-
return basename(filePath, extname(filePath));
|
|
1148
|
-
}
|
|
1149
|
-
function canHandle(filePath, sample) {
|
|
1150
|
-
if (extname(filePath).toLowerCase() !== ".json") {
|
|
1151
|
-
return false;
|
|
1152
|
-
}
|
|
1153
|
-
return sample === void 0 || sample.trimStart().startsWith("{");
|
|
1154
|
-
}
|
|
1155
|
-
function rethrowStructured(error, message) {
|
|
1156
|
-
if (error instanceof AdapterError) {
|
|
1157
|
-
throw error;
|
|
1158
|
-
}
|
|
1159
|
-
throw new AdapterError("INVALID_STRUCTURE", message);
|
|
1160
|
-
}
|
|
1161
|
-
function toEntries(content, namespace, deriveEntry, validateTree) {
|
|
1477
|
+
function toEntries(content, namespace, parse2, deriveEntry, keyMode, validateTree) {
|
|
1162
1478
|
try {
|
|
1163
|
-
const tree =
|
|
1479
|
+
const tree = parse2(content);
|
|
1164
1480
|
validateTree?.(tree);
|
|
1165
|
-
return flattenTree(tree, namespace, deriveEntry);
|
|
1166
|
-
} catch (error) {
|
|
1167
|
-
rethrowStructured(error, "The file could not be read as JSON.");
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
function computeIcu(entries, compute) {
|
|
1171
|
-
if (!compute) {
|
|
1172
|
-
return [];
|
|
1173
|
-
}
|
|
1174
|
-
try {
|
|
1175
|
-
return compute(entries);
|
|
1481
|
+
return flattenTree(tree, namespace, deriveEntry, keyMode);
|
|
1176
1482
|
} catch (error) {
|
|
1177
|
-
rethrowStructured(error, "The file could not be
|
|
1483
|
+
rethrowStructured(error, "The file could not be parsed.");
|
|
1178
1484
|
}
|
|
1179
1485
|
}
|
|
1180
|
-
function
|
|
1486
|
+
function createTreeFileAdapter(options) {
|
|
1181
1487
|
const {
|
|
1182
1488
|
format,
|
|
1489
|
+
extensions,
|
|
1490
|
+
sniff,
|
|
1491
|
+
parse: parse2,
|
|
1492
|
+
serialize,
|
|
1183
1493
|
deriveEntry,
|
|
1184
|
-
extractPlaceholders
|
|
1185
|
-
computeInvalidIcuKeys
|
|
1186
|
-
validateMessage
|
|
1494
|
+
extractPlaceholders,
|
|
1495
|
+
computeInvalidIcuKeys,
|
|
1496
|
+
validateMessage,
|
|
1187
1497
|
validateTree,
|
|
1188
|
-
buildWriteTree
|
|
1498
|
+
buildWriteTree,
|
|
1499
|
+
keyMode = "literal-leaf"
|
|
1189
1500
|
} = options;
|
|
1190
1501
|
return {
|
|
1191
1502
|
format,
|
|
1192
|
-
canHandle,
|
|
1193
|
-
extractPlaceholders
|
|
1194
|
-
|
|
1195
|
-
validateMessage: validateMessage2 ?? (() => true),
|
|
1503
|
+
canHandle: buildCanHandle(extensions, sniff),
|
|
1504
|
+
extractPlaceholders,
|
|
1505
|
+
validateMessage: validateMessage ?? (() => true),
|
|
1196
1506
|
async read(filePath, locale) {
|
|
1197
|
-
const
|
|
1198
|
-
if (outcome.kind === "not-a-file") {
|
|
1199
|
-
throw new AdapterError("INVALID_STRUCTURE", "The path is not a regular file.");
|
|
1200
|
-
}
|
|
1201
|
-
if (outcome.kind === "too-large") {
|
|
1202
|
-
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
1203
|
-
}
|
|
1507
|
+
const content = await readFileContent(filePath);
|
|
1204
1508
|
const namespace = namespaceOf(filePath);
|
|
1205
|
-
const entries = toEntries(
|
|
1509
|
+
const entries = toEntries(content, namespace, parse2, deriveEntry, keyMode, validateTree);
|
|
1206
1510
|
const resource = { locale, namespace, format, entries };
|
|
1207
|
-
const invalidIcuKeys = computeIcu(entries,
|
|
1511
|
+
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1208
1512
|
return { resource, invalidIcuKeys };
|
|
1209
1513
|
},
|
|
1210
1514
|
async write(resource, filePath) {
|
|
1211
1515
|
const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
|
|
1212
|
-
await atomicWriteFile(filePath,
|
|
1213
|
-
`);
|
|
1516
|
+
await atomicWriteFile(filePath, serialize(tree));
|
|
1214
1517
|
}
|
|
1215
1518
|
};
|
|
1216
1519
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1520
|
+
function isMetadataKey(key) {
|
|
1521
|
+
return key.startsWith("@");
|
|
1522
|
+
}
|
|
1523
|
+
function parseArbObject(content) {
|
|
1524
|
+
let parsed;
|
|
1525
|
+
try {
|
|
1526
|
+
parsed = JSON.parse(content);
|
|
1527
|
+
} catch {
|
|
1528
|
+
throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
|
|
1529
|
+
}
|
|
1530
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1531
|
+
throw new AdapterError(
|
|
1532
|
+
"INVALID_STRUCTURE",
|
|
1533
|
+
"The file is not a valid object (expected nested objects of string values)."
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
return parsed;
|
|
1537
|
+
}
|
|
1538
|
+
function stripArbMetadata(tree) {
|
|
1539
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1540
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
1541
|
+
if (!isMetadataKey(key)) {
|
|
1542
|
+
out[key] = value;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return out;
|
|
1546
|
+
}
|
|
1547
|
+
function originalKey(encoded) {
|
|
1548
|
+
return decodeKeyToSegments(encoded).join(".");
|
|
1549
|
+
}
|
|
1550
|
+
function messagesFromEntries(entries) {
|
|
1551
|
+
const out = /* @__PURE__ */ new Map();
|
|
1552
|
+
for (const [key, entry] of entries) {
|
|
1553
|
+
out.set(originalKey(key), entry.value);
|
|
1554
|
+
}
|
|
1555
|
+
return out;
|
|
1556
|
+
}
|
|
1557
|
+
async function readDestinationPairs(filePath) {
|
|
1558
|
+
let parsed;
|
|
1559
|
+
try {
|
|
1560
|
+
const outcome = await readBounded2(filePath);
|
|
1561
|
+
if (outcome.kind !== "ok") {
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
parsed = JSON.parse(outcome.content);
|
|
1565
|
+
} catch {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
return Object.entries(parsed);
|
|
1572
|
+
}
|
|
1573
|
+
async function buildArbWriteTree(entries, filePath) {
|
|
1574
|
+
const messages = messagesFromEntries(entries);
|
|
1575
|
+
const pairs = await readDestinationPairs(filePath);
|
|
1576
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1577
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1578
|
+
for (const [key, value] of pairs ?? []) {
|
|
1579
|
+
const translated = isMetadataKey(key) ? void 0 : messages.get(key);
|
|
1580
|
+
if (translated !== void 0) {
|
|
1581
|
+
consumed.add(key);
|
|
1582
|
+
}
|
|
1583
|
+
out[key] = translated ?? value;
|
|
1584
|
+
}
|
|
1585
|
+
for (const [key, value] of messages) {
|
|
1586
|
+
if (!consumed.has(key)) {
|
|
1587
|
+
out[key] = value;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return out;
|
|
1591
|
+
}
|
|
1592
|
+
function parseArb(content) {
|
|
1593
|
+
return assertJsonRecord(stripArbMetadata(parseArbObject(content)));
|
|
1594
|
+
}
|
|
1595
|
+
function createArbAdapter() {
|
|
1596
|
+
return createTreeFileAdapter({
|
|
1597
|
+
format: "arb",
|
|
1598
|
+
extensions: [".arb"],
|
|
1599
|
+
sniff: (sample) => sample.trimStart().startsWith("{"),
|
|
1600
|
+
parse: parseArb,
|
|
1601
|
+
serialize: serializeJsonTree,
|
|
1602
|
+
extractPlaceholders: icuPlaceholders,
|
|
1603
|
+
deriveEntry: icuDeriveEntry,
|
|
1604
|
+
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1605
|
+
validateMessage: icuIsValid,
|
|
1606
|
+
buildWriteTree: buildArbWriteTree
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
function createJsonFileAdapter(options) {
|
|
1610
|
+
return createTreeFileAdapter({
|
|
1611
|
+
...options,
|
|
1612
|
+
extensions: [".json"],
|
|
1613
|
+
sniff: (sample) => sample.trimStart().startsWith("{"),
|
|
1614
|
+
parse: parseJsonObject,
|
|
1615
|
+
serialize: serializeJsonTree
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
var DOUBLE_BRACE_PATTERN = /\{\{[^{}]*\}\}/g;
|
|
1619
|
+
var I18NEXT_PATTERN = /\{\{[^{}]*\}\}|\$t\([^()]*\)/g;
|
|
1620
|
+
function scanTokens(value, pattern) {
|
|
1220
1621
|
const result = [];
|
|
1221
|
-
for (const match of value.matchAll(
|
|
1622
|
+
for (const match of value.matchAll(pattern)) {
|
|
1222
1623
|
const token = match[0];
|
|
1223
|
-
if (token !== void 0
|
|
1224
|
-
seen.add(token);
|
|
1624
|
+
if (token !== void 0) {
|
|
1225
1625
|
result.push(token);
|
|
1226
1626
|
}
|
|
1227
1627
|
}
|
|
1228
1628
|
return result;
|
|
1229
1629
|
}
|
|
1630
|
+
function extractDoubleBracePlaceholders(value) {
|
|
1631
|
+
return scanTokens(value, DOUBLE_BRACE_PATTERN);
|
|
1632
|
+
}
|
|
1633
|
+
function extractI18nextPlaceholders(value) {
|
|
1634
|
+
return scanTokens(value, I18NEXT_PATTERN);
|
|
1635
|
+
}
|
|
1230
1636
|
var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
|
|
1231
1637
|
function isPluralKey(key) {
|
|
1232
1638
|
return PLURAL_SUFFIX.test(key);
|
|
1233
1639
|
}
|
|
1640
|
+
function pluralCategoryOf(key) {
|
|
1641
|
+
const match = PLURAL_SUFFIX.exec(key);
|
|
1642
|
+
return match?.[1];
|
|
1643
|
+
}
|
|
1644
|
+
function pluralBaseKey(key) {
|
|
1645
|
+
if (!isPluralKey(key)) {
|
|
1646
|
+
return void 0;
|
|
1647
|
+
}
|
|
1648
|
+
return key.replace(PLURAL_SUFFIX, "");
|
|
1649
|
+
}
|
|
1650
|
+
function makePluralKey(baseKey, category) {
|
|
1651
|
+
return `${baseKey}_${category}`;
|
|
1652
|
+
}
|
|
1234
1653
|
function createI18nextJsonAdapter() {
|
|
1235
1654
|
return createJsonFileAdapter({
|
|
1236
1655
|
format: "i18next-json",
|
|
@@ -1241,95 +1660,13 @@ function createI18nextJsonAdapter() {
|
|
|
1241
1660
|
})
|
|
1242
1661
|
});
|
|
1243
1662
|
}
|
|
1244
|
-
var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
|
|
1245
|
-
var INVALID = { placeholders: [], isPlural: false, valid: false };
|
|
1246
|
-
function tokenOf(element) {
|
|
1247
|
-
switch (element.type) {
|
|
1248
|
-
case TYPE.argument:
|
|
1249
|
-
case TYPE.number:
|
|
1250
|
-
case TYPE.date:
|
|
1251
|
-
case TYPE.time:
|
|
1252
|
-
case TYPE.select:
|
|
1253
|
-
case TYPE.plural:
|
|
1254
|
-
return `{${element.value}}`;
|
|
1255
|
-
case TYPE.tag:
|
|
1256
|
-
return `<${element.value}>`;
|
|
1257
|
-
default:
|
|
1258
|
-
return void 0;
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
function childMessages(element) {
|
|
1262
|
-
if (element.type === TYPE.plural || element.type === TYPE.select) {
|
|
1263
|
-
return Object.values(element.options).map((option) => option.value);
|
|
1264
|
-
}
|
|
1265
|
-
if (element.type === TYPE.tag) {
|
|
1266
|
-
return [element.children];
|
|
1267
|
-
}
|
|
1268
|
-
return [];
|
|
1269
|
-
}
|
|
1270
|
-
function collect(elements, add, state) {
|
|
1271
|
-
for (const element of elements) {
|
|
1272
|
-
const token = tokenOf(element);
|
|
1273
|
-
if (token !== void 0) {
|
|
1274
|
-
add(token);
|
|
1275
|
-
}
|
|
1276
|
-
if (element.type === TYPE.plural) {
|
|
1277
|
-
state.isPlural = true;
|
|
1278
|
-
}
|
|
1279
|
-
for (const child of childMessages(element)) {
|
|
1280
|
-
collect(child, add, state);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
function analyzeIcuValue(value) {
|
|
1285
|
-
if (!value.includes("{") && !value.includes("<")) {
|
|
1286
|
-
return VALID_EMPTY;
|
|
1287
|
-
}
|
|
1288
|
-
try {
|
|
1289
|
-
const ast = parse(value);
|
|
1290
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1291
|
-
const placeholders = [];
|
|
1292
|
-
const state = { isPlural: false };
|
|
1293
|
-
collect(
|
|
1294
|
-
ast,
|
|
1295
|
-
(token) => {
|
|
1296
|
-
if (!seen.has(token)) {
|
|
1297
|
-
seen.add(token);
|
|
1298
|
-
placeholders.push(token);
|
|
1299
|
-
}
|
|
1300
|
-
},
|
|
1301
|
-
state
|
|
1302
|
-
);
|
|
1303
|
-
return { placeholders, isPlural: state.isPlural, valid: true };
|
|
1304
|
-
} catch {
|
|
1305
|
-
return INVALID;
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
function extractPlaceholders(value) {
|
|
1309
|
-
return analyzeIcuValue(value).placeholders;
|
|
1310
|
-
}
|
|
1311
|
-
function validateMessage(value) {
|
|
1312
|
-
return analyzeIcuValue(value).valid;
|
|
1313
|
-
}
|
|
1314
|
-
function computeInvalidIcuKeys(entries) {
|
|
1315
|
-
const invalid = [];
|
|
1316
|
-
for (const [key, entry] of entries) {
|
|
1317
|
-
if (!validateMessage(entry.value)) {
|
|
1318
|
-
invalid.push(key);
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
return invalid;
|
|
1322
|
-
}
|
|
1323
1663
|
function createNextIntlJsonAdapter() {
|
|
1324
1664
|
return createJsonFileAdapter({
|
|
1325
1665
|
format: "next-intl-json",
|
|
1326
|
-
extractPlaceholders,
|
|
1327
|
-
deriveEntry:
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
},
|
|
1331
|
-
computeInvalidIcuKeys,
|
|
1332
|
-
validateMessage
|
|
1666
|
+
extractPlaceholders: icuPlaceholders,
|
|
1667
|
+
deriveEntry: icuDeriveEntry,
|
|
1668
|
+
computeInvalidIcuKeys: icuInvalidKeys,
|
|
1669
|
+
validateMessage: icuIsValid
|
|
1333
1670
|
});
|
|
1334
1671
|
}
|
|
1335
1672
|
function assertNotMixed(tree) {
|
|
@@ -1384,13 +1721,15 @@ async function buildNgxWriteTree(entries, filePath) {
|
|
|
1384
1721
|
function createNgxTranslateJsonAdapter() {
|
|
1385
1722
|
return createJsonFileAdapter({
|
|
1386
1723
|
format: "ngx-translate-json",
|
|
1387
|
-
extractPlaceholders:
|
|
1724
|
+
extractPlaceholders: extractDoubleBracePlaceholders,
|
|
1388
1725
|
deriveEntry: (_key, value) => ({
|
|
1389
|
-
placeholders:
|
|
1726
|
+
placeholders: extractDoubleBracePlaceholders(value),
|
|
1390
1727
|
isPlural: false
|
|
1391
1728
|
}),
|
|
1392
1729
|
validateTree: assertNotMixed,
|
|
1393
|
-
buildWriteTree: buildNgxWriteTree
|
|
1730
|
+
buildWriteTree: buildNgxWriteTree,
|
|
1731
|
+
// ngx-translate flat style uses dotted keys as path notation, not literal leaves.
|
|
1732
|
+
keyMode: "path-notation"
|
|
1394
1733
|
});
|
|
1395
1734
|
}
|
|
1396
1735
|
var AdapterRegistry = class {
|
|
@@ -1441,15 +1780,13 @@ var AdapterRegistry = class {
|
|
|
1441
1780
|
return this.resolveByDetection(filePath, options.sample);
|
|
1442
1781
|
}
|
|
1443
1782
|
};
|
|
1444
|
-
var
|
|
1783
|
+
var PLACEHOLDER_PATTERN = /(?<!\{)\{\s*([A-Za-z_][\w$-]*|\d+)\s*\}(?!\})/g;
|
|
1445
1784
|
function extractVueI18nPlaceholders(value) {
|
|
1446
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1447
1785
|
const result = [];
|
|
1448
|
-
for (const match of value.matchAll(
|
|
1449
|
-
const
|
|
1450
|
-
if (
|
|
1451
|
-
|
|
1452
|
-
result.push(token);
|
|
1786
|
+
for (const match of value.matchAll(PLACEHOLDER_PATTERN)) {
|
|
1787
|
+
const key = match[1];
|
|
1788
|
+
if (key !== void 0) {
|
|
1789
|
+
result.push(`{${key}}`);
|
|
1453
1790
|
}
|
|
1454
1791
|
}
|
|
1455
1792
|
return result;
|
|
@@ -1467,8 +1804,245 @@ function createVueI18nJsonAdapter() {
|
|
|
1467
1804
|
})
|
|
1468
1805
|
});
|
|
1469
1806
|
}
|
|
1807
|
+
function toEntries2(content, namespace, parseEntries) {
|
|
1808
|
+
try {
|
|
1809
|
+
return parseEntries(content, namespace);
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
rethrowStructured(error, "The file could not be parsed.");
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function createFlatFileAdapter(options) {
|
|
1815
|
+
const {
|
|
1816
|
+
format,
|
|
1817
|
+
extensions,
|
|
1818
|
+
sniff,
|
|
1819
|
+
parseEntries,
|
|
1820
|
+
serializeEntries,
|
|
1821
|
+
extractPlaceholders,
|
|
1822
|
+
validateMessage,
|
|
1823
|
+
computeInvalidIcuKeys
|
|
1824
|
+
} = options;
|
|
1825
|
+
return {
|
|
1826
|
+
format,
|
|
1827
|
+
canHandle: buildCanHandle(extensions, sniff),
|
|
1828
|
+
extractPlaceholders,
|
|
1829
|
+
validateMessage: validateMessage ?? (() => true),
|
|
1830
|
+
async read(filePath, locale) {
|
|
1831
|
+
const content = await readFileContent(filePath);
|
|
1832
|
+
const namespace = namespaceOf(filePath);
|
|
1833
|
+
const entries = toEntries2(content, namespace, parseEntries);
|
|
1834
|
+
const resource = { locale, namespace, format, entries };
|
|
1835
|
+
const invalidIcuKeys = computeIcu(entries, computeInvalidIcuKeys);
|
|
1836
|
+
return { resource, invalidIcuKeys };
|
|
1837
|
+
},
|
|
1838
|
+
async write(resource, filePath) {
|
|
1839
|
+
const data = await serializeEntries(resource.entries, filePath);
|
|
1840
|
+
await atomicWriteFile(filePath, data);
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
var XLIFF_PATTERN = /<(?:x|g|bx|ex|ph|it|mrk)\b[^>]*>|\{[^{}]+\}/g;
|
|
1845
|
+
function extractXliffPlaceholders(value) {
|
|
1846
|
+
const result = [];
|
|
1847
|
+
for (const match of value.matchAll(XLIFF_PATTERN)) {
|
|
1848
|
+
const token = match[0];
|
|
1849
|
+
if (token !== void 0) {
|
|
1850
|
+
result.push(token);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return result;
|
|
1854
|
+
}
|
|
1855
|
+
var ELEMENT_NODE = 1;
|
|
1856
|
+
function isElement(node) {
|
|
1857
|
+
return node.nodeType === ELEMENT_NODE;
|
|
1858
|
+
}
|
|
1859
|
+
function elementChildren(parent) {
|
|
1860
|
+
return Array.from(parent.childNodes).filter(isElement);
|
|
1861
|
+
}
|
|
1862
|
+
function childByName(parent, name) {
|
|
1863
|
+
return elementChildren(parent).find((el) => el.localName === name) ?? null;
|
|
1864
|
+
}
|
|
1865
|
+
function collectByTag(root, name) {
|
|
1866
|
+
return Array.from(root.getElementsByTagName(name));
|
|
1867
|
+
}
|
|
1868
|
+
function unitKey(element, index) {
|
|
1869
|
+
return element.getAttribute("id") ?? element.getAttribute("resname") ?? `unit-${index}`;
|
|
1870
|
+
}
|
|
1871
|
+
function onFatal(level) {
|
|
1872
|
+
if (level === "fatalError") {
|
|
1873
|
+
throw new Error("malformed XML");
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function assertNoDoctype(content) {
|
|
1877
|
+
if (/<!DOCTYPE/i.test(content) || /<!ENTITY/i.test(content)) {
|
|
1878
|
+
throw new AdapterError("INVALID_XML", "XLIFF with a DTD or entity declaration is rejected.");
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
function parseXml(content) {
|
|
1882
|
+
assertNoDoctype(content);
|
|
1883
|
+
let doc;
|
|
1884
|
+
try {
|
|
1885
|
+
doc = new DOMParser({ onError: onFatal }).parseFromString(content, "text/xml");
|
|
1886
|
+
} catch {
|
|
1887
|
+
throw new AdapterError("INVALID_XML", "The file is not valid XML.");
|
|
1888
|
+
}
|
|
1889
|
+
const root = doc.documentElement;
|
|
1890
|
+
if (root === null || root.localName !== "xliff") {
|
|
1891
|
+
throw new AdapterError("INVALID_STRUCTURE", "The file is not an XLIFF document.");
|
|
1892
|
+
}
|
|
1893
|
+
return { doc, root };
|
|
1894
|
+
}
|
|
1895
|
+
function walkXliff12(root) {
|
|
1896
|
+
const units = [];
|
|
1897
|
+
collectByTag(root, "trans-unit").forEach((tu, index) => {
|
|
1898
|
+
const source = childByName(tu, "source");
|
|
1899
|
+
if (source !== null) {
|
|
1900
|
+
units.push({
|
|
1901
|
+
key: unitKey(tu, index),
|
|
1902
|
+
source,
|
|
1903
|
+
target: childByName(tu, "target"),
|
|
1904
|
+
container: tu
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
return units;
|
|
1909
|
+
}
|
|
1910
|
+
function walkXliff20(root) {
|
|
1911
|
+
const units = [];
|
|
1912
|
+
collectByTag(root, "unit").forEach((unit, index) => {
|
|
1913
|
+
const baseKey = unitKey(unit, index);
|
|
1914
|
+
const segments = elementChildren(unit).filter((el) => el.localName === "segment");
|
|
1915
|
+
segments.forEach((segment, segIndex) => {
|
|
1916
|
+
const source = childByName(segment, "source");
|
|
1917
|
+
if (source !== null) {
|
|
1918
|
+
const key = segments.length > 1 ? `${baseKey}#${segIndex}` : baseKey;
|
|
1919
|
+
units.push({ key, source, target: childByName(segment, "target"), container: segment });
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
});
|
|
1923
|
+
return units;
|
|
1924
|
+
}
|
|
1925
|
+
function walkUnits(root) {
|
|
1926
|
+
const version = root.getAttribute("version") ?? "1.2";
|
|
1927
|
+
return version.startsWith("2") ? walkXliff20(root) : walkXliff12(root);
|
|
1928
|
+
}
|
|
1929
|
+
function innerXml(serializer, element) {
|
|
1930
|
+
return Array.from(element.childNodes).map((node) => serializer.serializeToString(node)).join("");
|
|
1931
|
+
}
|
|
1932
|
+
function unitValue(serializer, unit) {
|
|
1933
|
+
if (unit.target !== null) {
|
|
1934
|
+
const targetXml = innerXml(serializer, unit.target);
|
|
1935
|
+
if (targetXml.trim() !== "") {
|
|
1936
|
+
return targetXml;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return innerXml(serializer, unit.source);
|
|
1940
|
+
}
|
|
1941
|
+
function parseXliffEntries(content, namespace) {
|
|
1942
|
+
const { root } = parseXml(content);
|
|
1943
|
+
const serializer = new XMLSerializer();
|
|
1944
|
+
const out = /* @__PURE__ */ new Map();
|
|
1945
|
+
for (const unit of walkUnits(root)) {
|
|
1946
|
+
const value = unitValue(serializer, unit);
|
|
1947
|
+
out.set(unit.key, {
|
|
1948
|
+
key: unit.key,
|
|
1949
|
+
namespace,
|
|
1950
|
+
value,
|
|
1951
|
+
placeholders: extractXliffPlaceholders(value),
|
|
1952
|
+
isPlural: false
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
return out;
|
|
1956
|
+
}
|
|
1957
|
+
async function readDestination(filePath) {
|
|
1958
|
+
let outcome;
|
|
1959
|
+
try {
|
|
1960
|
+
outcome = await readBounded2(filePath);
|
|
1961
|
+
} catch {
|
|
1962
|
+
throw new AdapterError("INVALID_STRUCTURE", "The destination XLIFF file does not exist.");
|
|
1963
|
+
}
|
|
1964
|
+
if (outcome.kind === "not-a-file") {
|
|
1965
|
+
throw new AdapterError("INVALID_STRUCTURE", "The destination path is not a regular file.");
|
|
1966
|
+
}
|
|
1967
|
+
if (outcome.kind === "too-large") {
|
|
1968
|
+
throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
|
|
1969
|
+
}
|
|
1970
|
+
return outcome.content;
|
|
1971
|
+
}
|
|
1972
|
+
function fragmentNodes(parser, value) {
|
|
1973
|
+
try {
|
|
1974
|
+
const root = parser.parseFromString(`<wrapper>${value}</wrapper>`, "text/xml").documentElement;
|
|
1975
|
+
return root === null ? null : Array.from(root.childNodes);
|
|
1976
|
+
} catch {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function setTargetValue(doc, parser, element, value) {
|
|
1981
|
+
while (element.firstChild !== null) {
|
|
1982
|
+
element.removeChild(element.firstChild);
|
|
1983
|
+
}
|
|
1984
|
+
const nodes = fragmentNodes(parser, value);
|
|
1985
|
+
if (nodes === null) {
|
|
1986
|
+
element.textContent = value;
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
for (const node of nodes) {
|
|
1990
|
+
element.appendChild(doc.importNode(node, true));
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
async function serializeXliffEntries(entries, filePath) {
|
|
1994
|
+
const { doc, root } = parseXml(await readDestination(filePath));
|
|
1995
|
+
const parser = new DOMParser({ onError: onFatal });
|
|
1996
|
+
for (const unit of walkUnits(root)) {
|
|
1997
|
+
const entry = entries.get(unit.key);
|
|
1998
|
+
if (entry !== void 0) {
|
|
1999
|
+
const target = unit.target ?? doc.createElement("target");
|
|
2000
|
+
if (unit.target === null) {
|
|
2001
|
+
unit.container.appendChild(target);
|
|
2002
|
+
}
|
|
2003
|
+
setTargetValue(doc, parser, target, entry.value);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return new XMLSerializer().serializeToString(doc);
|
|
2007
|
+
}
|
|
2008
|
+
function sniffXliff(sample) {
|
|
2009
|
+
const head = sample.trimStart();
|
|
2010
|
+
return head.startsWith("<xliff") || head.startsWith("<?xml");
|
|
2011
|
+
}
|
|
2012
|
+
function createXliffAdapter() {
|
|
2013
|
+
return createFlatFileAdapter({
|
|
2014
|
+
format: "xliff",
|
|
2015
|
+
extensions: [".xlf", ".xliff"],
|
|
2016
|
+
sniff: sniffXliff,
|
|
2017
|
+
parseEntries: parseXliffEntries,
|
|
2018
|
+
serializeEntries: serializeXliffEntries,
|
|
2019
|
+
extractPlaceholders: extractXliffPlaceholders
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
function parseYamlObject(content) {
|
|
2023
|
+
let parsed;
|
|
2024
|
+
try {
|
|
2025
|
+
parsed = parse(content, { maxAliasCount: 100 });
|
|
2026
|
+
} catch {
|
|
2027
|
+
throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
|
|
2028
|
+
}
|
|
2029
|
+
return assertJsonRecord(parsed);
|
|
2030
|
+
}
|
|
2031
|
+
function createYamlAdapter() {
|
|
2032
|
+
return createTreeFileAdapter({
|
|
2033
|
+
format: "yaml",
|
|
2034
|
+
extensions: [".yml", ".yaml"],
|
|
2035
|
+
parse: parseYamlObject,
|
|
2036
|
+
serialize: (tree) => stringify(tree),
|
|
2037
|
+
extractPlaceholders: extractDoubleBracePlaceholders,
|
|
2038
|
+
deriveEntry: (_key, value) => ({
|
|
2039
|
+
placeholders: extractDoubleBracePlaceholders(value),
|
|
2040
|
+
isPlural: false
|
|
2041
|
+
})
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
1470
2044
|
function createDefaultRegistry() {
|
|
1471
|
-
return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter());
|
|
2045
|
+
return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter());
|
|
1472
2046
|
}
|
|
1473
2047
|
|
|
1474
2048
|
// src/selection/select-adapter.ts
|
|
@@ -1483,6 +2057,89 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
|
|
|
1483
2057
|
);
|
|
1484
2058
|
}
|
|
1485
2059
|
|
|
2060
|
+
// src/flow/source.ts
|
|
2061
|
+
async function readSource(config, cwd, fs, adapter) {
|
|
2062
|
+
const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
|
|
2063
|
+
if (!await fs.fileExists(sourcePath)) {
|
|
2064
|
+
throw new SdkError(
|
|
2065
|
+
"SOURCE_UNREADABLE",
|
|
2066
|
+
`The source locale file was not found at ${sourcePath}.`
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
try {
|
|
2070
|
+
return await adapter.read(sourcePath, config.sourceLocale);
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2073
|
+
throw new SdkError(
|
|
2074
|
+
"SOURCE_INVALID",
|
|
2075
|
+
`The source locale file at ${sourcePath} could not be read: ${detail}`
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/flow/diff-locales.ts
|
|
2081
|
+
async function readTarget(cwd, config, adapter, fs, locale) {
|
|
2082
|
+
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
2083
|
+
if (!await fs.fileExists(path)) {
|
|
2084
|
+
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
2085
|
+
}
|
|
2086
|
+
return (await adapter.read(path, locale)).resource;
|
|
2087
|
+
}
|
|
2088
|
+
function selectedLocales(config, requested) {
|
|
2089
|
+
if (requested === void 0) {
|
|
2090
|
+
return config.targetLocales;
|
|
2091
|
+
}
|
|
2092
|
+
const wanted = new Set(requested);
|
|
2093
|
+
return config.targetLocales.filter((locale) => wanted.has(locale));
|
|
2094
|
+
}
|
|
2095
|
+
async function diffLocales(input, deps = {}) {
|
|
2096
|
+
const config = input.config;
|
|
2097
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2098
|
+
const fs = deps.fs ?? defaultFs;
|
|
2099
|
+
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
2100
|
+
const source = await readSource(config, cwd, fs, adapter);
|
|
2101
|
+
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
2102
|
+
return Promise.all(
|
|
2103
|
+
selectedLocales(config, input.locales).map(async (locale) => {
|
|
2104
|
+
const target = await readTarget(cwd, config, adapter, fs, locale);
|
|
2105
|
+
const diff2 = diffResources(source.resource, target, { baseline: baselineFor(lock, locale) });
|
|
2106
|
+
return { locale, diff: diff2 };
|
|
2107
|
+
})
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// src/flow/check.ts
|
|
2112
|
+
function toCheckSummary(locale, diff2) {
|
|
2113
|
+
return {
|
|
2114
|
+
locale,
|
|
2115
|
+
missing: diff2.missing.length,
|
|
2116
|
+
stale: diff2.changed.length,
|
|
2117
|
+
upToDate: diff2.unchanged.length,
|
|
2118
|
+
inSync: diff2.missing.length === 0 && diff2.changed.length === 0
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
async function check(input, deps = {}) {
|
|
2122
|
+
const results = await diffLocales(input, deps);
|
|
2123
|
+
const locales = results.map(({ locale, diff: diff2 }) => toCheckSummary(locale, diff2));
|
|
2124
|
+
return { inSync: locales.every((entry) => entry.inSync), locales };
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/flow/diff.ts
|
|
2128
|
+
function toLocaleDiff(locale, diff2) {
|
|
2129
|
+
return {
|
|
2130
|
+
locale,
|
|
2131
|
+
missing: diff2.missing,
|
|
2132
|
+
changed: diff2.changed,
|
|
2133
|
+
orphaned: diff2.orphaned,
|
|
2134
|
+
hasPendingChanges: diff2.missing.length > 0 || diff2.changed.length > 0
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
async function diff(input, deps = {}) {
|
|
2138
|
+
const results = await diffLocales(input, deps);
|
|
2139
|
+
const locales = results.map(({ locale, diff: result }) => toLocaleDiff(locale, result));
|
|
2140
|
+
return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
|
|
2141
|
+
}
|
|
2142
|
+
|
|
1486
2143
|
// src/selection/select-provider.ts
|
|
1487
2144
|
function selectProvider(config, createProvider = buildProvider) {
|
|
1488
2145
|
try {
|
|
@@ -1511,8 +2168,10 @@ function failureSummary(locale, error) {
|
|
|
1511
2168
|
translated: [],
|
|
1512
2169
|
unchanged: [],
|
|
1513
2170
|
orphaned: [],
|
|
2171
|
+
pruned: [],
|
|
1514
2172
|
invalidIcuSource: [],
|
|
1515
2173
|
integrityMismatches: [],
|
|
2174
|
+
generated: [],
|
|
1516
2175
|
notices: [],
|
|
1517
2176
|
error: describeError(error)
|
|
1518
2177
|
};
|
|
@@ -1535,18 +2194,210 @@ function readNotices(result) {
|
|
|
1535
2194
|
return candidate.filter(isNotice);
|
|
1536
2195
|
}
|
|
1537
2196
|
|
|
2197
|
+
// src/flow/plural-categories.ts
|
|
2198
|
+
var LANGUAGE_CATEGORIES = {
|
|
2199
|
+
ar: ["zero", "one", "two", "few", "many", "other"],
|
|
2200
|
+
cy: ["zero", "one", "two", "few", "many", "other"],
|
|
2201
|
+
ga: ["one", "two", "few", "many", "other"],
|
|
2202
|
+
pl: ["one", "few", "many", "other"],
|
|
2203
|
+
ru: ["one", "few", "many", "other"],
|
|
2204
|
+
uk: ["one", "few", "many", "other"],
|
|
2205
|
+
be: ["one", "few", "many", "other"],
|
|
2206
|
+
lt: ["one", "few", "many", "other"],
|
|
2207
|
+
sl: ["one", "two", "few", "other"]
|
|
2208
|
+
};
|
|
2209
|
+
function isKnownRicherLanguage(locale) {
|
|
2210
|
+
const subtag = locale.toLowerCase().split(/[-_]/)[0] ?? "";
|
|
2211
|
+
return LANGUAGE_CATEGORIES[subtag] !== void 0;
|
|
2212
|
+
}
|
|
2213
|
+
function requiredCategories(locale) {
|
|
2214
|
+
const subtag = locale.toLowerCase().split(/[-_]/)[0] ?? "";
|
|
2215
|
+
return LANGUAGE_CATEGORIES[subtag] ?? ["one", "other"];
|
|
2216
|
+
}
|
|
2217
|
+
function groupPluralSources(source) {
|
|
2218
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2219
|
+
for (const [key, entry] of source.entries) {
|
|
2220
|
+
const baseKey = pluralBaseKey(key);
|
|
2221
|
+
const category = pluralCategoryOf(key);
|
|
2222
|
+
if (baseKey === void 0 || category === void 0) {
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
const group = groups.get(baseKey) ?? /* @__PURE__ */ new Map();
|
|
2226
|
+
group.set(category, entry);
|
|
2227
|
+
groups.set(baseKey, group);
|
|
2228
|
+
}
|
|
2229
|
+
return groups;
|
|
2230
|
+
}
|
|
2231
|
+
function suppliedCategories(groups) {
|
|
2232
|
+
const supplied = /* @__PURE__ */ new Set();
|
|
2233
|
+
for (const group of groups.values()) {
|
|
2234
|
+
for (const category of group.keys()) {
|
|
2235
|
+
supplied.add(category);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return supplied;
|
|
2239
|
+
}
|
|
2240
|
+
function detectMissingPluralCategories(source, targetLocale, format) {
|
|
2241
|
+
if (format !== "i18next-json") {
|
|
2242
|
+
return void 0;
|
|
2243
|
+
}
|
|
2244
|
+
const groups = groupPluralSources(source);
|
|
2245
|
+
const supplied = suppliedCategories(groups);
|
|
2246
|
+
if (supplied.size === 0) {
|
|
2247
|
+
return void 0;
|
|
2248
|
+
}
|
|
2249
|
+
const missing = requiredCategories(targetLocale).filter((category) => !supplied.has(category));
|
|
2250
|
+
if (missing.length === 0) {
|
|
2251
|
+
return void 0;
|
|
2252
|
+
}
|
|
2253
|
+
return {
|
|
2254
|
+
code: "PLURAL_CATEGORIES_INCOMPLETE",
|
|
2255
|
+
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.`
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function targetPluralSetIncomplete(targetKeys, targetLocale) {
|
|
2259
|
+
const required = requiredCategories(targetLocale);
|
|
2260
|
+
const present = /* @__PURE__ */ new Map();
|
|
2261
|
+
for (const key of targetKeys) {
|
|
2262
|
+
const baseKey = pluralBaseKey(key);
|
|
2263
|
+
const category = pluralCategoryOf(key);
|
|
2264
|
+
if (baseKey === void 0 || category === void 0) {
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
const set = present.get(baseKey) ?? /* @__PURE__ */ new Set();
|
|
2268
|
+
set.add(category);
|
|
2269
|
+
present.set(baseKey, set);
|
|
2270
|
+
}
|
|
2271
|
+
for (const categories of present.values()) {
|
|
2272
|
+
if (required.some((category) => !categories.has(category))) {
|
|
2273
|
+
return true;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
return false;
|
|
2277
|
+
}
|
|
2278
|
+
function sourcePluralBaseKeys(source) {
|
|
2279
|
+
const bases = /* @__PURE__ */ new Set();
|
|
2280
|
+
for (const key of source.entries.keys()) {
|
|
2281
|
+
const baseKey = pluralBaseKey(key);
|
|
2282
|
+
if (baseKey !== void 0) {
|
|
2283
|
+
bases.add(baseKey);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
return bases;
|
|
2287
|
+
}
|
|
2288
|
+
function isGeneratedPluralKey(key, sourceBaseKeys) {
|
|
2289
|
+
const baseKey = pluralBaseKey(key);
|
|
2290
|
+
return baseKey !== void 0 && sourceBaseKeys.has(baseKey);
|
|
2291
|
+
}
|
|
2292
|
+
function pluralIncompleteNotice(targetLocale) {
|
|
2293
|
+
return {
|
|
2294
|
+
code: "PLURAL_CATEGORIES_INCOMPLETE",
|
|
2295
|
+
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.`
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
function representativeEntry(group) {
|
|
2299
|
+
return group.get("other") ?? group.get("one") ?? [...group.values()][0];
|
|
2300
|
+
}
|
|
2301
|
+
function planPluralGeneration(source, targetLocale, format) {
|
|
2302
|
+
if (format !== "i18next-json" || !isKnownRicherLanguage(targetLocale)) {
|
|
2303
|
+
return { items: [] };
|
|
2304
|
+
}
|
|
2305
|
+
const required = requiredCategories(targetLocale);
|
|
2306
|
+
const groups = groupPluralSources(source);
|
|
2307
|
+
const items = [];
|
|
2308
|
+
for (const [baseKey, group] of groups) {
|
|
2309
|
+
const representative = representativeEntry(group);
|
|
2310
|
+
if (representative === void 0) {
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
const governingEntries = [...group.values()];
|
|
2314
|
+
for (const category of required) {
|
|
2315
|
+
if (group.has(category)) {
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
items.push({
|
|
2319
|
+
targetKey: makePluralKey(baseKey, category),
|
|
2320
|
+
category,
|
|
2321
|
+
sourceEntry: representative,
|
|
2322
|
+
governingEntries
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
return { items };
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// src/flow/plural-generation.ts
|
|
2330
|
+
function generatedLockHash(governingEntries, category) {
|
|
2331
|
+
const governingHashes = governingEntries.map(contentHash).sort();
|
|
2332
|
+
return contentHash({
|
|
2333
|
+
value: `${category}:${governingHashes.join("|")}`,
|
|
2334
|
+
placeholders: [],
|
|
2335
|
+
isPlural: true
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
function syntheticEntry(item) {
|
|
2339
|
+
return {
|
|
2340
|
+
...item.sourceEntry,
|
|
2341
|
+
key: item.targetKey,
|
|
2342
|
+
isPlural: true,
|
|
2343
|
+
// The CLDR category travels as data context (the meaning field), never the instruction channel.
|
|
2344
|
+
meaning: `CLDR plural category "${item.category}"`
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
function staleItems(items, baseline) {
|
|
2348
|
+
return items.filter((item) => {
|
|
2349
|
+
const hash = generatedLockHash(item.governingEntries, item.category);
|
|
2350
|
+
return baseline.get(item.targetKey) !== hash;
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
function buildRequest2(context, entries) {
|
|
2354
|
+
return {
|
|
2355
|
+
sourceLocale: context.sourceLocale,
|
|
2356
|
+
targetLocale: context.targetLocale,
|
|
2357
|
+
entries,
|
|
2358
|
+
extractPlaceholders: context.adapter.extractPlaceholders,
|
|
2359
|
+
...context.glossary !== void 0 ? { glossary: context.glossary } : {},
|
|
2360
|
+
...context.tone !== void 0 ? { tone: context.tone } : {}
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
async function generatePluralForms(context) {
|
|
2364
|
+
const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
|
|
2365
|
+
const stale = staleItems(plan.items, context.baseline);
|
|
2366
|
+
if (stale.length === 0) {
|
|
2367
|
+
return { accepted: [], withheld: [] };
|
|
2368
|
+
}
|
|
2369
|
+
const entries = stale.map(syntheticEntry);
|
|
2370
|
+
const result = await context.provider.translateBatch(buildRequest2(context, entries));
|
|
2371
|
+
const accepted = [];
|
|
2372
|
+
const withheld = [];
|
|
2373
|
+
for (const item of stale) {
|
|
2374
|
+
const value = result.values.get(item.targetKey);
|
|
2375
|
+
const integrity = result.integrity.get(item.targetKey);
|
|
2376
|
+
if (value !== void 0 && integrity?.matches === true) {
|
|
2377
|
+
accepted.push({
|
|
2378
|
+
targetKey: item.targetKey,
|
|
2379
|
+
entry: { ...syntheticEntry(item), value },
|
|
2380
|
+
lockHash: generatedLockHash(item.governingEntries, item.category)
|
|
2381
|
+
});
|
|
2382
|
+
} else {
|
|
2383
|
+
withheld.push(item.targetKey);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
return { accepted, withheld };
|
|
2387
|
+
}
|
|
2388
|
+
|
|
1538
2389
|
// src/flow/locale-run.ts
|
|
1539
2390
|
function emptyResource(locale, format) {
|
|
1540
2391
|
return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
|
|
1541
2392
|
}
|
|
1542
|
-
async function
|
|
2393
|
+
async function readTarget2(params) {
|
|
1543
2394
|
const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
|
|
1544
2395
|
if (!await params.fs.fileExists(path)) {
|
|
1545
2396
|
return emptyResource(params.targetLocale, params.format);
|
|
1546
2397
|
}
|
|
1547
2398
|
return (await params.adapter.read(path, params.targetLocale)).resource;
|
|
1548
2399
|
}
|
|
1549
|
-
function
|
|
2400
|
+
function buildRequest3(params, entries) {
|
|
1550
2401
|
return {
|
|
1551
2402
|
sourceLocale: params.sourceLocale,
|
|
1552
2403
|
targetLocale: params.targetLocale,
|
|
@@ -1557,27 +2408,58 @@ function buildRequest2(params, entries) {
|
|
|
1557
2408
|
};
|
|
1558
2409
|
}
|
|
1559
2410
|
async function runLocale(params) {
|
|
1560
|
-
const target = await
|
|
1561
|
-
const
|
|
2411
|
+
const target = await readTarget2(params);
|
|
2412
|
+
const diff2 = diffResources(params.source, target, { baseline: params.baseline });
|
|
2413
|
+
const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
|
|
2414
|
+
const pruned = params.prune ? orphaned : [];
|
|
1562
2415
|
const invalidIcu = new Set(params.sourceInvalidIcuKeys);
|
|
1563
|
-
const candidates = [...
|
|
2416
|
+
const candidates = [...diff2.missing, ...diff2.changed];
|
|
1564
2417
|
const toTranslate = candidates.filter((key) => !invalidIcu.has(key));
|
|
1565
2418
|
const invalidIcuSource = candidates.filter((key) => invalidIcu.has(key));
|
|
2419
|
+
const pluralNotice = detectMissingPluralCategories(
|
|
2420
|
+
params.source,
|
|
2421
|
+
params.targetLocale,
|
|
2422
|
+
params.format
|
|
2423
|
+
);
|
|
2424
|
+
const sdkNotices = pluralNotice ? [pluralNotice] : [];
|
|
1566
2425
|
const provider = params.provider;
|
|
1567
2426
|
if (provider === void 0) {
|
|
1568
2427
|
return {
|
|
1569
|
-
summary: baseSummary(
|
|
2428
|
+
summary: baseSummary({
|
|
2429
|
+
locale: params.targetLocale,
|
|
2430
|
+
unchanged: diff2.unchanged,
|
|
2431
|
+
orphaned,
|
|
2432
|
+
invalidIcuSource,
|
|
2433
|
+
translated: toTranslate,
|
|
2434
|
+
generated: [],
|
|
2435
|
+
integrityMismatches: [],
|
|
2436
|
+
pruned,
|
|
2437
|
+
notices: sdkNotices
|
|
2438
|
+
}),
|
|
1570
2439
|
lockEntries: {}
|
|
1571
2440
|
};
|
|
1572
2441
|
}
|
|
1573
2442
|
const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
|
|
1574
2443
|
const accepted = /* @__PURE__ */ new Map();
|
|
1575
2444
|
const integrityMismatches = [];
|
|
1576
|
-
const
|
|
2445
|
+
const subBatchNotices = await translateAndCheck(
|
|
2446
|
+
provider,
|
|
2447
|
+
params,
|
|
2448
|
+
entries,
|
|
2449
|
+
accepted,
|
|
2450
|
+
integrityMismatches
|
|
2451
|
+
);
|
|
1577
2452
|
const merged = new Map(target.entries);
|
|
2453
|
+
for (const key of pruned) {
|
|
2454
|
+
merged.delete(key);
|
|
2455
|
+
}
|
|
1578
2456
|
for (const [key, { value, source }] of accepted) {
|
|
1579
2457
|
merged.set(key, { ...source, value, namespace: target.namespace });
|
|
1580
2458
|
}
|
|
2459
|
+
const generation = await runGeneration(params, provider);
|
|
2460
|
+
for (const form of generation.accepted) {
|
|
2461
|
+
merged.set(form.targetKey, { ...form.entry, namespace: target.namespace });
|
|
2462
|
+
}
|
|
1581
2463
|
const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
|
|
1582
2464
|
await params.adapter.write(
|
|
1583
2465
|
{
|
|
@@ -1588,37 +2470,83 @@ async function runLocale(params) {
|
|
|
1588
2470
|
},
|
|
1589
2471
|
path
|
|
1590
2472
|
);
|
|
1591
|
-
const
|
|
2473
|
+
const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
|
|
2474
|
+
const notices = [...pluralNotices, ...subBatchNotices];
|
|
2475
|
+
const withheld = /* @__PURE__ */ new Set([...integrityMismatches, ...invalidIcuSource, ...generation.withheld]);
|
|
1592
2476
|
return {
|
|
1593
|
-
summary: baseSummary(
|
|
1594
|
-
params.targetLocale,
|
|
1595
|
-
|
|
2477
|
+
summary: baseSummary({
|
|
2478
|
+
locale: params.targetLocale,
|
|
2479
|
+
unchanged: diff2.unchanged,
|
|
2480
|
+
orphaned,
|
|
1596
2481
|
invalidIcuSource,
|
|
1597
|
-
[...accepted.keys()],
|
|
1598
|
-
|
|
2482
|
+
translated: [...accepted.keys()],
|
|
2483
|
+
generated: generation.accepted.map((form) => form.targetKey).sort(),
|
|
2484
|
+
// Withheld generated forms surface alongside withheld translations: both failed integrity.
|
|
2485
|
+
integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
|
|
2486
|
+
pruned,
|
|
1599
2487
|
notices
|
|
1600
|
-
),
|
|
1601
|
-
lockEntries: computeLockEntries(params, merged, withheld)
|
|
2488
|
+
}),
|
|
2489
|
+
lockEntries: computeLockEntries(params, merged, withheld, generation.accepted)
|
|
1602
2490
|
};
|
|
1603
2491
|
}
|
|
1604
|
-
function
|
|
2492
|
+
async function runGeneration(params, provider) {
|
|
2493
|
+
if (!params.generatePlurals || provider.kind !== "llm") {
|
|
2494
|
+
return { accepted: [], withheld: [] };
|
|
2495
|
+
}
|
|
2496
|
+
return generatePluralForms({
|
|
2497
|
+
source: params.source,
|
|
2498
|
+
sourceLocale: params.sourceLocale,
|
|
2499
|
+
targetLocale: params.targetLocale,
|
|
2500
|
+
format: params.format,
|
|
2501
|
+
adapter: params.adapter,
|
|
2502
|
+
provider,
|
|
2503
|
+
glossary: params.glossary,
|
|
2504
|
+
tone: params.tone,
|
|
2505
|
+
baseline: params.baseline
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
function pluralNoticeFor(params, merged) {
|
|
2509
|
+
if (params.format !== "i18next-json") {
|
|
2510
|
+
return [];
|
|
2511
|
+
}
|
|
2512
|
+
if (!targetPluralSetIncomplete(merged.keys(), params.targetLocale)) {
|
|
2513
|
+
return [];
|
|
2514
|
+
}
|
|
2515
|
+
return [pluralIncompleteNotice(params.targetLocale)];
|
|
2516
|
+
}
|
|
2517
|
+
function baseSummary(parts) {
|
|
1605
2518
|
return {
|
|
1606
|
-
locale,
|
|
2519
|
+
locale: parts.locale,
|
|
1607
2520
|
status: "succeeded",
|
|
1608
|
-
translated,
|
|
1609
|
-
unchanged:
|
|
1610
|
-
orphaned:
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
2521
|
+
translated: parts.translated,
|
|
2522
|
+
unchanged: parts.unchanged,
|
|
2523
|
+
orphaned: parts.orphaned,
|
|
2524
|
+
pruned: parts.pruned,
|
|
2525
|
+
invalidIcuSource: parts.invalidIcuSource,
|
|
2526
|
+
integrityMismatches: parts.integrityMismatches,
|
|
2527
|
+
generated: parts.generated,
|
|
2528
|
+
notices: parts.notices
|
|
1614
2529
|
};
|
|
1615
2530
|
}
|
|
1616
2531
|
async function translateAndCheck(provider, params, entries, accepted, integrityMismatches) {
|
|
1617
|
-
|
|
1618
|
-
|
|
2532
|
+
const notices = [];
|
|
2533
|
+
for (const batch of chunk(entries, params.maxBatchSize)) {
|
|
2534
|
+
const subNotices = await runSubBatch(provider, params, batch, accepted, integrityMismatches);
|
|
2535
|
+
notices.push(...subNotices);
|
|
1619
2536
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
2537
|
+
return notices;
|
|
2538
|
+
}
|
|
2539
|
+
async function runSubBatch(provider, params, batch, accepted, integrityMismatches) {
|
|
2540
|
+
let result;
|
|
2541
|
+
try {
|
|
2542
|
+
result = await provider.translateBatch(buildRequest3(params, batch));
|
|
2543
|
+
} catch {
|
|
2544
|
+
for (const entry of batch) {
|
|
2545
|
+
integrityMismatches.push(entry.key);
|
|
2546
|
+
}
|
|
2547
|
+
return [subBatchFailedNotice(batch.length)];
|
|
2548
|
+
}
|
|
2549
|
+
for (const entry of batch) {
|
|
1622
2550
|
const value = result.values.get(entry.key);
|
|
1623
2551
|
const integrity = result.integrity.get(entry.key);
|
|
1624
2552
|
if (value !== void 0 && integrity?.matches === true) {
|
|
@@ -1629,11 +2557,28 @@ async function translateAndCheck(provider, params, entries, accepted, integrityM
|
|
|
1629
2557
|
}
|
|
1630
2558
|
return readNotices(result);
|
|
1631
2559
|
}
|
|
1632
|
-
function
|
|
2560
|
+
function subBatchFailedNotice(count) {
|
|
2561
|
+
return {
|
|
2562
|
+
code: "SUB_BATCH_FAILED",
|
|
2563
|
+
message: `A sub-batch of ${count} entries failed and was withheld; it will be retried next run.`
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
function chunk(items, size) {
|
|
2567
|
+
const chunks = [];
|
|
2568
|
+
for (let index = 0; index < items.length; index += size) {
|
|
2569
|
+
chunks.push(items.slice(index, index + size));
|
|
2570
|
+
}
|
|
2571
|
+
return chunks;
|
|
2572
|
+
}
|
|
2573
|
+
function computeLockEntries(params, merged, withheld, generated) {
|
|
1633
2574
|
const lockEntries = {};
|
|
2575
|
+
const sourceBaseKeys = sourcePluralBaseKeys(params.source);
|
|
1634
2576
|
for (const key of merged.keys()) {
|
|
1635
2577
|
const sourceEntry = params.source.entries.get(key);
|
|
1636
2578
|
if (sourceEntry === void 0) {
|
|
2579
|
+
if (params.generatePlurals) {
|
|
2580
|
+
carryGeneratedLock(lockEntries, params.baseline, key, sourceBaseKeys);
|
|
2581
|
+
}
|
|
1637
2582
|
continue;
|
|
1638
2583
|
}
|
|
1639
2584
|
if (withheld.has(key)) {
|
|
@@ -1645,26 +2590,18 @@ function computeLockEntries(params, merged, withheld) {
|
|
|
1645
2590
|
}
|
|
1646
2591
|
lockEntries[key] = contentHash(sourceEntry);
|
|
1647
2592
|
}
|
|
2593
|
+
for (const form of generated) {
|
|
2594
|
+
lockEntries[form.targetKey] = form.lockHash;
|
|
2595
|
+
}
|
|
1648
2596
|
return lockEntries;
|
|
1649
2597
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
|
|
1654
|
-
if (!await fs.fileExists(sourcePath)) {
|
|
1655
|
-
throw new SdkError(
|
|
1656
|
-
"SOURCE_UNREADABLE",
|
|
1657
|
-
`The source locale file was not found at ${sourcePath}.`
|
|
1658
|
-
);
|
|
2598
|
+
function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
|
|
2599
|
+
if (!isGeneratedPluralKey(key, sourceBaseKeys)) {
|
|
2600
|
+
return;
|
|
1659
2601
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1664
|
-
throw new SdkError(
|
|
1665
|
-
"SOURCE_INVALID",
|
|
1666
|
-
`The source locale file at ${sourcePath} could not be read: ${detail}`
|
|
1667
|
-
);
|
|
2602
|
+
const prior = baseline.get(key);
|
|
2603
|
+
if (prior !== void 0) {
|
|
2604
|
+
lockEntries[key] = prior;
|
|
1668
2605
|
}
|
|
1669
2606
|
}
|
|
1670
2607
|
|
|
@@ -1673,6 +2610,9 @@ async function translate2(input, deps = {}) {
|
|
|
1673
2610
|
const config = input.config;
|
|
1674
2611
|
const cwd = input.cwd ?? process.cwd();
|
|
1675
2612
|
const dryRun = input.dryRun ?? false;
|
|
2613
|
+
const prune = input.prune ?? config.prune ?? false;
|
|
2614
|
+
const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
|
|
2615
|
+
const maxBatchSize = config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
1676
2616
|
const fs = deps.fs ?? defaultFs;
|
|
1677
2617
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
1678
2618
|
const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
|
|
@@ -1695,6 +2635,9 @@ async function translate2(input, deps = {}) {
|
|
|
1695
2635
|
format: config.format,
|
|
1696
2636
|
glossary: config.glossary,
|
|
1697
2637
|
tone: config.tone,
|
|
2638
|
+
prune,
|
|
2639
|
+
generatePlurals,
|
|
2640
|
+
maxBatchSize,
|
|
1698
2641
|
fs
|
|
1699
2642
|
};
|
|
1700
2643
|
const { summary, lockEntries } = await runLocale(params);
|
|
@@ -1870,7 +2813,7 @@ var DEFAULT_WORKBOOK_LIMITS = {
|
|
|
1870
2813
|
maxRowsPerSheet: 1e5,
|
|
1871
2814
|
maxCellsPerRow: 64
|
|
1872
2815
|
};
|
|
1873
|
-
function
|
|
2816
|
+
function assertNoDoctype2(name, xml) {
|
|
1874
2817
|
if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
|
|
1875
2818
|
throw new ExchangeError(
|
|
1876
2819
|
"WORKBOOK_INVALID",
|
|
@@ -1925,7 +2868,7 @@ async function guardWorkbookBytes(bytes, limits) {
|
|
|
1925
2868
|
`The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
|
|
1926
2869
|
);
|
|
1927
2870
|
}
|
|
1928
|
-
|
|
2871
|
+
assertNoDoctype2(file.name, content);
|
|
1929
2872
|
}
|
|
1930
2873
|
}
|
|
1931
2874
|
var rowSchema = z.object({
|
|
@@ -2034,7 +2977,7 @@ async function readWorkbook(bytes, options = {}) {
|
|
|
2034
2977
|
|
|
2035
2978
|
// src/flow/workbook/export-workbook.ts
|
|
2036
2979
|
var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
|
|
2037
|
-
async function
|
|
2980
|
+
async function readTarget3(cwd, config, adapter, fs, locale) {
|
|
2038
2981
|
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
2039
2982
|
if (!await fs.fileExists(path)) {
|
|
2040
2983
|
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
@@ -2042,7 +2985,7 @@ async function readTarget2(cwd, config, adapter, fs, locale) {
|
|
|
2042
2985
|
return (await adapter.read(path, locale)).resource;
|
|
2043
2986
|
}
|
|
2044
2987
|
function buildRows(source, target, baseline, includeUnchanged) {
|
|
2045
|
-
const
|
|
2988
|
+
const diff2 = diffResources(source, target, { baseline });
|
|
2046
2989
|
const rows = [];
|
|
2047
2990
|
const add = (keys, status) => {
|
|
2048
2991
|
for (const key of keys) {
|
|
@@ -2060,14 +3003,14 @@ function buildRows(source, target, baseline, includeUnchanged) {
|
|
|
2060
3003
|
});
|
|
2061
3004
|
}
|
|
2062
3005
|
};
|
|
2063
|
-
add(
|
|
2064
|
-
add(
|
|
3006
|
+
add(diff2.missing, "new");
|
|
3007
|
+
add(diff2.changed, "changed");
|
|
2065
3008
|
if (includeUnchanged) {
|
|
2066
|
-
add(
|
|
3009
|
+
add(diff2.unchanged, "changed");
|
|
2067
3010
|
}
|
|
2068
3011
|
return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
2069
3012
|
}
|
|
2070
|
-
function
|
|
3013
|
+
function selectedLocales2(config, requested) {
|
|
2071
3014
|
if (requested === void 0) {
|
|
2072
3015
|
return config.targetLocales;
|
|
2073
3016
|
}
|
|
@@ -2081,10 +3024,10 @@ async function exportWorkbook(input, deps = {}) {
|
|
|
2081
3024
|
const adapter = selectAdapter(config.format, deps.adapterRegistry);
|
|
2082
3025
|
const source = await readSource(config, cwd, fs, adapter);
|
|
2083
3026
|
const lock = await readLockFile(lockFilePath(cwd), fs);
|
|
2084
|
-
const locales =
|
|
3027
|
+
const locales = selectedLocales2(config, input.locales);
|
|
2085
3028
|
const sheets = await Promise.all(
|
|
2086
3029
|
locales.map(async (locale) => {
|
|
2087
|
-
const target = await
|
|
3030
|
+
const target = await readTarget3(cwd, config, adapter, fs, locale);
|
|
2088
3031
|
const rows = buildRows(
|
|
2089
3032
|
source.resource,
|
|
2090
3033
|
target,
|
|
@@ -2154,7 +3097,7 @@ function classifyRows(params, buckets) {
|
|
|
2154
3097
|
}
|
|
2155
3098
|
}
|
|
2156
3099
|
function importLocale(params) {
|
|
2157
|
-
const
|
|
3100
|
+
const diff2 = diffResources(params.source, params.target, { baseline: params.baseline });
|
|
2158
3101
|
const buckets = { accepted: /* @__PURE__ */ new Map(), mismatches: [], withheld: /* @__PURE__ */ new Set() };
|
|
2159
3102
|
classifyRows(params, buckets);
|
|
2160
3103
|
const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
|
|
@@ -2163,10 +3106,14 @@ function importLocale(params) {
|
|
|
2163
3106
|
locale: params.sheet.locale,
|
|
2164
3107
|
status: "succeeded",
|
|
2165
3108
|
translated: [...buckets.accepted.keys()].sort(),
|
|
2166
|
-
unchanged:
|
|
2167
|
-
orphaned:
|
|
3109
|
+
unchanged: diff2.unchanged,
|
|
3110
|
+
orphaned: diff2.orphaned,
|
|
3111
|
+
// Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
|
|
3112
|
+
pruned: [],
|
|
2168
3113
|
invalidIcuSource,
|
|
2169
3114
|
integrityMismatches: [...buckets.mismatches].sort(),
|
|
3115
|
+
// Plural generation is a translate-flow concern; the manual workbook import never generates forms.
|
|
3116
|
+
generated: [],
|
|
2170
3117
|
notices: []
|
|
2171
3118
|
};
|
|
2172
3119
|
return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
|
|
@@ -2187,7 +3134,7 @@ async function readWorkbookBytes(path, fs) {
|
|
|
2187
3134
|
}
|
|
2188
3135
|
return read.bytes;
|
|
2189
3136
|
}
|
|
2190
|
-
async function
|
|
3137
|
+
async function readTarget4(cwd, config, adapter, fs, locale) {
|
|
2191
3138
|
const path = localeFilePath(cwd, config.files.pattern, locale);
|
|
2192
3139
|
if (!await fs.fileExists(path)) {
|
|
2193
3140
|
return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
|
|
@@ -2226,7 +3173,7 @@ async function runSheet(ctx, sheet, lock) {
|
|
|
2226
3173
|
`The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
|
|
2227
3174
|
);
|
|
2228
3175
|
}
|
|
2229
|
-
const target = await
|
|
3176
|
+
const target = await readTarget4(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
|
|
2230
3177
|
const baseline = baselineFor(lock, sheet.locale);
|
|
2231
3178
|
const { summary, accepted, withheld } = importLocale({
|
|
2232
3179
|
sheet,
|
|
@@ -2296,6 +3243,16 @@ async function importWorkbook(input, deps = {}) {
|
|
|
2296
3243
|
const { succeeded, failed } = partition(summaries);
|
|
2297
3244
|
return { dryRun, locales: summaries, succeeded, failed };
|
|
2298
3245
|
}
|
|
3246
|
+
|
|
3247
|
+
// src/scaffolding.ts
|
|
3248
|
+
var scaffoldingMetadata = {
|
|
3249
|
+
/** Provider id -> the environment variable its API key is read from. Owned by ai-providers. */
|
|
3250
|
+
providerEnv: PROVIDER_ENV,
|
|
3251
|
+
/** LLM provider id -> a cosmetic default scaffold model. Owned by ai-providers. DeepL has none. */
|
|
3252
|
+
scaffoldModels: SCAFFOLD_MODELS,
|
|
3253
|
+
/** The closed set of source format ids. Owned by core. */
|
|
3254
|
+
supportedFormats: SUPPORTED_FORMATS
|
|
3255
|
+
};
|
|
2299
3256
|
var defaultCreateWatcher = (paths) => {
|
|
2300
3257
|
const fsWatcher = watch$1([...paths], { persistent: true, ignoreInitial: true });
|
|
2301
3258
|
return {
|
|
@@ -2402,6 +3359,6 @@ async function watch(input, deps = {}) {
|
|
|
2402
3359
|
return { stop };
|
|
2403
3360
|
}
|
|
2404
3361
|
|
|
2405
|
-
export { DEFAULT_WORKBOOK_PATH, SdkError, defineConfig, exportWorkbook, importWorkbook, loadConfig, translate2 as translate, verbatraConfigSchema, watch };
|
|
3362
|
+
export { DEFAULT_WORKBOOK_PATH, SdkError, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
|
|
2406
3363
|
//# sourceMappingURL=index.js.map
|
|
2407
3364
|
//# sourceMappingURL=index.js.map
|