@x18n/core 0.0.2 → 0.0.5

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/dist/index.mjs ADDED
@@ -0,0 +1,3046 @@
1
+ import * as v from "valibot";
2
+ //#region src/text.ts
3
+ /** One edit covering only the part that differs, so bytes outside it stay the same. */
4
+ function diffText(before, after) {
5
+ if (before === after) return [];
6
+ let start = 0;
7
+ const max = Math.min(before.length, after.length);
8
+ while (start < max && before.charCodeAt(start) === after.charCodeAt(start)) start++;
9
+ let endBefore = before.length;
10
+ let endAfter = after.length;
11
+ while (endBefore > start && endAfter > start && before.charCodeAt(endBefore - 1) === after.charCodeAt(endAfter - 1)) {
12
+ endBefore--;
13
+ endAfter--;
14
+ }
15
+ return [{
16
+ range: {
17
+ start,
18
+ end: endBefore
19
+ },
20
+ text: after.slice(start, endAfter)
21
+ }];
22
+ }
23
+ /** Applies non-overlapping edits given against the same text. */
24
+ function applyTextEdits(text, edits) {
25
+ const sorted = [...edits].sort((a, b) => b.range.start - a.range.start || b.range.end - a.range.end);
26
+ let out = text;
27
+ for (const edit of sorted) out = out.slice(0, edit.range.start) + edit.text + out.slice(edit.range.end);
28
+ return out;
29
+ }
30
+ const lineIndexes = /* @__PURE__ */ new Map();
31
+ /** Line index of a text, cached for the few texts used at a time (reporters, LSP ranges). */
32
+ function createLineIndex(text) {
33
+ let index = lineIndexes.get(text);
34
+ if (!index) {
35
+ if (lineIndexes.size >= 16) lineIndexes.delete(lineIndexes.keys().next().value);
36
+ index = buildLineIndex(text);
37
+ lineIndexes.set(text, index);
38
+ }
39
+ return index;
40
+ }
41
+ function buildLineIndex(text) {
42
+ const starts = [0];
43
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
44
+ return {
45
+ lineCount: starts.length,
46
+ position(offset) {
47
+ let low = 0;
48
+ let high = starts.length - 1;
49
+ while (low < high) {
50
+ const mid = low + high + 1 >> 1;
51
+ if (starts[mid] <= offset) low = mid;
52
+ else high = mid - 1;
53
+ }
54
+ return {
55
+ line: low,
56
+ character: offset - starts[low]
57
+ };
58
+ },
59
+ offset(position) {
60
+ const start = starts[Math.min(position.line, starts.length - 1)] ?? 0;
61
+ return Math.min(start + position.character, text.length);
62
+ },
63
+ line(line) {
64
+ const start = starts[line] ?? text.length;
65
+ const end = starts[line + 1] ?? text.length;
66
+ return text.slice(start, end).replace(/\r?\n$/, "");
67
+ }
68
+ };
69
+ }
70
+ function rangeContains(range, offset) {
71
+ return offset >= range.start && offset <= range.end;
72
+ }
73
+ /** Fast non-cryptographic string hash (cyrb53), as hex. */
74
+ function hashText(text) {
75
+ let h1 = 3735928559;
76
+ let h2 = 1103547991;
77
+ for (let i = 0; i < text.length; i++) {
78
+ const ch = text.charCodeAt(i);
79
+ h1 = Math.imul(h1 ^ ch, 2654435761);
80
+ h2 = Math.imul(h2 ^ ch, 1597334677);
81
+ }
82
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
83
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
84
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
85
+ }
86
+ //#endregion
87
+ //#region src/contracts.ts
88
+ /** Version of every JSON contract (CLI `--json`, LSP `x18n/*`, RPC, MCP). Bumped on breaking changes to any `data` shape. */
89
+ const contractVersion = 1;
90
+ /** Line and column form of an offset range in a text. */
91
+ function toWireRange(text, start, end) {
92
+ const index = createLineIndex(text ?? "");
93
+ const from = index.position(start);
94
+ const to = index.position(end);
95
+ return {
96
+ line: from.line + 1,
97
+ column: from.character + 1,
98
+ endLine: to.line + 1,
99
+ endColumn: to.character + 1,
100
+ start,
101
+ end
102
+ };
103
+ }
104
+ function toWireDiagnostic(project, diagnostic, text) {
105
+ const location = diagnostic.location;
106
+ return {
107
+ project,
108
+ code: diagnostic.code,
109
+ severity: diagnostic.severity,
110
+ message: diagnostic.message,
111
+ key: diagnostic.key,
112
+ locale: diagnostic.locale,
113
+ file: location?.path,
114
+ range: location ? toWireRange(text(location.path), location.range.start, location.range.end) : void 0,
115
+ fixes: diagnostic.fixes
116
+ };
117
+ }
118
+ function envelope(command, data, diagnostics = [], errors = []) {
119
+ return {
120
+ x18n: 1,
121
+ command,
122
+ data,
123
+ diagnostics,
124
+ errors
125
+ };
126
+ }
127
+ //#endregion
128
+ //#region src/define.ts
129
+ function definePlugin(plugin) {
130
+ return plugin;
131
+ }
132
+ function definePreset(preset) {
133
+ return preset;
134
+ }
135
+ function defineConfig(config) {
136
+ return config;
137
+ }
138
+ //#endregion
139
+ //#region src/errors.ts
140
+ const errorCodes = {
141
+ X18N0001: {
142
+ message: "Invalid config",
143
+ hint: "Check the reported field against the config schema (`x18n schema config`)."
144
+ },
145
+ X18N0002: {
146
+ message: "Config file could not be loaded",
147
+ hint: "Fix the syntax error, or run `x18n doctor` for details."
148
+ },
149
+ X18N0003: {
150
+ message: "Unknown project",
151
+ hint: "Pick a project name listed by `x18n status`."
152
+ },
153
+ X18N1001: {
154
+ message: "Plugin could not be loaded",
155
+ hint: "Install the plugin package, or check the path."
156
+ },
157
+ X18N1002: {
158
+ message: "Duplicate capability id",
159
+ hint: "Rename the id, or set `override: true` on the later one."
160
+ },
161
+ X18N1003: {
162
+ message: "Unknown preset",
163
+ hint: "Use a preset name listed in the docs, or a preset object."
164
+ },
165
+ X18N1004: {
166
+ message: "Invalid YAML plugin",
167
+ hint: "Validate the file against `schema/plugin.json`."
168
+ },
169
+ X18N2001: {
170
+ message: "No format for resource",
171
+ hint: "Map the extension in `formats`, or add a format plugin."
172
+ },
173
+ X18N2002: {
174
+ message: "Resource could not be parsed",
175
+ hint: "Fix the syntax error in the file."
176
+ },
177
+ X18N2003: {
178
+ message: "Resource is not an object of messages",
179
+ hint: "Locale files hold nested objects of strings, numbers, booleans or null."
180
+ },
181
+ X18N2004: {
182
+ message: "Locale file is not static",
183
+ hint: "Use JSON or YAML, or opt in to execution with `execute: true` on the resource pattern."
184
+ },
185
+ X18N3001: {
186
+ message: "Source file could not be scanned",
187
+ hint: "Fix the syntax error, or exclude the file with `usage.exclude`."
188
+ },
189
+ X18N4001: {
190
+ message: "Resource is read-only",
191
+ hint: "Edit the source of computed values, or use a writable format."
192
+ },
193
+ X18N4002: {
194
+ message: "Rename target already exists",
195
+ hint: "Delete or rename the existing key first."
196
+ },
197
+ X18N4003: {
198
+ message: "A decision is needed",
199
+ hint: "Answer the listed decisions, or pass `--yes` to accept defaults."
200
+ },
201
+ X18N4004: {
202
+ message: "Plan is stale",
203
+ hint: "Files changed since the plan was made. Plan again."
204
+ },
205
+ X18N4005: {
206
+ message: "No resource for the key",
207
+ hint: "Add a resource pattern for the locale, or create the locale file."
208
+ },
209
+ X18N4006: {
210
+ message: "Key not found",
211
+ hint: "List keys with `x18n keys`."
212
+ },
213
+ X18N4007: {
214
+ message: "Placeholder mismatch",
215
+ hint: "Keep every placeholder of the source value, or pass `--force`."
216
+ },
217
+ X18N4008: {
218
+ message: "Invalid edit input",
219
+ hint: "Check the input shape against the docs."
220
+ }
221
+ };
222
+ var X18nError = class extends Error {
223
+ code;
224
+ hint;
225
+ docs;
226
+ details;
227
+ constructor(code, detail, details = []) {
228
+ const base = errorCodes[code];
229
+ super(detail ? `${base.message}: ${detail}` : base.message);
230
+ this.name = "X18nError";
231
+ this.code = code;
232
+ this.hint = base.hint;
233
+ this.docs = `https://x18n.antfu.dev/errors/${code.toLowerCase()}`;
234
+ this.details = details;
235
+ }
236
+ toJSON() {
237
+ return {
238
+ code: this.code,
239
+ message: this.message,
240
+ hint: this.hint,
241
+ docs: this.docs
242
+ };
243
+ }
244
+ };
245
+ function isX18nError(error) {
246
+ return error instanceof X18nError;
247
+ }
248
+ //#endregion
249
+ //#region src/keys.ts
250
+ /** Canonical string form of a key: `ns:a.b.c`, or `a.b.c` without namespace. */
251
+ function joinKey(namespace, path) {
252
+ const dotted = path.join(".");
253
+ return namespace === void 0 ? dotted : `${namespace}:${dotted}`;
254
+ }
255
+ /** Splits a key string. `namespaced` is false for projects without namespaces, where `:` is part of the key. */
256
+ function splitKey(key, namespaced = true) {
257
+ const index = namespaced ? key.indexOf(":") : -1;
258
+ const namespace = index === -1 ? void 0 : key.slice(0, index);
259
+ const rest = index === -1 ? key : key.slice(index + 1);
260
+ return {
261
+ namespace,
262
+ path: rest === "" ? [] : rest.split(".")
263
+ };
264
+ }
265
+ /** True when `key` is `prefix` itself or lies under it (`a.b` is under `a`). */
266
+ function isKeyUnder(key, prefix) {
267
+ if (key === prefix) return true;
268
+ const sep = prefix.endsWith(":") ? "" : ".";
269
+ return key.startsWith(prefix + sep);
270
+ }
271
+ /** Replaces the `from` prefix of `key` with `to`. */
272
+ function replaceKeyPrefix(key, from, to) {
273
+ return key === from ? to : to + key.slice(from.length);
274
+ }
275
+ function escapeRegExp(text) {
276
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
277
+ }
278
+ /**
279
+ * Glob over paths. `**` spans separators, `*` and `?` stay inside a segment,
280
+ * `{a,b}` alternates, `[...]` is a character class.
281
+ */
282
+ function globToRegExp(glob, separator = "/") {
283
+ const sep = escapeRegExp(separator);
284
+ const notSep = `[^${sep}]`;
285
+ let out = "";
286
+ let depth = 0;
287
+ for (let i = 0; i < glob.length; i++) {
288
+ const char = glob[i];
289
+ if (char === "*") {
290
+ if (glob[i + 1] === "*") {
291
+ const slashAfter = glob[i + 2] === separator;
292
+ const atStart = i === 0 || glob[i - 1] === separator;
293
+ i++;
294
+ if (slashAfter && atStart) {
295
+ out += `(?:.*${sep})?`;
296
+ i++;
297
+ } else out += ".*";
298
+ } else out += `${notSep}*`;
299
+ } else if (char === "?") out += notSep;
300
+ else if (char === "{") {
301
+ depth++;
302
+ out += "(?:";
303
+ } else if (char === "}" && depth > 0) {
304
+ depth--;
305
+ out += ")";
306
+ } else if (char === "," && depth > 0) out += "|";
307
+ else if (char === "[") {
308
+ const close = glob.indexOf("]", i);
309
+ if (close === -1) out += "\\[";
310
+ else {
311
+ out += `[${glob.slice(i + 1, close).replace(/^!/, "^").replace(/\\/g, "\\\\")}]`;
312
+ i = close;
313
+ }
314
+ } else out += escapeRegExp(char);
315
+ }
316
+ return new RegExp(`^${out}$`);
317
+ }
318
+ /** Matcher over key globs: `*` stays inside a segment, `**` spans segments. */
319
+ function keyMatcher(globs) {
320
+ if (globs.length === 0) return () => false;
321
+ const regexes = globs.map((glob) => globToRegExp(glob, "."));
322
+ return (key) => regexes.some((re) => re.test(key));
323
+ }
324
+ /** Matcher over posix file paths. */
325
+ function pathMatcher(globs) {
326
+ if (globs.length === 0) return () => false;
327
+ const regexes = globs.map((glob) => globToRegExp(glob.replace(/^\.\//, "")));
328
+ return (path) => regexes.some((re) => re.test(path));
329
+ }
330
+ //#endregion
331
+ //#region src/locale.ts
332
+ const defaultRegions = {
333
+ ar: "SA",
334
+ cs: "CZ",
335
+ da: "DK",
336
+ de: "DE",
337
+ el: "GR",
338
+ en: "US",
339
+ es: "ES",
340
+ fa: "IR",
341
+ fi: "FI",
342
+ fr: "FR",
343
+ he: "IL",
344
+ hi: "IN",
345
+ hu: "HU",
346
+ id: "ID",
347
+ it: "IT",
348
+ ja: "JP",
349
+ ko: "KR",
350
+ nb: "NO",
351
+ nl: "NL",
352
+ pl: "PL",
353
+ pt: "PT",
354
+ ro: "RO",
355
+ ru: "RU",
356
+ sv: "SE",
357
+ th: "TH",
358
+ tr: "TR",
359
+ uk: "UA",
360
+ vi: "VN",
361
+ zh: "CN"
362
+ };
363
+ const pluralOrder = [
364
+ "zero",
365
+ "one",
366
+ "two",
367
+ "few",
368
+ "many",
369
+ "other"
370
+ ];
371
+ function flagOf(region) {
372
+ if (!/^[A-Z]{2}$/.test(region)) return void 0;
373
+ return String.fromCodePoint(...[...region].map((c) => 127462 + c.charCodeAt(0) - 65));
374
+ }
375
+ function safe(fn) {
376
+ try {
377
+ return fn();
378
+ } catch {
379
+ return;
380
+ }
381
+ }
382
+ /** Lenient display parse of a locale code (`zh_CN`, `pt-BR`, `sr-Latn-RS`). Never changes the code. */
383
+ function parseLocaleInfo(code) {
384
+ const parts = code.replace(/_/g, "-").split("-").filter(Boolean);
385
+ const language = parts[0] && /^[a-z]{2,3}$/i.test(parts[0]) ? parts[0].toLowerCase() : void 0;
386
+ let script;
387
+ let region;
388
+ for (const part of parts.slice(1)) if (!script && !region && /^[a-z]{4}$/i.test(part)) script = part[0].toUpperCase() + part.slice(1).toLowerCase();
389
+ else if (!region && /^(?:[a-z]{2}|\d{3})$/i.test(part)) region = part.toUpperCase();
390
+ const tag = [
391
+ language,
392
+ script,
393
+ region
394
+ ].filter(Boolean).join("-");
395
+ const displayName = language ? safe(() => new Intl.DisplayNames(["en"], { type: "language" }).of(tag)) : void 0;
396
+ const found = language ? safe(() => new Intl.PluralRules(tag).resolvedOptions().pluralCategories) : void 0;
397
+ const pluralCategories = found ? pluralOrder.filter((category) => found.includes(category)) : ["other"];
398
+ const flagRegion = region ?? (language === "zh" && script === "Hant" ? "TW" : language ? defaultRegions[language] : void 0);
399
+ return {
400
+ code,
401
+ language,
402
+ region,
403
+ displayName,
404
+ flag: flagRegion ? flagOf(flagRegion) : void 0,
405
+ pluralCategories
406
+ };
407
+ }
408
+ /** Codes that differ only in spelling (`zh_CN` and `zh-cn`), the same after a lenient normalization. */
409
+ function localeSpellingKey(code) {
410
+ return code.replace(/_/g, "-").toLowerCase();
411
+ }
412
+ //#endregion
413
+ //#region src/memory-host.ts
414
+ /** A host over in-memory files, for tests, demos and agents that pass content directly. */
415
+ function createMemoryHost(initial = {}, root = "/") {
416
+ const files = new Map(Object.entries(initial));
417
+ return {
418
+ root,
419
+ files,
420
+ readFile: async (path) => files.get(path),
421
+ writeFile: async (path, content) => {
422
+ files.set(path, content);
423
+ },
424
+ deleteFile: async (path) => {
425
+ files.delete(path);
426
+ },
427
+ glob: async (patterns, options = {}) => {
428
+ const include = pathMatcher(patterns);
429
+ const ignore = pathMatcher(options.ignore ?? []);
430
+ return [...files.keys()].filter((path) => include(path) && !ignore(path)).sort();
431
+ }
432
+ };
433
+ }
434
+ //#endregion
435
+ //#region src/message-tree.ts
436
+ function isMessageLeaf(value) {
437
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
438
+ }
439
+ /** Checks parsed data (for example from `JSON.parse`) before it enters the project. */
440
+ function isMessageTree(value) {
441
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
442
+ return Object.values(value).every((child) => isMessageLeaf(child) || isMessageTree(child));
443
+ }
444
+ /** Stable id of a stored property path, the key of `ParseResult.ranges`. */
445
+ function pathId(path) {
446
+ return path.join("\0");
447
+ }
448
+ function* walkLeaves(tree, prefix = []) {
449
+ for (const [segment, child] of Object.entries(tree)) {
450
+ const path = [...prefix, segment];
451
+ if (isMessageLeaf(child)) yield {
452
+ path,
453
+ value: child
454
+ };
455
+ else yield* walkLeaves(child, path);
456
+ }
457
+ }
458
+ function cloneTree(tree) {
459
+ const out = {};
460
+ for (const [segment, child] of Object.entries(tree)) out[segment] = isMessageLeaf(child) ? child : cloneTree(child);
461
+ return out;
462
+ }
463
+ function setIn(tree, path, value) {
464
+ let node = tree;
465
+ for (const segment of path.slice(0, -1)) {
466
+ const child = node[segment];
467
+ if (child === void 0 || isMessageLeaf(child)) {
468
+ const created = {};
469
+ node[segment] = created;
470
+ node = created;
471
+ } else node = child;
472
+ }
473
+ node[path.at(-1)] = value;
474
+ }
475
+ function deleteIn(tree, path) {
476
+ const parents = [tree];
477
+ let node = tree;
478
+ for (const segment of path.slice(0, -1)) {
479
+ const child = node[segment];
480
+ if (child === void 0 || isMessageLeaf(child)) return;
481
+ node = child;
482
+ parents.push(node);
483
+ }
484
+ delete node[path.at(-1)];
485
+ for (let i = parents.length - 1; i > 0; i--) {
486
+ if (Object.keys(parents[i]).length > 0) break;
487
+ delete parents[i - 1][path[i - 1]];
488
+ }
489
+ }
490
+ /** Applies ops to a copy of the tree. Parent objects left empty are removed. */
491
+ function applyMessageOps(tree, ops) {
492
+ const out = cloneTree(tree);
493
+ for (const op of ops) if (op.type === "set") setIn(out, op.path, op.value);
494
+ else deleteIn(out, op.path);
495
+ return out;
496
+ }
497
+ /** Sorts keys at every level. */
498
+ function sortTree(tree, compare) {
499
+ const out = {};
500
+ for (const segment of Object.keys(tree).sort(compare)) {
501
+ const child = tree[segment];
502
+ out[segment] = isMessageLeaf(child) ? child : sortTree(child, compare);
503
+ }
504
+ return out;
505
+ }
506
+ /** Builds a nested or flat tree from leaves. */
507
+ function buildTree(leaves) {
508
+ const out = {};
509
+ for (const leaf of leaves) setIn(out, leaf.path, leaf.value);
510
+ return out;
511
+ }
512
+ //#endregion
513
+ //#region src/overlay-host.ts
514
+ /** Wraps a host so open editor buffers shadow the content on disk. */
515
+ function createOverlayHost(base) {
516
+ const overlays = /* @__PURE__ */ new Map();
517
+ return {
518
+ ...base,
519
+ readFile: async (path) => overlays.get(path) ?? base.readFile(path),
520
+ async writeFile(path, content) {
521
+ overlays.delete(path);
522
+ await base.writeFile(path, content);
523
+ },
524
+ setOverlay: (path, content) => {
525
+ overlays.set(path, content);
526
+ },
527
+ clearOverlay: (path) => {
528
+ overlays.delete(path);
529
+ },
530
+ hasOverlay: (path) => overlays.has(path)
531
+ };
532
+ }
533
+ //#endregion
534
+ //#region src/plugins.ts
535
+ function isPreset(value) {
536
+ return "plugins" in value && Array.isArray(value.plugins);
537
+ }
538
+ function extensionOf(path) {
539
+ const base = path.slice(path.lastIndexOf("/") + 1);
540
+ const dot = base.lastIndexOf(".");
541
+ return dot <= 0 ? "" : base.slice(dot + 1).toLowerCase();
542
+ }
543
+ function sortByEnforce(plugins) {
544
+ const rank = (plugin) => plugin.enforce === "pre" ? 0 : plugin.enforce === "post" ? 2 : 1;
545
+ return [...plugins].sort((a, b) => rank(a) - rank(b));
546
+ }
547
+ function uniqueById(items, kind) {
548
+ const out = /* @__PURE__ */ new Map();
549
+ for (const item of items) {
550
+ if (out.has(item.id) && !item.override) throw new X18nError("X18N1002", `${kind} "${item.id}"`);
551
+ out.set(item.id, item);
552
+ }
553
+ return out;
554
+ }
555
+ function createRegistry(input, config) {
556
+ const plugins = sortByEnforce(input);
557
+ const formats = plugins.flatMap((plugin) => plugin.formats ?? []);
558
+ const scanners = plugins.flatMap((plugin) => plugin.scanners ?? []);
559
+ const finders = [...uniqueById(plugins.flatMap((plugin) => plugin.localeFinders ?? []), "Locale finder").values()];
560
+ const checks = uniqueById(plugins.flatMap((plugin) => plugin.checks ?? []), "Check");
561
+ const reporters = uniqueById(plugins.flatMap((plugin) => plugin.reporters ?? []), "Reporter");
562
+ const conflicts = [];
563
+ const byExtension = /* @__PURE__ */ new Map();
564
+ for (const format of formats) for (const extension of format.extensions) {
565
+ const winner = byExtension.get(extension);
566
+ if (winner) conflicts.push({
567
+ capability: "format",
568
+ extension,
569
+ winner: winner.id,
570
+ loser: format.id
571
+ });
572
+ else byExtension.set(extension, format);
573
+ }
574
+ const formatById = (id) => formats.find((format) => format.id === id);
575
+ const scannerCache = /* @__PURE__ */ new Map();
576
+ return {
577
+ plugins,
578
+ formats,
579
+ finders,
580
+ scanners,
581
+ checks,
582
+ reporters,
583
+ placeholders: plugins.flatMap((plugin) => plugin.placeholders ?? []),
584
+ manifests: plugins.flatMap((plugin) => plugin.manifests ?? []),
585
+ extractors: plugins.flatMap((plugin) => plugin.extractors ?? []),
586
+ histories: plugins.flatMap((plugin) => plugin.histories ?? []),
587
+ conflicts,
588
+ formatById,
589
+ formatFor(path, hint) {
590
+ if (hint) return formatById(hint);
591
+ const extension = extensionOf(path);
592
+ const mapped = config.formats[extension];
593
+ return mapped ? formatById(mapped) : byExtension.get(extension);
594
+ },
595
+ scannersFor(path) {
596
+ const extension = extensionOf(path);
597
+ let list = scannerCache.get(extension);
598
+ if (!list) {
599
+ list = extension ? scanners.filter((scanner) => scanner.extensions.includes(extension) || scanner.extensions.includes("*")) : [];
600
+ scannerCache.set(extension, list);
601
+ }
602
+ return list;
603
+ }
604
+ };
605
+ }
606
+ //#endregion
607
+ //#region src/schema.ts
608
+ function isJsonValue(value) {
609
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return true;
610
+ if (Array.isArray(value)) return value.every(isJsonValue);
611
+ return isJsonObject(value);
612
+ }
613
+ function isJsonObject(value) {
614
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every(isJsonValue);
615
+ }
616
+ const jsonObject = v.custom(isJsonObject, "Expected a JSON object");
617
+ const strings = v.array(v.string());
618
+ const level = v.picklist([
619
+ "error",
620
+ "warning",
621
+ "info",
622
+ "hint",
623
+ "off"
624
+ ]);
625
+ const argSource = v.strictObject({
626
+ arg: v.number(),
627
+ property: v.optional(v.string())
628
+ });
629
+ const callSpec = v.strictObject({
630
+ callee: v.union([v.string(), strings]),
631
+ kind: v.optional(v.picklist([
632
+ "call",
633
+ "selector",
634
+ "accessor"
635
+ ])),
636
+ key: v.optional(v.union([v.number(), argSource])),
637
+ namespace: v.optional(argSource),
638
+ keyPrefix: v.optional(argSource),
639
+ factory: v.optional(v.boolean()),
640
+ returns: v.optional(strings),
641
+ types: v.optional(strings)
642
+ });
643
+ const regexSpec = v.strictObject({
644
+ extensions: strings,
645
+ patterns: strings
646
+ });
647
+ const resourceSpec = v.strictObject({
648
+ pattern: v.string(),
649
+ format: v.optional(v.string()),
650
+ namespace: v.optional(v.string()),
651
+ locale: v.optional(v.string()),
652
+ execute: v.optional(v.boolean())
653
+ });
654
+ const pluginSpecifier = v.union([v.string(), v.tuple([v.string(), jsonObject])]);
655
+ const extractTemplate = v.strictObject({
656
+ template: v.string(),
657
+ kinds: v.optional(strings),
658
+ files: v.optional(strings),
659
+ imports: v.optional(strings)
660
+ });
661
+ const extractTemplates = v.array(v.union([v.string(), extractTemplate]));
662
+ const extractOptions = v.strictObject({
663
+ autoDetect: v.optional(v.boolean()),
664
+ keygen: v.optional(v.picklist([
665
+ "slug",
666
+ "source",
667
+ "random",
668
+ "empty"
669
+ ])),
670
+ keyCase: v.optional(v.picklist([
671
+ "camel",
672
+ "snake",
673
+ "kebab",
674
+ "dot"
675
+ ])),
676
+ keyPrefix: v.optional(v.string()),
677
+ keyMaxLength: v.optional(v.number()),
678
+ reuseExisting: v.optional(v.boolean()),
679
+ ignore: v.optional(strings),
680
+ ignoreByFile: v.optional(v.record(v.string(), strings)),
681
+ ignoreAttributes: v.optional(strings),
682
+ ignoreCallees: v.optional(strings),
683
+ templates: v.optional(extractTemplates)
684
+ });
685
+ const glossaryEntry = v.strictObject({
686
+ term: v.string(),
687
+ translations: v.optional(v.record(v.string(), v.string())),
688
+ note: v.optional(v.string()),
689
+ doNotTranslate: v.optional(v.boolean())
690
+ });
691
+ const checkSetting = v.union([level, v.tuple([level, jsonObject])]);
692
+ const namespaceOptions = v.strictObject({
693
+ separator: v.optional(v.string()),
694
+ default: v.optional(v.string()),
695
+ fromPath: v.optional(v.picklist([
696
+ "dot",
697
+ "slash",
698
+ "camel"
699
+ ]))
700
+ });
701
+ /** Validates YAML and JSON configs, and the serializable part of TS configs. */
702
+ const ConfigSchema = v.strictObject({
703
+ $schema: v.optional(v.string()),
704
+ root: v.optional(v.string()),
705
+ name: v.optional(v.string()),
706
+ sourceLocale: v.optional(v.string()),
707
+ locales: v.optional(strings),
708
+ ignoreLocales: v.optional(strings),
709
+ presets: v.optional(v.union([v.literal("auto"), v.array(pluginSpecifier)])),
710
+ plugins: v.optional(v.array(pluginSpecifier)),
711
+ resources: v.optional(v.array(resourceSpec)),
712
+ formats: v.optional(v.record(v.string(), v.string())),
713
+ keyStyle: v.optional(v.picklist([
714
+ "auto",
715
+ "nested",
716
+ "flat"
717
+ ])),
718
+ namespace: v.optional(namespaceOptions),
719
+ usage: v.optional(v.strictObject({
720
+ include: v.optional(strings),
721
+ exclude: v.optional(strings),
722
+ calls: v.optional(v.array(callSpec)),
723
+ regex: v.optional(v.array(regexSpec)),
724
+ ignoreCallees: v.optional(strings),
725
+ keysInUse: v.optional(strings),
726
+ derivedKeys: v.optional(strings)
727
+ })),
728
+ write: v.optional(v.strictObject({
729
+ sort: v.optional(v.union([v.literal(false), v.picklist(["binary", "locale"])])),
730
+ indent: v.optional(v.union([v.number(), v.literal("tab")])),
731
+ keepFilled: v.optional(v.boolean())
732
+ })),
733
+ extract: v.optional(extractOptions),
734
+ checks: v.optional(v.record(v.string(), checkSetting)),
735
+ strict: v.optional(v.boolean()),
736
+ ignoreKeys: v.optional(strings),
737
+ glossary: v.optional(v.array(glossaryEntry)),
738
+ history: v.optional(v.union([v.boolean(), v.record(v.string(), v.union([v.literal(false), v.looseObject({})]))])),
739
+ projects: v.optional(strings)
740
+ });
741
+ const PluginFileSchema = v.strictObject({
742
+ $schema: v.optional(v.string()),
743
+ name: v.string(),
744
+ detect: v.optional(v.strictObject({
745
+ dependencies: v.optional(v.strictObject({
746
+ any: v.optional(strings),
747
+ all: v.optional(strings)
748
+ })),
749
+ files: v.optional(strings)
750
+ })),
751
+ exclusive: v.optional(v.boolean()),
752
+ config: v.optional(v.strictObject({
753
+ sourceLocale: v.optional(v.string()),
754
+ locales: v.optional(strings),
755
+ resources: v.optional(v.array(resourceSpec)),
756
+ namespace: v.optional(namespaceOptions),
757
+ keyStyle: v.optional(v.picklist([
758
+ "auto",
759
+ "nested",
760
+ "flat"
761
+ ]))
762
+ })),
763
+ scanners: v.optional(v.strictObject({
764
+ calls: v.optional(v.array(callSpec)),
765
+ regex: v.optional(v.array(regexSpec))
766
+ })),
767
+ scopes: v.optional(v.strictObject({ regex: v.string() })),
768
+ derivedKeys: v.optional(strings),
769
+ normalizeKey: v.optional(v.array(v.strictObject({ replace: v.strictObject({
770
+ pattern: v.string(),
771
+ with: v.string()
772
+ }) }))),
773
+ placeholders: v.optional(strings),
774
+ localeInfo: v.optional(v.record(v.string(), v.strictObject({
775
+ flag: v.optional(v.string()),
776
+ displayName: v.optional(v.string())
777
+ }))),
778
+ extract: v.optional(v.strictObject({ templates: v.optional(extractTemplates) }))
779
+ });
780
+ /** Validates data against a schema and returns readable issues instead of throwing. */
781
+ function validate(schema, data) {
782
+ const result = v.safeParse(schema, data);
783
+ if (result.success) return {
784
+ success: true,
785
+ output: result.output
786
+ };
787
+ return {
788
+ success: false,
789
+ issues: result.issues.map((issue) => ({
790
+ path: issue.path?.map((item) => String(item.key)).join(".") ?? "",
791
+ message: issue.message
792
+ }))
793
+ };
794
+ }
795
+ //#endregion
796
+ //#region src/project/batch.ts
797
+ function isLeaf(value) {
798
+ return value === null || typeof value !== "object";
799
+ }
800
+ /** Parses the batch shape `{ "<locale>": { "<key>": "<value>" } }` (as in `x18n apply`) into set ops. */
801
+ function parseMessageBatch(project, data) {
802
+ if (!isJsonObject(data)) throw new X18nError("X18N4008", "expected { \"<locale>\": { \"<key>\": \"<value>\" } }");
803
+ const ops = [];
804
+ for (const [locale, entries] of Object.entries(data)) {
805
+ if (!isJsonObject(entries)) throw new X18nError("X18N4008", `"${locale}" must map keys to values`);
806
+ for (const [raw, value] of Object.entries(entries)) {
807
+ if (!isLeaf(value)) throw new X18nError("X18N4008", `"${locale}.${raw}" must be a string, number, boolean or null`);
808
+ ops.push({
809
+ type: "set",
810
+ key: project.parseKey(raw),
811
+ locale,
812
+ value
813
+ });
814
+ }
815
+ }
816
+ return ops;
817
+ }
818
+ /**
819
+ * Compares the placeholders of a target value with its source. Key references (`@:key`) are not
820
+ * parameters, and implicit names (vue-i18n `{n}` in plural messages) may be left out. Build it once per run.
821
+ */
822
+ function createPlaceholderComparer(syntaxes, find = (value) => syntaxes.flatMap((s) => s.find(value))) {
823
+ const references = new Set(syntaxes.filter((s) => s.reference).map((s) => s.id));
824
+ const implicit = new Set(syntaxes.flatMap((s) => s.implicit ?? []));
825
+ const seen = /* @__PURE__ */ new Map();
826
+ const names = (text) => {
827
+ let found = seen.get(text);
828
+ if (!found) {
829
+ found = new Set(find(text).filter((p) => !references.has(p.syntax) && !implicit.has(p.name)).map((p) => p.name));
830
+ seen.set(text, found);
831
+ }
832
+ return found;
833
+ };
834
+ return (source, target) => {
835
+ const expected = names(source);
836
+ const actual = names(target);
837
+ return {
838
+ missing: [...expected].filter((name) => !actual.has(name)).sort(),
839
+ extra: [...actual].filter((name) => !expected.has(name)).sort()
840
+ };
841
+ };
842
+ }
843
+ function placeholderProblems(project, ops) {
844
+ const compare = createPlaceholderComparer(project.placeholderSyntaxes);
845
+ return ops.flatMap((op) => {
846
+ if (op.type !== "set" || op.locale === project.sourceLocale || typeof op.value !== "string") return [];
847
+ const source = project.message(op.key, project.sourceLocale)?.value;
848
+ if (typeof source !== "string") return [];
849
+ const { missing, extra } = compare(source, op.value);
850
+ return missing.length || extra.length ? [{
851
+ key: op.key,
852
+ locale: op.locale,
853
+ missing,
854
+ extra
855
+ }] : [];
856
+ });
857
+ }
858
+ //#endregion
859
+ //#region src/project/history.ts
860
+ const localChange = { updatedAt: Number.POSITIVE_INFINITY };
861
+ /** A target message is stale when it last changed before its source message. */
862
+ function isOutdated(source, target) {
863
+ return source !== void 0 && target !== void 0 && target.updatedAt < source.updatedAt;
864
+ }
865
+ function createHistory(input) {
866
+ const { providers } = input;
867
+ let histories = [];
868
+ let loading;
869
+ let watchers;
870
+ async function reload() {
871
+ const ctx = input.context();
872
+ histories = (await Promise.all(providers.map((provider) => provider.load(ctx).catch((error) => {
873
+ input.logger?.warn(`History provider "${provider.id}" failed: ${error instanceof Error ? error.message : String(error)}`);
874
+ })))).filter((history) => history !== void 0);
875
+ input.onLoad();
876
+ }
877
+ const owner = (key, locale) => {
878
+ for (const history of histories) {
879
+ const change = history.changes.get(key)?.get(locale);
880
+ if (change) return {
881
+ history,
882
+ change
883
+ };
884
+ }
885
+ };
886
+ return {
887
+ load(options) {
888
+ if (providers.length === 0) return Promise.resolve();
889
+ loading ??= reload();
890
+ if (options.watch && !watchers) {
891
+ const ctx = input.context();
892
+ watchers = providers.flatMap((provider) => provider.watch ? [provider.watch(ctx, () => {
893
+ loading = reload();
894
+ })] : []);
895
+ }
896
+ return loading;
897
+ },
898
+ get(key, locale) {
899
+ const change = owner(key, locale)?.change;
900
+ if (!change) return void 0;
901
+ const current = input.current(key, locale);
902
+ return change.value !== void 0 && current !== void 0 && current !== change.value ? localChange : change;
903
+ },
904
+ valueAt: (key, locale, at) => owner(key, locale)?.history.valueAt?.(key, locale, at),
905
+ dispose() {
906
+ watchers?.forEach((watcher) => watcher.dispose());
907
+ watchers = void 0;
908
+ }
909
+ };
910
+ }
911
+ //#endregion
912
+ //#region src/project/analysis.ts
913
+ function levelOf(setting) {
914
+ if (setting === void 0) return void 0;
915
+ if (typeof setting === "string") return {
916
+ level: setting,
917
+ options: {}
918
+ };
919
+ return {
920
+ level: setting[0],
921
+ options: setting[1]
922
+ };
923
+ }
924
+ function computeCoverage(state, reference) {
925
+ const { messages, sourceLocale } = state;
926
+ return state.locales.map((locale) => {
927
+ const missing = [];
928
+ const empty = [];
929
+ const outdated = [];
930
+ for (const key of reference) {
931
+ const value = messages.get(key)?.get(locale)?.[0]?.value;
932
+ if (value === void 0) missing.push(key);
933
+ else if (value === "") empty.push(key);
934
+ else if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale))) outdated.push(key);
935
+ }
936
+ return {
937
+ locale,
938
+ total: reference.length,
939
+ translated: reference.length - missing.length - empty.length,
940
+ missing,
941
+ empty,
942
+ outdated
943
+ };
944
+ });
945
+ }
946
+ function errorDiagnostics(state) {
947
+ return [
948
+ ...state.discoveryErrors,
949
+ ...state.resourceErrors,
950
+ ...state.scanErrors
951
+ ].map(({ error, path }) => ({
952
+ code: "invalid-resource",
953
+ severity: error.code.startsWith("X18N3") ? "warning" : "error",
954
+ message: `${error.message} (${error.code})`,
955
+ key: void 0,
956
+ locale: void 0,
957
+ location: {
958
+ path,
959
+ range: {
960
+ start: 0,
961
+ end: 0
962
+ }
963
+ },
964
+ fixes: []
965
+ }));
966
+ }
967
+ function* runCheck(check, level, inputs, input) {
968
+ const { state, config } = input;
969
+ const resourcePath = (id) => state.resources.find((s) => s.resource.id === id)?.resource.path ?? id;
970
+ for (const item of inputs) {
971
+ if (item.key !== void 0 && input.ignored(item.key)) continue;
972
+ const severity = config.strict && (item.severity ?? level) === "warning" ? "error" : item.severity ?? level;
973
+ let location = item.location;
974
+ if (!location && item.key !== void 0) {
975
+ const byLocale = state.messages.get(item.key);
976
+ const found = byLocale?.get(item.locale ?? state.sourceLocale)?.[0] ?? byLocale?.get(state.sourceLocale)?.[0];
977
+ if (found) location = {
978
+ path: resourcePath(found.resource),
979
+ range: found.range?.key ?? {
980
+ start: 0,
981
+ end: 0
982
+ }
983
+ };
984
+ }
985
+ yield {
986
+ code: check.id,
987
+ severity,
988
+ message: item.message,
989
+ key: item.key,
990
+ locale: item.locale,
991
+ location,
992
+ fixes: item.fixes ?? []
993
+ };
994
+ }
995
+ }
996
+ function runChecks(input) {
997
+ const out = [];
998
+ for (const check of input.registry.checks.values()) {
999
+ const setting = levelOf(input.config.checks[check.id]) ?? {
1000
+ level: check.defaultSeverity,
1001
+ options: {}
1002
+ };
1003
+ if (setting.level === "off") continue;
1004
+ try {
1005
+ out.push(...runCheck(check, setting.level, check.run({
1006
+ ...input.ctx,
1007
+ options: setting.options
1008
+ }), input));
1009
+ } catch (error) {
1010
+ out.push({
1011
+ code: check.id,
1012
+ severity: "error",
1013
+ message: `Check failed: ${error instanceof Error ? error.message : String(error)}`,
1014
+ key: void 0,
1015
+ locale: void 0,
1016
+ location: void 0,
1017
+ fixes: []
1018
+ });
1019
+ }
1020
+ }
1021
+ return out;
1022
+ }
1023
+ function createAnalyzer(options) {
1024
+ const { config, registry } = options;
1025
+ let memo;
1026
+ const ignored = keyMatcher(config.ignoreKeys);
1027
+ const inUse = keyMatcher(config.usage.keysInUse);
1028
+ const placeholderCache = /* @__PURE__ */ new Map();
1029
+ function analyze() {
1030
+ const state = options.state();
1031
+ if (memo && memo.revision === state.revision) return memo;
1032
+ const { sourceLocale, locales, messages } = state;
1033
+ const sourceKeys = [...messages.keys()].filter((key) => messages.get(key)?.has(sourceLocale)).sort();
1034
+ const allKeys = [...messages.keys()].sort();
1035
+ const coverage = computeCoverage(state, sourceKeys.length > 0 ? sourceKeys : allKeys);
1036
+ const isDefined = (key) => messages.get(key)?.has(sourceLocale) === true || state.parents.has(key) || (state.derived.get(key)?.length ?? 0) > 0 || sourceKeys.length === 0 && messages.has(key);
1037
+ const hasScanners = state.files.size > 0;
1038
+ const referenceSyntaxes = registry.placeholders.filter((syntax) => syntax.reference);
1039
+ const referenced = /* @__PURE__ */ new Set();
1040
+ if (referenceSyntaxes.length > 0) for (const byLocale of messages.values()) {
1041
+ const value = byLocale.get(sourceLocale)?.[0]?.value;
1042
+ if (typeof value === "string") referenceSyntaxes.forEach((syntax) => syntax.find(value).forEach((p) => referenced.add(p.name)));
1043
+ }
1044
+ const prefixes = [...new Set(state.prefixUsages.flatMap((usage) => usage.key === void 0 ? [] : [usage.key]))];
1045
+ const isUsed = (key) => {
1046
+ if (state.usagesByKey.has(key) || inUse(key) || referenced.has(key)) return true;
1047
+ const base = options.baseOf(key);
1048
+ if (base !== void 0 && (state.usagesByKey.has(base) || inUse(base))) return true;
1049
+ return prefixes.some((prefix) => key.startsWith(prefix));
1050
+ };
1051
+ const unused = hasScanners ? allKeys.filter((key) => !isUsed(key)) : [];
1052
+ const undefinedUsages = state.usages.filter((usage) => usage.dynamic === false && usage.key !== void 0 && !isDefined(usage.key));
1053
+ const placeholders = (value) => {
1054
+ let found = placeholderCache.get(value);
1055
+ if (!found) {
1056
+ found = registry.placeholders.flatMap((syntax) => syntax.find(value)).sort((a, b) => a.range.start - b.range.start);
1057
+ if (placeholderCache.size > 1e5) placeholderCache.clear();
1058
+ placeholderCache.set(value, found);
1059
+ }
1060
+ return found;
1061
+ };
1062
+ const baseCtx = {
1063
+ config,
1064
+ sourceLocale,
1065
+ locales,
1066
+ resources: state.resources.map((s) => s.resource),
1067
+ hasScanners,
1068
+ keys: () => allKeys,
1069
+ *messages(locale) {
1070
+ for (const byLocale of messages.values()) for (const [code, list] of byLocale) if (locale === void 0 || code === locale) yield list[0];
1071
+ },
1072
+ message: (key, locale) => messages.get(key)?.get(locale)?.[0],
1073
+ duplicates: (key, locale) => messages.get(key)?.get(locale) ?? [],
1074
+ usages: () => state.usages,
1075
+ coverage: () => coverage,
1076
+ unused: () => unused,
1077
+ undefinedUsages: () => undefinedUsages,
1078
+ placeholders,
1079
+ placeholderSyntaxes: registry.placeholders,
1080
+ syntaxErrors: (value) => registry.placeholders.flatMap((syntax) => syntax.validate?.(value) ?? []),
1081
+ localeInfo: options.localeInfo,
1082
+ text: options.text,
1083
+ history: state.history,
1084
+ *hardStrings() {
1085
+ for (const [path, file] of state.files) for (const hard of file.result.hardStrings ?? []) yield {
1086
+ ...hard,
1087
+ path
1088
+ };
1089
+ }
1090
+ };
1091
+ const diagnostics = [...errorDiagnostics(state), ...runChecks({
1092
+ config,
1093
+ registry,
1094
+ ctx: baseCtx,
1095
+ state,
1096
+ ignored
1097
+ })];
1098
+ diagnostics.sort((a, b) => (a.location?.path ?? "").localeCompare(b.location?.path ?? "") || (a.location?.range.start ?? 0) - (b.location?.range.start ?? 0) || a.code.localeCompare(b.code));
1099
+ memo = {
1100
+ revision: state.revision,
1101
+ diagnostics,
1102
+ coverage,
1103
+ unused,
1104
+ undefinedUsages
1105
+ };
1106
+ return memo;
1107
+ }
1108
+ return { analyze };
1109
+ }
1110
+ //#endregion
1111
+ //#region src/project/planner.ts
1112
+ const safeSegment = /^[\w-]+$/;
1113
+ const safeFlat = /^[\w.-]+$/;
1114
+ function commonPrefix(a, b) {
1115
+ let i = 0;
1116
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
1117
+ return i;
1118
+ }
1119
+ function nodeAt(tree, path) {
1120
+ let node = tree;
1121
+ for (const segment of path) {
1122
+ if (node === void 0 || isMessageLeaf(node)) return void 0;
1123
+ node = node[segment];
1124
+ }
1125
+ return node;
1126
+ }
1127
+ /** True for flat files (`{ "a.b": "x" }`), false for nested ones, undefined when it cannot tell. */
1128
+ function isFlatTree(tree) {
1129
+ const entries = Object.entries(tree);
1130
+ if (entries.length === 0) return void 0;
1131
+ if (entries.some(([, value]) => !isMessageLeaf(value))) return false;
1132
+ return entries.some(([key]) => key.includes(".")) ? true : void 0;
1133
+ }
1134
+ /** The longest key prefix a tree shares with `path`. */
1135
+ function longestPrefix(tree, prefix, path) {
1136
+ let best = 0;
1137
+ for (const [segment, child] of Object.entries(tree)) {
1138
+ const next = [...prefix, ...segment.split(".")];
1139
+ best = Math.max(best, commonPrefix(next, path));
1140
+ if (!isMessageLeaf(child)) best = Math.max(best, longestPrefix(child, next, path));
1141
+ }
1142
+ return best;
1143
+ }
1144
+ function mirrorPath(path, from, to) {
1145
+ const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1146
+ return path.replace(new RegExp(`(^|[/._-])${escaped}(?=$|[/._-])`, "g"), `$1${to}`);
1147
+ }
1148
+ function importEdit(text, lines) {
1149
+ const missing = lines.filter((line) => !text.includes(line.trim()));
1150
+ if (missing.length === 0) return void 0;
1151
+ const script = /<script\b[^>]*>\r?\n?/.exec(text);
1152
+ let at = 0;
1153
+ if (script) at = script.index + script[0].length;
1154
+ else {
1155
+ const last = [...text.matchAll(/^import\s.*$/gm)].at(-1);
1156
+ if (last) at = last.index + last[0].length + 1;
1157
+ }
1158
+ return {
1159
+ range: {
1160
+ start: at,
1161
+ end: at
1162
+ },
1163
+ text: `${missing.join("\n")}\n`
1164
+ };
1165
+ }
1166
+ var PlanBuilder = class {
1167
+ options;
1168
+ state;
1169
+ answers;
1170
+ work = /* @__PURE__ */ new Map();
1171
+ created = /* @__PURE__ */ new Map();
1172
+ sourceEdits = /* @__PURE__ */ new Map();
1173
+ sourceImports = /* @__PURE__ */ new Map();
1174
+ decisions = [];
1175
+ warnings = [];
1176
+ touched = /* @__PURE__ */ new Set();
1177
+ stateById;
1178
+ config;
1179
+ constructor(options, state, answers) {
1180
+ this.options = options;
1181
+ this.state = state;
1182
+ this.answers = answers;
1183
+ this.config = options.config;
1184
+ this.stateById = new Map(state.resources.map((s) => [s.resource.id, s]));
1185
+ }
1186
+ /** ---- Targets ---- */
1187
+ targetOf(s) {
1188
+ const format = this.options.registry.formatById(s.resource.format);
1189
+ if (!format) throw new X18nError("X18N2001", s.resource.path);
1190
+ return {
1191
+ id: s.resource.id,
1192
+ path: s.resource.path,
1193
+ locale: s.resource.locale,
1194
+ namespace: s.resource.namespace,
1195
+ format,
1196
+ state: s
1197
+ };
1198
+ }
1199
+ workFor(target) {
1200
+ let entry = this.work.get(target.id);
1201
+ if (!entry) {
1202
+ entry = {
1203
+ target,
1204
+ tree: target.state?.rawTree ?? {},
1205
+ ops: [],
1206
+ keyRenames: [],
1207
+ formatOnly: false
1208
+ };
1209
+ this.work.set(target.id, entry);
1210
+ }
1211
+ return entry;
1212
+ }
1213
+ writable(id) {
1214
+ const s = this.stateById.get(id);
1215
+ if (s.resource.readonly) throw new X18nError("X18N4001", s.resource.path);
1216
+ return s;
1217
+ }
1218
+ keyId(key) {
1219
+ const id = splitKey(key, this.state.namespaced);
1220
+ return {
1221
+ namespace: id.namespace,
1222
+ path: [...id.path]
1223
+ };
1224
+ }
1225
+ /** Key path relative to a resource, or undefined when the key does not belong there. */
1226
+ relativePath(key, target) {
1227
+ const { namespace, path } = this.keyId(key);
1228
+ const fallback = this.config.namespace.default;
1229
+ if (this.state.namespaced) return (namespace ?? fallback) === (target.namespace ?? fallback) ? path : void 0;
1230
+ const prefix = target.namespace ? target.namespace.split(".") : [];
1231
+ if (path.length <= prefix.length || prefix.some((segment, i) => path[i] !== segment)) return void 0;
1232
+ return path.slice(prefix.length);
1233
+ }
1234
+ storedPathFor(entry, relative) {
1235
+ const stored = this.options.toStored(entry.target, relative);
1236
+ return stored.length === relative.length && (isFlatTree(entry.tree) ?? this.config.keyStyle === "flat") ? [stored.join(".")] : stored;
1237
+ }
1238
+ createTarget(key, locale) {
1239
+ const { resources, sourceLocale, namespaced } = this.state;
1240
+ const fits = (s) => this.relativePath(key, s.resource) !== void 0;
1241
+ const source = this.options.message(key, sourceLocale);
1242
+ const sourceState = source && this.stateById.get(source.resource);
1243
+ const namespace = namespaced ? this.keyId(key).namespace ?? this.config.namespace.default : sourceState?.resource.namespace;
1244
+ const exists = (path) => resources.some((r) => r.resource.path === path);
1245
+ const resolved = this.options.resolveNew(locale, namespace);
1246
+ if (resolved && !exists(resolved)) {
1247
+ const format = this.options.registry.formatFor(resolved);
1248
+ if (format?.serialize) {
1249
+ const target = {
1250
+ id: resolved,
1251
+ path: resolved,
1252
+ locale,
1253
+ namespace,
1254
+ format,
1255
+ state: void 0
1256
+ };
1257
+ this.created.set(resolved, target);
1258
+ return target;
1259
+ }
1260
+ }
1261
+ const sibling = (r) => r.resource.locale === locale && r.resource.namespace !== void 0 && namespace !== void 0;
1262
+ const model = (sourceState && fits(sourceState) ? sourceState : void 0) ?? resources.find((s) => s.resource.locale === sourceLocale && fits(s)) ?? resources.find(fits) ?? resources.find(sibling);
1263
+ const format = model && this.options.registry.formatById(model.resource.format);
1264
+ let path = model && mirrorPath(model.resource.path, model.resource.locale, locale);
1265
+ if (model && path && namespace !== void 0 && model.resource.namespace !== void 0 && model.resource.namespace !== namespace) path = mirrorPath(path, model.resource.namespace, namespace);
1266
+ if (!model || !format?.serialize || model.resource.block || !path || exists(path)) throw new X18nError("X18N4005", `${key} in ${locale}`);
1267
+ const target = {
1268
+ id: path,
1269
+ path,
1270
+ locale,
1271
+ namespace: namespace ?? model.resource.namespace,
1272
+ format,
1273
+ state: void 0
1274
+ };
1275
+ this.created.set(path, target);
1276
+ return target;
1277
+ }
1278
+ /** Placement: the namespace's resource, then the longest shared prefix, then the first; a tie is a decision. */
1279
+ pickTarget(key, locale) {
1280
+ let inLocale = this.state.resources.filter((s) => s.resource.locale === locale && this.relativePath(key, s.resource) !== void 0);
1281
+ const source = this.options.message(key, this.state.sourceLocale);
1282
+ const sourceNamespace = source && this.stateById.get(source.resource)?.resource.namespace;
1283
+ if (sourceNamespace !== void 0 && !this.state.namespaced) inLocale = inLocale.filter((s) => s.resource.namespace === sourceNamespace);
1284
+ if (inLocale.length === 0) return [...this.created.values()].find((t) => t.locale === locale && this.relativePath(key, t) !== void 0) ?? this.createTarget(key, locale);
1285
+ if (inLocale.length === 1) return this.targetOf(inLocale[0]);
1286
+ const path = this.keyId(key).path;
1287
+ const scores = inLocale.map((s) => {
1288
+ const prefix = s.resource.namespace && !this.state.namespaced ? path.slice(0, s.resource.namespace.split(".").length) : [];
1289
+ return {
1290
+ s,
1291
+ best: longestPrefix(s.tree, prefix, path)
1292
+ };
1293
+ });
1294
+ const top = Math.max(...scores.map((x) => x.best));
1295
+ const winners = scores.filter((x) => x.best === top);
1296
+ if (winners.length === 1 || top === 0) return this.targetOf(winners[0].s);
1297
+ const id = `target:${key}:${locale}`;
1298
+ this.decisions.push({
1299
+ id,
1300
+ message: `Which file gets "${key}" in ${locale}?`,
1301
+ options: winners.map((w) => ({
1302
+ id: w.s.resource.id,
1303
+ label: w.s.resource.id
1304
+ })),
1305
+ default: winners[0].s.resource.id
1306
+ });
1307
+ return this.targetOf((winners.find((w) => w.s.resource.id === this.answers[id]) ?? winners[0]).s);
1308
+ }
1309
+ /** ---- Message ops ---- */
1310
+ pushOp(entry, op) {
1311
+ entry.ops.push(op);
1312
+ entry.tree = applyMessageOps(entry.tree, [op]);
1313
+ }
1314
+ setValue(key, locale, value) {
1315
+ this.touched.add(key);
1316
+ const existing = this.options.message(key, locale);
1317
+ if (existing) {
1318
+ this.pushOp(this.workFor(this.targetOf(this.writable(existing.resource))), {
1319
+ type: "set",
1320
+ path: existing.storedPath,
1321
+ value
1322
+ });
1323
+ return;
1324
+ }
1325
+ const target = this.pickTarget(key, locale);
1326
+ if (target.state) this.writable(target.state.resource.id);
1327
+ const entry = this.workFor(target);
1328
+ const stored = this.storedPathFor(entry, this.relativePath(key, target));
1329
+ const occupied = nodeAt(entry.tree, stored);
1330
+ if (occupied !== void 0 && !isMessageLeaf(occupied)) this.warnings.push(`"${key}" is a parent of other keys in ${target.path}; they are replaced.`);
1331
+ this.pushOp(entry, {
1332
+ type: "set",
1333
+ path: stored,
1334
+ value
1335
+ });
1336
+ }
1337
+ deleteMessage(m) {
1338
+ const entry = this.workFor(this.targetOf(this.writable(m.resource)));
1339
+ let path = [...m.storedPath];
1340
+ while (path.length > 1) {
1341
+ const parent = nodeAt(entry.tree, path.slice(0, -1));
1342
+ if (parent === void 0 || isMessageLeaf(parent) || Object.keys(parent).length > 1) break;
1343
+ path = path.slice(0, -1);
1344
+ }
1345
+ this.pushOp(entry, {
1346
+ type: "delete",
1347
+ path
1348
+ });
1349
+ }
1350
+ /** Renames a key in every locale, in place when only the last segment changes. */
1351
+ /**
1352
+ * Renames a parent object in place where only its last segment changes, so the subtree keeps its
1353
+ * position and layout. Returns the resources it handled.
1354
+ */
1355
+ renameNode(from, to) {
1356
+ const handled = /* @__PURE__ */ new Set();
1357
+ for (const s of this.state.resources) {
1358
+ const relativeFrom = this.relativePath(from, s.resource);
1359
+ const relativeTo = this.relativePath(to, s.resource);
1360
+ if (!relativeFrom || !relativeTo || s.resource.readonly || !this.options.registry.formatById(s.resource.format)?.edit) continue;
1361
+ const stored = s.toStored(relativeFrom);
1362
+ const next = s.toStored(relativeTo);
1363
+ const node = nodeAt(s.rawTree, stored);
1364
+ const range = s.ranges.get(pathId(stored));
1365
+ const last = next.at(-1);
1366
+ const sameParent = next.length === stored.length && next.slice(0, -1).every((segment, i) => segment === stored[i]);
1367
+ if (node === void 0 || isMessageLeaf(node) || !range || !sameParent || !safeSegment.test(last)) continue;
1368
+ const entry = this.workFor(this.targetOf(s));
1369
+ entry.keyRenames.push({
1370
+ range: {
1371
+ start: range.key.start + s.offset,
1372
+ end: range.key.end + s.offset
1373
+ },
1374
+ text: last
1375
+ });
1376
+ const leaves = [...walkLeaves(node, stored)];
1377
+ entry.tree = applyMessageOps(entry.tree, [...leaves.map((leaf) => ({
1378
+ type: "delete",
1379
+ path: leaf.path
1380
+ })), ...leaves.map((leaf) => ({
1381
+ type: "set",
1382
+ path: [...next, ...leaf.path.slice(stored.length)],
1383
+ value: leaf.value
1384
+ }))]);
1385
+ handled.add(s.resource.id);
1386
+ }
1387
+ return handled;
1388
+ }
1389
+ moveKey(from, to, renamed = /* @__PURE__ */ new Set()) {
1390
+ this.touched.add(from);
1391
+ this.touched.add(to);
1392
+ for (const [locale, list] of this.options.messages(from) ?? []) {
1393
+ const m = list[0];
1394
+ if (renamed.has(m.resource)) continue;
1395
+ const s = this.writable(m.resource);
1396
+ const relative = this.relativePath(to, s.resource);
1397
+ if (!relative) {
1398
+ this.deleteMessage(m);
1399
+ this.setValue(to, locale, m.value);
1400
+ continue;
1401
+ }
1402
+ const entry = this.workFor(this.targetOf(s));
1403
+ const flat = m.storedPath.length === 1 && m.storedPath[0].includes(".");
1404
+ const next = flat ? [relative.join(".")] : this.options.toStored(s.resource, relative);
1405
+ const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
1406
+ const last = next.at(-1);
1407
+ if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
1408
+ entry.keyRenames.push({
1409
+ range: m.range.key,
1410
+ text: last
1411
+ });
1412
+ entry.tree = applyMessageOps(entry.tree, [{
1413
+ type: "delete",
1414
+ path: m.storedPath
1415
+ }, {
1416
+ type: "set",
1417
+ path: next,
1418
+ value: m.value
1419
+ }]);
1420
+ continue;
1421
+ }
1422
+ this.deleteMessage(m);
1423
+ this.pushOp(entry, {
1424
+ type: "set",
1425
+ path: this.storedPathFor(entry, relative),
1426
+ value: m.value
1427
+ });
1428
+ }
1429
+ }
1430
+ /** The raw text a usage needs to refer to `next`, given its scope. */
1431
+ rawFor(usage, next) {
1432
+ if (usage.raw.startsWith(".") && usage.key?.endsWith(usage.raw)) {
1433
+ const scope = usage.key.slice(0, usage.key.length - usage.raw.length);
1434
+ if (next.startsWith(`${scope}.`)) return next.slice(scope.length);
1435
+ }
1436
+ const { namespace, path } = this.keyId(next);
1437
+ let rest = path.join(".");
1438
+ const separator = this.config.namespace.separator;
1439
+ const explicit = this.state.namespaced && usage.raw.indexOf(separator) > 0;
1440
+ if (usage.scopePrefix && !explicit) {
1441
+ if (!rest.startsWith(`${usage.scopePrefix}.`)) return void 0;
1442
+ rest = rest.slice(usage.scopePrefix.length + 1);
1443
+ }
1444
+ if (!this.state.namespaced) return rest;
1445
+ if (explicit || namespace !== (usage.scopeNamespace ?? this.config.namespace.default)) return `${namespace}${separator}${rest}`;
1446
+ return rest;
1447
+ }
1448
+ editSource(path, edit) {
1449
+ const list = this.sourceEdits.get(path) ?? [];
1450
+ this.sourceEdits.set(path, list);
1451
+ if (!list.some((e) => e.range.start === edit.range.start && e.range.end === edit.range.end)) list.push(edit);
1452
+ }
1453
+ rewriteUsages(from, to) {
1454
+ for (const { usage, possible } of this.options.usageMatches({ key: from })) {
1455
+ if (possible || usage.key !== from) continue;
1456
+ const raw = this.rawFor(usage, to);
1457
+ if (raw === void 0) this.warnings.push(`${usage.path}: usage of "${from}" is under the key prefix "${usage.scopePrefix}" and was not updated.`);
1458
+ else this.editSource(usage.path, {
1459
+ range: usage.range,
1460
+ text: raw
1461
+ });
1462
+ }
1463
+ }
1464
+ /** Rewrites the static part of prefix usages under `from`, and warns about the ones it cannot follow. */
1465
+ rewritePrefixUsages(from, to) {
1466
+ for (const { usage, possible } of this.options.usageMatches({ prefix: from })) {
1467
+ if (!possible || usage.key === void 0) continue;
1468
+ const raw = usage.key.startsWith(from) ? this.rawFor(usage, to + usage.key.slice(from.length)) : void 0;
1469
+ if (raw === void 0) this.warnings.push(`${usage.path}: dynamic usage "${usage.raw}" may refer to renamed keys and was not updated.`);
1470
+ else this.editSource(usage.path, {
1471
+ range: usage.range,
1472
+ text: raw
1473
+ });
1474
+ }
1475
+ }
1476
+ /** ---- Ops ---- */
1477
+ conflictCheck(to) {
1478
+ const existing = this.options.messages(to);
1479
+ if (!existing && !this.options.parents().has(to)) return;
1480
+ const files = [...new Set([...existing?.values() ?? []].flatMap((list) => list.map((m) => this.stateById.get(m.resource)?.resource.path ?? m.resource)))];
1481
+ throw new X18nError("X18N4002", `"${to}"${files.length ? ` in ${files.join(", ")}` : ""}`, files);
1482
+ }
1483
+ rename(op) {
1484
+ if (op.from === op.to) return;
1485
+ const all = this.options.allKeys();
1486
+ const isLeaf = this.options.messages(op.from) !== void 0;
1487
+ const under = all.filter((key) => isKeyUnder(key, op.from));
1488
+ const derived = all.filter((key) => this.options.baseOf(key) === op.from);
1489
+ let keys;
1490
+ if (op.type === "rename-prefix") keys = under;
1491
+ else if (isLeaf) keys = [op.from, ...derived];
1492
+ else keys = under.length > 0 ? under : derived;
1493
+ if (keys.length === 0) throw new X18nError("X18N4006", op.from);
1494
+ const pairs = keys.map((key) => [key, replaceKeyPrefix(key, op.from, op.to)]);
1495
+ for (const [, to] of pairs) this.conflictCheck(to);
1496
+ const renamed = keys.includes(op.from) ? /* @__PURE__ */ new Set() : this.renameNode(op.from, op.to);
1497
+ for (const [from, to] of pairs) {
1498
+ this.moveKey(from, to, renamed);
1499
+ if (op.usages !== false) this.rewriteUsages(from, to);
1500
+ }
1501
+ if (op.usages === false) return;
1502
+ if (!keys.includes(op.from)) this.rewriteUsages(op.from, op.to);
1503
+ this.rewritePrefixUsages(op.from, op.to);
1504
+ }
1505
+ delete(op) {
1506
+ const keys = this.options.messages(op.key) ? [op.key] : this.options.allKeys().filter((key) => isKeyUnder(key, op.key));
1507
+ if (keys.length === 0) throw new X18nError("X18N4006", op.key);
1508
+ for (const key of keys) {
1509
+ this.touched.add(key);
1510
+ for (const [locale, list] of this.options.messages(key) ?? []) if (op.locales === void 0 || op.locales === "all" || op.locales.includes(locale)) list.forEach((m) => this.deleteMessage(m));
1511
+ const used = this.options.usageMatches({ key }).filter((match) => !match.possible).length;
1512
+ if (used > 0) this.warnings.push(`"${key}" still has ${used} usage${used === 1 ? "" : "s"}.`);
1513
+ }
1514
+ }
1515
+ extract(items) {
1516
+ for (const item of items) {
1517
+ if (!this.options.message(item.key, this.state.sourceLocale)) this.setValue(item.key, this.state.sourceLocale, item.value);
1518
+ this.editSource(item.path, {
1519
+ range: item.range,
1520
+ text: item.replacement
1521
+ });
1522
+ if (item.imports?.length) {
1523
+ const set = this.sourceImports.get(item.path) ?? /* @__PURE__ */ new Set();
1524
+ this.sourceImports.set(item.path, set);
1525
+ item.imports.forEach((line) => set.add(line));
1526
+ }
1527
+ }
1528
+ }
1529
+ run(ops) {
1530
+ for (const op of ops) switch (op.type) {
1531
+ case "set":
1532
+ this.setValue(op.key, op.locale, op.value);
1533
+ if (this.config.write.keepFilled) {
1534
+ for (const locale of this.state.locales) if (locale !== op.locale && !this.options.message(op.key, locale)) this.setValue(op.key, locale, "");
1535
+ }
1536
+ break;
1537
+ case "delete":
1538
+ this.delete(op);
1539
+ break;
1540
+ case "rename":
1541
+ case "rename-prefix":
1542
+ this.rename(op);
1543
+ break;
1544
+ case "extract":
1545
+ this.extract(op.items);
1546
+ break;
1547
+ case "format": for (const s of this.state.resources) if ((!op.paths || op.paths.includes(s.resource.path)) && !s.resource.readonly && !s.error) this.workFor(this.targetOf(s)).formatOnly = true;
1548
+ }
1549
+ }
1550
+ /** ---- Changes ---- */
1551
+ compare() {
1552
+ return this.config.write.sort === "locale" ? (a, b) => a.localeCompare(b) : (a, b) => a < b ? -1 : a > b ? 1 : 0;
1553
+ }
1554
+ /** The new content of one existing resource. */
1555
+ nextContent(entry, s) {
1556
+ const { target } = entry;
1557
+ const ctx = {
1558
+ path: target.path,
1559
+ locale: target.locale,
1560
+ namespace: target.namespace,
1561
+ indent: this.config.write.indent
1562
+ };
1563
+ const sort = this.config.write.sort !== false;
1564
+ const edit = target.format.edit;
1565
+ if (entry.formatOnly || sort || !edit) {
1566
+ if (!target.format.serialize) throw new X18nError("X18N4001", target.path);
1567
+ if (!entry.formatOnly && !edit) this.warnings.push(`${target.path} was re-written in full (the ${target.format.id} format cannot edit in place); formatting was reset.`);
1568
+ return target.format.serialize(sort || entry.formatOnly ? sortTree(entry.tree, sort ? this.compare() : () => 0) : entry.tree, ctx);
1569
+ }
1570
+ const local = (e) => ({
1571
+ range: {
1572
+ start: e.range.start - s.offset,
1573
+ end: e.range.end - s.offset
1574
+ },
1575
+ text: e.text
1576
+ });
1577
+ if (entry.ops.length === 0) return applyTextEdits(s.content, entry.keyRenames.map(local));
1578
+ const renameOps = [];
1579
+ for (const rename of entry.keyRenames) {
1580
+ const node = [...s.ranges].find(([, range]) => range.key.start + s.offset === rename.range.start);
1581
+ if (!node) continue;
1582
+ const oldPath = node[0].split("\0");
1583
+ const newPath = [...oldPath.slice(0, -1), rename.text];
1584
+ const value = nodeAt(entry.tree, newPath);
1585
+ renameOps.push({
1586
+ type: "delete",
1587
+ path: oldPath
1588
+ });
1589
+ if (value !== void 0 && isMessageLeaf(value)) renameOps.push({
1590
+ type: "set",
1591
+ path: newPath,
1592
+ value
1593
+ });
1594
+ else if (value !== void 0) renameOps.push(...[...walkLeaves(value, newPath)].map((leaf) => ({
1595
+ type: "set",
1596
+ path: leaf.path,
1597
+ value: leaf.value
1598
+ })));
1599
+ }
1600
+ return applyTextEdits(s.content, edit(s.content, [...renameOps, ...entry.ops], ctx));
1601
+ }
1602
+ changes() {
1603
+ const changes = [];
1604
+ const texts = /* @__PURE__ */ new Map();
1605
+ const resourceEdits = /* @__PURE__ */ new Map();
1606
+ for (const entry of this.work.values()) {
1607
+ const { target } = entry;
1608
+ const s = target.state;
1609
+ if (!s) {
1610
+ const ctx = {
1611
+ path: target.path,
1612
+ locale: target.locale,
1613
+ namespace: target.namespace,
1614
+ indent: this.config.write.indent
1615
+ };
1616
+ const tree = this.config.write.sort === false ? entry.tree : sortTree(entry.tree, this.compare());
1617
+ changes.push({
1618
+ type: "create",
1619
+ path: target.path,
1620
+ content: target.format.serialize(tree, ctx)
1621
+ });
1622
+ texts.set(target.path, void 0);
1623
+ continue;
1624
+ }
1625
+ const edits = diffText(s.content, this.nextContent(entry, s)).map((e) => ({
1626
+ range: {
1627
+ start: e.range.start + s.offset,
1628
+ end: e.range.end + s.offset
1629
+ },
1630
+ text: e.text
1631
+ }));
1632
+ if (edits.length === 0) continue;
1633
+ const list = resourceEdits.get(target.path) ?? [];
1634
+ resourceEdits.set(target.path, list);
1635
+ list.push(...edits);
1636
+ }
1637
+ for (const [path, edits] of resourceEdits) {
1638
+ changes.push({
1639
+ type: "edit",
1640
+ path,
1641
+ edits
1642
+ });
1643
+ texts.set(path, this.state.fileTexts.get(path));
1644
+ }
1645
+ for (const [path, edits] of this.sourceEdits) {
1646
+ const text = this.options.text(path);
1647
+ const imports = this.sourceImports.get(path);
1648
+ const extra = imports && text !== void 0 ? importEdit(text, [...imports]) : void 0;
1649
+ changes.push({
1650
+ type: "edit",
1651
+ path,
1652
+ edits: [...edits, ...extra ? [extra] : []].sort((a, b) => a.range.start - b.range.start)
1653
+ });
1654
+ texts.set(path, text);
1655
+ }
1656
+ return {
1657
+ changes,
1658
+ texts
1659
+ };
1660
+ }
1661
+ };
1662
+ function createPlanner(options) {
1663
+ const texts = /* @__PURE__ */ new Map();
1664
+ function planWith(ops, answers) {
1665
+ const state = options.state();
1666
+ const builder = new PlanBuilder(options, state, answers);
1667
+ builder.run(ops);
1668
+ const result = builder.changes();
1669
+ return {
1670
+ plan: {
1671
+ id: hashText(JSON.stringify({
1672
+ revision: state.revision,
1673
+ ops,
1674
+ answers
1675
+ })),
1676
+ revision: state.revision,
1677
+ ops,
1678
+ changes: result.changes,
1679
+ decisions: builder.decisions,
1680
+ warnings: builder.warnings,
1681
+ keys: [...builder.touched].sort()
1682
+ },
1683
+ texts: result.texts
1684
+ };
1685
+ }
1686
+ async function write(change) {
1687
+ if (change.type === "edit") {
1688
+ const current = await options.host.readFile(change.path);
1689
+ await options.host.writeFile(change.path, applyTextEdits(current ?? "", change.edits));
1690
+ return {
1691
+ type: "change",
1692
+ path: change.path
1693
+ };
1694
+ }
1695
+ if (change.type === "create") {
1696
+ await options.host.writeFile(change.path, change.content);
1697
+ return {
1698
+ type: "create",
1699
+ path: change.path
1700
+ };
1701
+ }
1702
+ await options.host.deleteFile(change.path);
1703
+ return {
1704
+ type: "delete",
1705
+ path: change.path
1706
+ };
1707
+ }
1708
+ return {
1709
+ plan(ops, answers = {}) {
1710
+ const result = planWith(ops, answers);
1711
+ texts.set(result.plan.id, result.texts);
1712
+ return result.plan;
1713
+ },
1714
+ async apply(plan, answers) {
1715
+ const unanswered = plan.decisions.filter((d) => answers[d.id] === void 0);
1716
+ if (unanswered.length > 0) throw new X18nError("X18N4003", unanswered.map((d) => d.message).join(" "), unanswered.map((d) => d.id));
1717
+ if (plan.revision !== options.state().revision) throw new X18nError("X18N4004");
1718
+ const fresh = plan.decisions.length > 0 ? planWith(plan.ops, answers) : {
1719
+ plan,
1720
+ texts: texts.get(plan.id) ?? /* @__PURE__ */ new Map()
1721
+ };
1722
+ for (const change of fresh.plan.changes) {
1723
+ const current = await options.host.readFile(change.path);
1724
+ const expected = fresh.texts.get(change.path);
1725
+ if (change.type === "edit" && expected !== void 0 && current !== expected || change.type === "create" && current !== void 0) throw new X18nError("X18N4004", change.path);
1726
+ }
1727
+ const events = [];
1728
+ for (const change of fresh.plan.changes) events.push(await write(change));
1729
+ texts.delete(plan.id);
1730
+ return {
1731
+ revision: (await options.update(events))?.revision ?? options.state().revision,
1732
+ changes: fresh.plan.changes
1733
+ };
1734
+ }
1735
+ };
1736
+ }
1737
+ //#endregion
1738
+ //#region src/project/prompt.ts
1739
+ function text(value) {
1740
+ return value === void 0 || value === null ? void 0 : String(value);
1741
+ }
1742
+ function contextOf(project, key) {
1743
+ return project.usages({ key }).slice(0, 2).flatMap(({ usage }) => {
1744
+ const source = project.text(usage.path);
1745
+ if (source === void 0) return [];
1746
+ const index = createLineIndex(source);
1747
+ const line = index.position(usage.range.start).line;
1748
+ const lines = [
1749
+ line - 1,
1750
+ line,
1751
+ line + 1
1752
+ ].filter((n) => n >= 0 && n < index.lineCount).map((n) => index.line(n));
1753
+ return [{
1754
+ path: usage.path,
1755
+ line: line + 1,
1756
+ snippet: lines.join("\n")
1757
+ }];
1758
+ });
1759
+ }
1760
+ /** Builds the parts of a translate or review prompt from project state. */
1761
+ function buildPrompt(project, kind, to, options, extra) {
1762
+ const from = project.sourceLocale;
1763
+ const match = typeof options.keys === "string" ? keyMatcher([options.keys]) : void 0;
1764
+ const list = Array.isArray(options.keys) ? new Set(options.keys) : void 0;
1765
+ const stale = (key) => isOutdated(project.history(key, from), project.history(key, to));
1766
+ const candidates = project.keys({ namespace: options.namespace }).filter((key) => {
1767
+ if (match && !match(key)) return false;
1768
+ if (list && !list.has(key)) return false;
1769
+ const source = text(project.message(key, from)?.value);
1770
+ if (source === void 0 || source === "") return false;
1771
+ const target = text(project.message(key, to)?.value);
1772
+ if (kind === "review") return target !== void 0 && target !== "";
1773
+ return list !== void 0 || target === void 0 || target === "" || options.stale === true && stale(key);
1774
+ });
1775
+ const start = Number(options.cursor ?? 0) || 0;
1776
+ const limit = options.limit ?? 50;
1777
+ const page = candidates.slice(start, start + limit);
1778
+ const end = start + page.length;
1779
+ const syntaxes = project.placeholderSyntaxes;
1780
+ const info = project.localeInfo(to);
1781
+ const draft = {
1782
+ rules: [],
1783
+ notes: []
1784
+ };
1785
+ if (syntaxes.length > 0) draft.rules.push(`Keep every placeholder exactly as written (${syntaxes.map((s) => `\`${s.example ?? s.id}\``).join(", ")}); do not translate their names.`);
1786
+ draft.rules.push("Keep markup tags and their attributes as they are.", "Match the tone and wording of the existing messages in the target locale.", "Do not translate glossary terms marked \"do not translate\".");
1787
+ if (info.pluralCategories.length > 1) draft.notes.push(`${to} uses the plural categories ${info.pluralCategories.join(", ")}.`);
1788
+ for (const hook of extra.hooks) hook(draft, {
1789
+ sourceLocale: from,
1790
+ targetLocale: to,
1791
+ localeInfo: info
1792
+ });
1793
+ const examples = kind === "translate" ? project.keys().flatMap((key) => {
1794
+ const source = text(project.message(key, from)?.value);
1795
+ const target = text(project.message(key, to)?.value);
1796
+ return source && target && !candidates.includes(key) ? [{
1797
+ key,
1798
+ source,
1799
+ target
1800
+ }] : [];
1801
+ }).slice(0, 5) : [];
1802
+ const items = page.map((key) => {
1803
+ const source = text(project.message(key, from)?.value) ?? "";
1804
+ const translated = project.history(key, to);
1805
+ const previous = translated && stale(key) ? text(project.valueAt(key, from, translated.updatedAt)) : void 0;
1806
+ return {
1807
+ key,
1808
+ source,
1809
+ placeholders: [...new Set(syntaxes.flatMap((s) => s.find(source)).map((p) => p.text))],
1810
+ existing: text(project.message(key, to)?.value) || void 0,
1811
+ previousSource: previous === source ? void 0 : previous,
1812
+ context: options.context === false ? [] : contextOf(project, key)
1813
+ };
1814
+ });
1815
+ const nextCursor = end < candidates.length ? String(end) : void 0;
1816
+ const instructions = kind === "translate" ? [
1817
+ `Write a JSON file \`{ "${to}": { "<key>": "<translation>" } }\` and run \`x18n apply <file> --json\`, or run \`x18n set <key> --locale ${to} --value <text>\` per key.`,
1818
+ `Then run \`x18n check --only placeholder-mismatch,invalid-syntax --locale ${to} --json\` and fix the reported keys.`,
1819
+ nextCursor ? `${candidates.length - end} more keys remain: \`x18n prompt translate --to ${to} --cursor ${nextCursor}\`.` : `To find more work: \`x18n keys --missing ${to} --json --limit 50\`.`
1820
+ ] : [`Fix wrong translations with \`x18n set <key> --locale ${to} --value <text>\`, then list what you changed and why.`, `Then run \`x18n check --only placeholder-mismatch,invalid-syntax --locale ${to} --json\`.`];
1821
+ return {
1822
+ kind,
1823
+ project: project.name,
1824
+ from,
1825
+ to,
1826
+ targetName: info.displayName,
1827
+ total: candidates.length,
1828
+ remaining: candidates.length - end,
1829
+ nextCursor,
1830
+ rules: draft.rules,
1831
+ notes: draft.notes,
1832
+ glossary: extra.config.glossary.map((entry) => ({
1833
+ term: entry.term,
1834
+ translation: entry.translations?.[to],
1835
+ note: entry.note,
1836
+ doNotTranslate: entry.doNotTranslate === true
1837
+ })),
1838
+ examples,
1839
+ items,
1840
+ instructions
1841
+ };
1842
+ }
1843
+ function renderItem(item, to) {
1844
+ const out = [
1845
+ `### \`${item.key}\``,
1846
+ "",
1847
+ `- Source: ${JSON.stringify(item.source)}`
1848
+ ];
1849
+ if (item.placeholders.length) out.push(`- Placeholders: ${item.placeholders.map((p) => `\`${p}\``).join(", ")}`);
1850
+ if (item.previousSource !== void 0) out.push(`- Source when ${to} last changed: ${JSON.stringify(item.previousSource)}`);
1851
+ if (item.existing !== void 0) out.push(`- Current ${to}: ${JSON.stringify(item.existing)}`);
1852
+ for (const context of item.context) out.push(`- Used in \`${context.path}:${context.line}\`:`, "", " ```", ...context.snippet.split("\n").map((line) => ` ${line}`), " ```");
1853
+ out.push("");
1854
+ return out;
1855
+ }
1856
+ /** Plain markdown, with no provider-specific syntax. */
1857
+ function renderPrompt(data) {
1858
+ const target = data.targetName ? `${data.to} (${data.targetName})` : data.to;
1859
+ const out = [];
1860
+ if (data.kind === "translate") out.push(`# Translate ${data.items.length} messages from ${data.from} to ${target}`, "");
1861
+ else out.push(`# Review ${data.items.length} ${target} translations against ${data.from}`, "", "Check meaning, placeholders, glossary terms, tone and length. Report and fix wrong translations.", "");
1862
+ if (data.items.length === 0) return `${out.join("\n")}Nothing to do: every key is ${data.kind === "translate" ? "translated" : "reviewed"}.\n`;
1863
+ out.push("## Rules", "", ...data.rules.map((rule) => `- ${rule}`), ...data.notes.map((note) => `- ${note}`), "");
1864
+ if (data.glossary.length > 0) {
1865
+ out.push("## Glossary", "");
1866
+ for (const entry of data.glossary) out.push(`- ${entry.term}${entry.doNotTranslate ? ": do not translate" : entry.translation ? ` → ${entry.translation}` : ""}${entry.note ? ` (${entry.note})` : ""}`);
1867
+ out.push("");
1868
+ }
1869
+ if (data.examples.length > 0) {
1870
+ out.push(`## Existing ${data.to} messages (style reference)`, "");
1871
+ for (const example of data.examples) out.push(`- \`${example.key}\`: ${JSON.stringify(example.source)} → ${JSON.stringify(example.target)}`);
1872
+ out.push("");
1873
+ }
1874
+ out.push("## Messages", "", ...data.items.flatMap((item) => renderItem(item, data.to)));
1875
+ out.push("## How to write results", "", ...data.instructions.map((line) => `- ${line}`), "");
1876
+ if (data.remaining > 0) out.push(`This prompt holds ${data.items.length} of ${data.total} messages.`, "");
1877
+ return out.join("\n");
1878
+ }
1879
+ //#endregion
1880
+ //#region src/project/usages.ts
1881
+ function innermost(scopes, range) {
1882
+ let best;
1883
+ for (const scope of scopes) if (scope.range.start <= range.start && scope.range.end >= range.end && (!best || scope.range.end - scope.range.start < best.range.end - best.range.start)) best = scope;
1884
+ return best;
1885
+ }
1886
+ function createUsageResolver(config, namespaced, normalizers) {
1887
+ const separator = config.namespace.separator;
1888
+ return { resolve(candidate, scopes, path, scanner) {
1889
+ const scope = innermost(scopes, candidate.range);
1890
+ const ctx = { path };
1891
+ let text = candidate.raw;
1892
+ for (const normalize of normalizers) {
1893
+ const result = normalize(text, ctx);
1894
+ if (result !== void 0) {
1895
+ text = result;
1896
+ break;
1897
+ }
1898
+ }
1899
+ const scopeNamespace = candidate.namespace ?? scope?.namespace;
1900
+ const scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
1901
+ let namespace;
1902
+ let explicit = false;
1903
+ if (namespaced) {
1904
+ const index = text.indexOf(separator);
1905
+ explicit = index > 0;
1906
+ if (explicit) {
1907
+ namespace = text.slice(0, index);
1908
+ text = text.slice(index + separator.length);
1909
+ } else namespace = scopeNamespace ?? config.namespace.default;
1910
+ }
1911
+ if (scopePrefix && !explicit) text = text ? `${scopePrefix}.${text}` : scopePrefix;
1912
+ let key;
1913
+ if (candidate.dynamic === "unknown") key = void 0;
1914
+ else key = joinKey(namespace, text === "" ? [] : text.split("."));
1915
+ return {
1916
+ key,
1917
+ raw: candidate.raw,
1918
+ path,
1919
+ range: candidate.range,
1920
+ scanner,
1921
+ kind: candidate.kind,
1922
+ dynamic: candidate.dynamic,
1923
+ locale: candidate.locale,
1924
+ scopeNamespace,
1925
+ scopePrefix
1926
+ };
1927
+ } };
1928
+ }
1929
+ /** True when a prefix usage may refer to `key`. The key of `t(`pages.${id}`)` is `pages.`, so only children match. */
1930
+ function prefixMatches(usage, key) {
1931
+ return usage.dynamic === "prefix" && usage.key !== void 0 && key.startsWith(usage.key);
1932
+ }
1933
+ /** Ranges covered by `x18n-disable-next-line` and `x18n-disable` ... `x18n-enable` comments. */
1934
+ function suppressedRanges(text) {
1935
+ if (!text.includes("x18n-disable")) return [];
1936
+ const ranges = [];
1937
+ for (const match of text.matchAll(/x18n-disable-next-line/g)) {
1938
+ const lineEnd = text.indexOf("\n", match.index);
1939
+ if (lineEnd === -1) continue;
1940
+ const nextEnd = text.indexOf("\n", lineEnd + 1);
1941
+ ranges.push({
1942
+ start: lineEnd + 1,
1943
+ end: nextEnd === -1 ? text.length : nextEnd
1944
+ });
1945
+ }
1946
+ for (const match of text.matchAll(/x18n-disable(?![-\w])/g)) {
1947
+ const end = text.indexOf("x18n-enable", match.index);
1948
+ ranges.push({
1949
+ start: match.index,
1950
+ end: end === -1 ? text.length : end
1951
+ });
1952
+ }
1953
+ return ranges;
1954
+ }
1955
+ /** Compiles derived-key rules (`{key}_plural`, `{key}_*`) into a function from a key to its base key. */
1956
+ function derivedBase(rules) {
1957
+ const regexes = rules.map((rule) => {
1958
+ const source = rule.split("{key}").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^.:]+")).join("(.+)");
1959
+ return new RegExp(`^${source}$`);
1960
+ });
1961
+ return (key) => {
1962
+ for (const re of regexes) {
1963
+ const match = re.exec(key);
1964
+ if (match?.[1]) return match[1];
1965
+ }
1966
+ };
1967
+ }
1968
+ //#endregion
1969
+ //#region src/project/project.ts
1970
+ const scanConcurrency = 64;
1971
+ function createProject(options) {
1972
+ const { host, setup } = options;
1973
+ const { config, registry } = setup;
1974
+ const listeners = /* @__PURE__ */ new Set();
1975
+ const fileTexts = /* @__PURE__ */ new Map();
1976
+ const resourceStates = /* @__PURE__ */ new Map();
1977
+ const files = /* @__PURE__ */ new Map();
1978
+ const discoveryErrors = [];
1979
+ let duplicates = [];
1980
+ let finderResults = [];
1981
+ let revision = 0;
1982
+ let namespaced = false;
1983
+ let locales = [];
1984
+ let sourceLocale = config.sourceLocale;
1985
+ let resolver;
1986
+ let watcher;
1987
+ let usageEligible = () => false;
1988
+ let resourceIndex;
1989
+ let resolved = /* @__PURE__ */ new WeakMap();
1990
+ const normalizers = registry.plugins.flatMap((plugin) => plugin.normalizeKey ? [plugin.normalizeKey] : []);
1991
+ const transforms = registry.plugins.flatMap((plugin) => plugin.transformResource ? [plugin.transformResource] : []);
1992
+ const localeInfoHooks = registry.plugins.flatMap((plugin) => plugin.localeInfo ? [plugin.localeInfo] : []);
1993
+ const baseOf = derivedBase(config.usage.derivedKeys);
1994
+ const scanSignature = hashText(JSON.stringify({
1995
+ usage: config.usage,
1996
+ namespace: config.namespace,
1997
+ scanners: registry.scanners.map((s) => s.id),
1998
+ v: 1
1999
+ }));
2000
+ const localeFilter = (code) => (!config.locales || config.locales.includes(code)) && !config.ignoreLocales.includes(code);
2001
+ const infoCache = /* @__PURE__ */ new Map();
2002
+ function localeInfo(code) {
2003
+ let info = infoCache.get(code);
2004
+ if (!info) {
2005
+ info = parseLocaleInfo(code);
2006
+ for (const hook of localeInfoHooks) {
2007
+ const result = hook(code);
2008
+ if (result) {
2009
+ info = {
2010
+ ...info,
2011
+ ...result,
2012
+ code
2013
+ };
2014
+ break;
2015
+ }
2016
+ }
2017
+ infoCache.set(code, info);
2018
+ }
2019
+ return info;
2020
+ }
2021
+ async function discover() {
2022
+ const ctx = {
2023
+ host,
2024
+ config,
2025
+ extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
2026
+ };
2027
+ const seen = /* @__PURE__ */ new Map();
2028
+ const out = [];
2029
+ duplicates = [];
2030
+ finderResults = [];
2031
+ for (const finder of registry.finders) {
2032
+ const found = await finder.find(ctx);
2033
+ const kept = [];
2034
+ for (const candidate of found) {
2035
+ const id = candidate.block ? `${candidate.path}#${candidate.block.index}` : candidate.path;
2036
+ const previous = seen.get(id);
2037
+ if (previous) {
2038
+ duplicates.push({
2039
+ path: id,
2040
+ finder: finder.id,
2041
+ keptFrom: previous
2042
+ });
2043
+ continue;
2044
+ }
2045
+ seen.set(id, finder.id);
2046
+ if (!localeFilter(candidate.locale)) continue;
2047
+ kept.push(id);
2048
+ out.push({
2049
+ path: candidate.path,
2050
+ locale: candidate.locale,
2051
+ namespace: candidate.namespace,
2052
+ format: candidate.format,
2053
+ block: candidate.block,
2054
+ readonly: candidate.readonly === true,
2055
+ finder: finder.id
2056
+ });
2057
+ }
2058
+ finderResults.push({
2059
+ id: finder.id,
2060
+ resources: kept
2061
+ });
2062
+ }
2063
+ return out;
2064
+ }
2065
+ function keyFor(resource, stored) {
2066
+ const logical = stored.flatMap((segment) => segment.split("."));
2067
+ if (namespaced) return joinKey(resource.namespace ?? config.namespace.default, logical);
2068
+ return joinKey(void 0, [...resource.namespace ? resource.namespace.split(".") : [], ...logical]);
2069
+ }
2070
+ /** Key path to stored path through the transforms that apply to a resource, last first. */
2071
+ function storedPathOf(ctx) {
2072
+ const active = transforms.filter((transform) => transform.applies?.(ctx) !== false);
2073
+ if (active.length === 0) return (path) => [...path];
2074
+ return (path) => active.reduceRight((current, transform) => transform.toStored(current, ctx), [...path]);
2075
+ }
2076
+ function parseResource(candidate, text) {
2077
+ const format = registry.formatFor(candidate.path, candidate.format);
2078
+ const resource = {
2079
+ id: candidate.block ? `${candidate.path}#${candidate.block.index}` : candidate.path,
2080
+ path: candidate.path,
2081
+ locale: candidate.locale,
2082
+ namespace: candidate.namespace,
2083
+ format: format?.id ?? "unknown",
2084
+ block: candidate.block,
2085
+ readonly: candidate.readonly || !format || !format.edit && !format.serialize,
2086
+ finder: candidate.finder
2087
+ };
2088
+ const content = candidate.block ? text.slice(candidate.block.range.start, candidate.block.range.end) : text;
2089
+ const offset = candidate.block?.range.start ?? 0;
2090
+ const ctx = {
2091
+ path: candidate.path,
2092
+ locale: candidate.locale,
2093
+ namespace: candidate.namespace,
2094
+ indent: config.write.indent
2095
+ };
2096
+ const base = {
2097
+ resource,
2098
+ hash: hashText(content),
2099
+ content,
2100
+ offset,
2101
+ toStored: storedPathOf(ctx)
2102
+ };
2103
+ if (!format) return {
2104
+ ...base,
2105
+ tree: {},
2106
+ rawTree: {},
2107
+ ranges: /* @__PURE__ */ new Map(),
2108
+ error: new X18nError("X18N2001", candidate.path)
2109
+ };
2110
+ try {
2111
+ const parsed = format.parse(content, ctx);
2112
+ let tree = parsed.tree;
2113
+ for (const transform of transforms) if (transform.applies?.(ctx) !== false) tree = transform.read(tree, ctx);
2114
+ if (!isMessageTree(tree)) throw new X18nError("X18N2003", candidate.path);
2115
+ return {
2116
+ ...base,
2117
+ tree,
2118
+ rawTree: parsed.tree,
2119
+ ranges: parsed.ranges ?? /* @__PURE__ */ new Map(),
2120
+ error: void 0
2121
+ };
2122
+ } catch (error) {
2123
+ const coded = isX18nError(error) ? error : new X18nError("X18N2002", `${candidate.path}: ${error instanceof Error ? error.message : String(error)}`);
2124
+ return {
2125
+ ...base,
2126
+ tree: {},
2127
+ rawTree: {},
2128
+ ranges: /* @__PURE__ */ new Map(),
2129
+ error: coded
2130
+ };
2131
+ }
2132
+ }
2133
+ function messagesIn(resource, text) {
2134
+ if (resource.block) return void 0;
2135
+ const format = resource.format === "unknown" ? void 0 : resource.format;
2136
+ const state = parseResource({
2137
+ ...resource,
2138
+ format,
2139
+ block: void 0
2140
+ }, text);
2141
+ if (state.error) return void 0;
2142
+ return new Map([...walkLeaves(state.tree)].map((leaf) => [keyFor(state.resource, leaf.path), leaf.value]));
2143
+ }
2144
+ async function loadResources(candidates, reread) {
2145
+ const next = /* @__PURE__ */ new Map();
2146
+ const paths = [...new Set(candidates.map((c) => c.path))];
2147
+ await Promise.all(paths.map(async (path) => {
2148
+ if (reread === "all" || reread.has(path) || !fileTexts.has(path)) {
2149
+ const text = await host.readFile(path);
2150
+ if (text === void 0) fileTexts.delete(path);
2151
+ else fileTexts.set(path, text);
2152
+ }
2153
+ }));
2154
+ namespaced = config.namespace.separator !== "." && candidates.some((c) => c.namespace !== void 0);
2155
+ for (const candidate of candidates) {
2156
+ const text = fileTexts.get(candidate.path);
2157
+ if (text === void 0) continue;
2158
+ const id = candidate.block ? `${candidate.path}#${candidate.block.index}` : candidate.path;
2159
+ const previous = resourceStates.get(id);
2160
+ const content = candidate.block ? text.slice(candidate.block.range.start, candidate.block.range.end) : text;
2161
+ if (previous && previous.hash === hashText(content) && previous.resource.locale === candidate.locale && previous.resource.namespace === candidate.namespace && previous.offset === (candidate.block?.range.start ?? 0)) next.set(id, previous);
2162
+ else next.set(id, parseResource(candidate, text));
2163
+ }
2164
+ resourceStates.clear();
2165
+ for (const [id, state] of next) resourceStates.set(id, state);
2166
+ const found = [...new Set([...resourceStates.values()].map((state) => state.resource.locale))];
2167
+ sourceLocale = config.sourceLocale;
2168
+ if (!found.includes(sourceLocale) && config.origins.sourceLocale === void 0 && found.length > 0) sourceLocale = found.find((code) => code.toLowerCase().startsWith("en")) ?? [...found].sort()[0];
2169
+ locales = [sourceLocale, ...found.filter((code) => code !== sourceLocale).sort()];
2170
+ if (!found.includes(sourceLocale) && found.length === 0) locales = [sourceLocale];
2171
+ resolver = createUsageResolver(config, namespaced, normalizers);
2172
+ resolved = /* @__PURE__ */ new WeakMap();
2173
+ resourceIndex = void 0;
2174
+ }
2175
+ async function scanOne(file) {
2176
+ const usages = [];
2177
+ const scopes = [];
2178
+ const keySlots = [];
2179
+ const hardStrings = [];
2180
+ const calls = config.usage.calls.filter((spec) => ![spec.callee].flat().some((name) => config.usage.ignoreCallees.includes(name)));
2181
+ for (const scanner of registry.scannersFor(file.path)) {
2182
+ const result = await scanner.scan(file, {
2183
+ config,
2184
+ calls
2185
+ });
2186
+ usages.push(...result.usages);
2187
+ scopes.push(...result.scopes ?? []);
2188
+ keySlots.push(...result.keySlots ?? []);
2189
+ hardStrings.push(...result.hardStrings ?? []);
2190
+ }
2191
+ const suppressed = suppressedRanges(file.text);
2192
+ const seen = /* @__PURE__ */ new Set();
2193
+ const kept = usages.filter((usage) => {
2194
+ const id = `${usage.range.start}:${usage.range.end}`;
2195
+ if (seen.has(id)) return false;
2196
+ seen.add(id);
2197
+ return !suppressed.some((range) => usage.range.start >= range.start && usage.range.end <= range.end);
2198
+ });
2199
+ kept.sort((a, b) => a.range.start - b.range.start);
2200
+ return {
2201
+ usages: kept,
2202
+ scopes,
2203
+ keySlots,
2204
+ hardStrings
2205
+ };
2206
+ }
2207
+ async function scanPaths(paths) {
2208
+ const pending = [];
2209
+ const cacheKeys = /* @__PURE__ */ new Map();
2210
+ await Promise.all(paths.map(async (path) => {
2211
+ const text = await host.readFile(path);
2212
+ if (text === void 0) {
2213
+ files.delete(path);
2214
+ return;
2215
+ }
2216
+ const hash = hashText(text);
2217
+ if (files.get(path)?.hash === hash) return;
2218
+ if (host.cache) {
2219
+ const cacheKey = `scan:${scanSignature}:${path}:${hash}`;
2220
+ cacheKeys.set(path, cacheKey);
2221
+ const cached = await host.cache.get(cacheKey);
2222
+ if (cached !== void 0) {
2223
+ files.set(path, {
2224
+ hash,
2225
+ text,
2226
+ result: JSON.parse(cached),
2227
+ error: void 0
2228
+ });
2229
+ return;
2230
+ }
2231
+ }
2232
+ pending.push({
2233
+ path,
2234
+ text,
2235
+ extension: extensionOf(path)
2236
+ });
2237
+ }));
2238
+ const store = (file, result, error) => {
2239
+ files.set(file.path, {
2240
+ hash: hashText(file.text),
2241
+ text: file.text,
2242
+ result: result ?? { usages: [] },
2243
+ error
2244
+ });
2245
+ const cacheKey = cacheKeys.get(file.path);
2246
+ if (result && cacheKey && host.cache) host.cache.set(cacheKey, JSON.stringify(result));
2247
+ };
2248
+ for (let i = 0; i < pending.length; i += scanConcurrency) await Promise.all(pending.slice(i, i + scanConcurrency).map(async (file) => {
2249
+ try {
2250
+ store(file, await scanOne(file));
2251
+ } catch (error) {
2252
+ store(file, void 0, new X18nError("X18N3001", `${file.path}: ${error instanceof Error ? error.message : String(error)}`));
2253
+ }
2254
+ }));
2255
+ }
2256
+ async function scanAll() {
2257
+ if (registry.scanners.length === 0) return;
2258
+ const include = pathMatcher(config.usage.include);
2259
+ const exclude = pathMatcher(config.usage.exclude);
2260
+ usageEligible = (path) => registry.scannersFor(path).length > 0 && include(path) && !exclude(path);
2261
+ await scanPaths((await host.glob(config.usage.include, {
2262
+ ignore: config.usage.exclude,
2263
+ gitignore: true
2264
+ })).filter((path) => registry.scannersFor(path).length > 0));
2265
+ }
2266
+ let index;
2267
+ function buildResourceIndex() {
2268
+ const messages = /* @__PURE__ */ new Map();
2269
+ const keyPaths = /* @__PURE__ */ new Map();
2270
+ const nodes = /* @__PURE__ */ new Map();
2271
+ const parents = /* @__PURE__ */ new Set();
2272
+ for (const state of resourceStates.values()) {
2273
+ const { resource } = state;
2274
+ const fileNodes = nodes.get(resource.path) ?? [];
2275
+ nodes.set(resource.path, fileNodes);
2276
+ const shift = (range) => ({
2277
+ start: range.start + state.offset,
2278
+ end: range.end + state.offset
2279
+ });
2280
+ const visit = (tree, stored) => {
2281
+ for (const [segment, child] of Object.entries(tree)) {
2282
+ const path = [...stored, segment];
2283
+ const key = keyFor(resource, path);
2284
+ const storedPath = state.toStored(path);
2285
+ const raw = state.ranges.get(pathId(storedPath));
2286
+ const range = raw ? {
2287
+ key: shift(raw.key),
2288
+ value: shift(raw.value)
2289
+ } : void 0;
2290
+ if (range) fileNodes.push({
2291
+ key,
2292
+ range,
2293
+ leaf: isMessageLeaf(child),
2294
+ locale: resource.locale,
2295
+ path: resource.path
2296
+ });
2297
+ if (isMessageLeaf(child)) {
2298
+ const byLocale = messages.get(key) ?? /* @__PURE__ */ new Map();
2299
+ messages.set(key, byLocale);
2300
+ const list = byLocale.get(resource.locale) ?? [];
2301
+ byLocale.set(resource.locale, list);
2302
+ list.push({
2303
+ key,
2304
+ locale: resource.locale,
2305
+ value: child,
2306
+ resource: resource.id,
2307
+ storedPath,
2308
+ range
2309
+ });
2310
+ if (!keyPaths.has(key)) {
2311
+ const logical = path.flatMap((s) => s.split("."));
2312
+ keyPaths.set(key, namespaced ? {
2313
+ namespace: resource.namespace ?? config.namespace.default,
2314
+ path: logical
2315
+ } : {
2316
+ namespace: void 0,
2317
+ path: [...resource.namespace ? resource.namespace.split(".") : [], ...logical]
2318
+ });
2319
+ }
2320
+ } else {
2321
+ parents.add(key);
2322
+ visit(child, path);
2323
+ }
2324
+ }
2325
+ };
2326
+ visit(state.tree, []);
2327
+ }
2328
+ for (const { namespace, path } of keyPaths.values()) for (let i = 1; i < path.length; i++) parents.add(joinKey(namespace, path.slice(0, i)));
2329
+ const derived = /* @__PURE__ */ new Map();
2330
+ if (config.usage.derivedKeys.length > 0) for (const key of messages.keys()) {
2331
+ const base = baseOf(key);
2332
+ if (base) {
2333
+ const list = derived.get(base) ?? [];
2334
+ derived.set(base, list);
2335
+ list.push(key);
2336
+ }
2337
+ }
2338
+ return {
2339
+ messages,
2340
+ keyPaths,
2341
+ nodes,
2342
+ parents,
2343
+ derived
2344
+ };
2345
+ }
2346
+ function buildIndex() {
2347
+ resourceIndex ??= buildResourceIndex();
2348
+ const usages = [];
2349
+ for (const [path, state] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
2350
+ let list = resolved.get(state);
2351
+ if (!list) {
2352
+ list = state.result.usages.map((candidate) => resolver.resolve(candidate, state.result.scopes ?? [], path, "scanner"));
2353
+ resolved.set(state, list);
2354
+ }
2355
+ usages.push(...list);
2356
+ }
2357
+ const usagesByKey = /* @__PURE__ */ new Map();
2358
+ const prefixUsages = [];
2359
+ for (const usage of usages) {
2360
+ if (usage.key === void 0) continue;
2361
+ if (usage.dynamic === "prefix") {
2362
+ prefixUsages.push(usage);
2363
+ continue;
2364
+ }
2365
+ const list = usagesByKey.get(usage.key) ?? [];
2366
+ usagesByKey.set(usage.key, list);
2367
+ list.push(usage);
2368
+ }
2369
+ return {
2370
+ revision,
2371
+ ...resourceIndex,
2372
+ usages,
2373
+ usagesByKey,
2374
+ prefixUsages
2375
+ };
2376
+ }
2377
+ function idx() {
2378
+ if (!index || index.revision !== revision) index = buildIndex();
2379
+ return index;
2380
+ }
2381
+ function message(key, locale) {
2382
+ return idx().messages.get(key)?.get(locale)?.[0];
2383
+ }
2384
+ const history = createHistory({
2385
+ providers: config.history === false ? [] : [...config.history.providers, ...registry.histories],
2386
+ context: () => ({
2387
+ host,
2388
+ config,
2389
+ resources: [...resourceStates.values()].map((state) => state.resource),
2390
+ messagesIn
2391
+ }),
2392
+ current: (key, locale) => message(key, locale)?.value,
2393
+ onLoad() {
2394
+ revision++;
2395
+ emit({
2396
+ revision,
2397
+ paths: [],
2398
+ keys: []
2399
+ });
2400
+ },
2401
+ logger: host.logger
2402
+ });
2403
+ function usageMatches(query = {}) {
2404
+ const { usages, usagesByKey, prefixUsages } = idx();
2405
+ const inPath = (usage) => query.path === void 0 || usage.path === query.path;
2406
+ if (query.key !== void 0) {
2407
+ const key = query.key;
2408
+ const base = baseOf(key);
2409
+ return [...[...usagesByKey.get(key) ?? [], ...base ? usagesByKey.get(base) ?? [] : []].filter(inPath).map((usage) => ({
2410
+ usage,
2411
+ possible: false
2412
+ })), ...prefixUsages.filter((usage) => inPath(usage) && prefixMatches(usage, key)).map((usage) => ({
2413
+ usage,
2414
+ possible: true
2415
+ }))];
2416
+ }
2417
+ if (query.prefix !== void 0) {
2418
+ const prefix = query.prefix;
2419
+ return [...usages.filter((usage) => inPath(usage) && usage.dynamic === false && usage.key !== void 0 && isKeyUnder(usage.key, prefix)).map((usage) => ({
2420
+ usage,
2421
+ possible: false
2422
+ })), ...prefixUsages.filter((usage) => inPath(usage) && usage.key !== void 0 && (usage.key.startsWith(prefix) || prefix.startsWith(usage.key))).map((usage) => ({
2423
+ usage,
2424
+ possible: true
2425
+ }))];
2426
+ }
2427
+ return usages.filter(inPath).map((usage) => ({
2428
+ usage,
2429
+ possible: usage.dynamic !== false
2430
+ }));
2431
+ }
2432
+ const analyzer = createAnalyzer({
2433
+ config,
2434
+ registry,
2435
+ state: () => ({
2436
+ revision,
2437
+ sourceLocale,
2438
+ locales,
2439
+ resources: [...resourceStates.values()],
2440
+ resourceErrors: [...resourceStates.values()].flatMap((s) => s.error ? [{
2441
+ error: s.error,
2442
+ path: s.resource.path
2443
+ }] : []),
2444
+ scanErrors: [...files].flatMap(([path, s]) => s.error ? [{
2445
+ error: s.error,
2446
+ path
2447
+ }] : []),
2448
+ discoveryErrors,
2449
+ messages: idx().messages,
2450
+ parents: idx().parents,
2451
+ usages: idx().usages,
2452
+ usagesByKey: idx().usagesByKey,
2453
+ prefixUsages: idx().prefixUsages,
2454
+ derived: idx().derived,
2455
+ files,
2456
+ history: history.get
2457
+ }),
2458
+ baseOf,
2459
+ localeInfo,
2460
+ text: (path) => fileTexts.get(path) ?? files.get(path)?.text
2461
+ });
2462
+ function keys(query = {}) {
2463
+ const { messages, keyPaths } = idx();
2464
+ const match = query.glob ? keyMatcher([query.glob]) : void 0;
2465
+ let list = [...messages.keys()];
2466
+ if (match) list = list.filter((key) => match(key));
2467
+ if (query.namespace !== void 0) list = list.filter((key) => keyPaths.get(key)?.namespace === query.namespace);
2468
+ if (query.missing !== void 0) {
2469
+ const locale = query.missing;
2470
+ list = list.filter((key) => {
2471
+ const value = message(key, locale)?.value;
2472
+ return message(key, sourceLocale) !== void 0 && (value === void 0 || value === "");
2473
+ });
2474
+ }
2475
+ if (query.unused) {
2476
+ const unused = new Set(analyzer.analyze().unused);
2477
+ list = list.filter((key) => unused.has(key));
2478
+ }
2479
+ return list.sort();
2480
+ }
2481
+ function get(key) {
2482
+ const { messages, keyPaths } = idx();
2483
+ const byLocale = messages.get(key);
2484
+ if (!byLocale) return void 0;
2485
+ const id = keyPaths.get(key);
2486
+ const list = locales.flatMap((locale) => byLocale.get(locale)?.slice(0, 1) ?? []);
2487
+ return {
2488
+ key,
2489
+ namespace: id.namespace,
2490
+ path: id.path,
2491
+ messages: list,
2492
+ missing: locales.filter((locale) => !byLocale.has(locale)),
2493
+ usages: usageMatches({ key }),
2494
+ diagnostics: analyzer.analyze().diagnostics.filter((d) => d.key === key)
2495
+ };
2496
+ }
2497
+ function keyAt(path, offset) {
2498
+ const fileNodes = idx().nodes.get(path);
2499
+ if (fileNodes) {
2500
+ let best;
2501
+ for (const node of fileNodes) if ((rangeContains(node.range.key, offset) || node.leaf && rangeContains(node.range.value, offset)) && (!best || node.range.key.start > best.range.key.start)) best = node;
2502
+ if (best) return best.leaf ? {
2503
+ type: "key",
2504
+ key: best.key,
2505
+ range: best.range.key,
2506
+ source: "resource"
2507
+ } : {
2508
+ type: "subtree",
2509
+ key: best.key,
2510
+ range: best.range.key,
2511
+ source: "resource"
2512
+ };
2513
+ }
2514
+ if (files.get(path)) {
2515
+ const usage = idx().usages.find((u) => u.path === path && u.key !== void 0 && rangeContains(u.range, offset));
2516
+ if (usage?.key !== void 0) return {
2517
+ type: "key",
2518
+ key: usage.key,
2519
+ range: usage.range,
2520
+ source: "usage",
2521
+ usage
2522
+ };
2523
+ }
2524
+ }
2525
+ function explain() {
2526
+ const resources = [...resourceStates.values()];
2527
+ return {
2528
+ name: config.name,
2529
+ root: host.root,
2530
+ presets: setup.presets,
2531
+ plugins: registry.plugins.map((plugin) => plugin.name),
2532
+ skipped: setup.skipped,
2533
+ finders: finderResults,
2534
+ duplicates,
2535
+ formats: registry.formats.map((format) => ({
2536
+ id: format.id,
2537
+ extensions: format.extensions,
2538
+ resources: resources.filter((state) => state.resource.format === format.id).length
2539
+ })),
2540
+ scanners: registry.scanners.map((scanner) => ({
2541
+ id: scanner.id,
2542
+ extensions: scanner.extensions,
2543
+ files: [...files.keys()].filter((path) => registry.scannersFor(path).includes(scanner)).length
2544
+ })),
2545
+ conflicts: registry.conflicts,
2546
+ errors: [
2547
+ ...discoveryErrors,
2548
+ ...resources.flatMap((state) => state.error ? [{
2549
+ error: state.error,
2550
+ path: state.resource.path
2551
+ }] : []),
2552
+ ...[...files].flatMap(([path, state]) => state.error ? [{
2553
+ error: state.error,
2554
+ path
2555
+ }] : [])
2556
+ ].map(({ error, path }) => ({
2557
+ ...error.toJSON(),
2558
+ path
2559
+ })),
2560
+ config
2561
+ };
2562
+ }
2563
+ function emit(change) {
2564
+ for (const listener of listeners) listener(change);
2565
+ }
2566
+ function touchedKeys(paths, before) {
2567
+ const out = /* @__PURE__ */ new Set();
2568
+ const collect = (from) => {
2569
+ if (!from) return;
2570
+ for (const path of paths) for (const node of from.nodes.get(path) ?? []) out.add(node.key);
2571
+ for (const usage of from.usages) if (usage.key !== void 0 && paths.has(usage.path)) out.add(usage.key);
2572
+ };
2573
+ collect(before);
2574
+ collect(idx());
2575
+ return [...out];
2576
+ }
2577
+ let current = Promise.resolve();
2578
+ async function update(events) {
2579
+ const run = current.then(async () => {
2580
+ if (events.length === 0) return void 0;
2581
+ const before = idx();
2582
+ const paths = new Set(events.map((event) => event.path));
2583
+ const resourcePaths = new Set([...resourceStates.values()].map((state) => state.resource.path));
2584
+ const maybeResource = (path) => !resourcePaths.has(path) && registry.formatFor(path) !== void 0 && !usageEligible(path);
2585
+ const structural = events.some((event) => event.type !== "change" || maybeResource(event.path));
2586
+ if (structural || [...paths].some((path) => resourcePaths.has(path))) await loadResources(structural ? await discover() : [...resourceStates.values()].map((state) => ({
2587
+ ...state.resource,
2588
+ format: state.resource.format === "unknown" ? void 0 : state.resource.format
2589
+ })), paths);
2590
+ const scanTargets = [...paths].filter((path) => usageEligible(path) || files.has(path));
2591
+ for (const event of events) if (event.type === "delete") files.delete(event.path);
2592
+ await scanPaths(scanTargets.filter((path) => events.some((event) => event.path === path && event.type !== "delete")));
2593
+ revision++;
2594
+ const change = {
2595
+ revision,
2596
+ paths: [...paths],
2597
+ keys: touchedKeys(paths, before)
2598
+ };
2599
+ emit(change);
2600
+ return change;
2601
+ });
2602
+ current = run.then(() => void 0, () => void 0);
2603
+ return run;
2604
+ }
2605
+ const planner = createPlanner({
2606
+ host,
2607
+ config,
2608
+ registry,
2609
+ state: () => ({
2610
+ revision,
2611
+ sourceLocale,
2612
+ locales,
2613
+ namespaced,
2614
+ resources: [...resourceStates.values()],
2615
+ fileTexts
2616
+ }),
2617
+ message: (key, locale) => message(key, locale),
2618
+ messages: (key) => idx().messages.get(key),
2619
+ allKeys: () => [...idx().messages.keys()],
2620
+ parents: () => idx().parents,
2621
+ usageMatches,
2622
+ text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
2623
+ baseOf,
2624
+ toStored: (target, path) => storedPathOf(target)(path),
2625
+ resolveNew(locale, namespace) {
2626
+ const ctx = {
2627
+ host,
2628
+ config,
2629
+ extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
2630
+ };
2631
+ for (const finder of registry.finders) {
2632
+ const path = finder.resolveNew?.({
2633
+ locale,
2634
+ namespace
2635
+ }, ctx);
2636
+ if (path) return path;
2637
+ }
2638
+ },
2639
+ update
2640
+ });
2641
+ const ready = (async () => {
2642
+ await loadResources(await discover(), "all");
2643
+ await scanAll();
2644
+ revision++;
2645
+ })();
2646
+ const project = {
2647
+ get name() {
2648
+ return config.name;
2649
+ },
2650
+ config,
2651
+ ready,
2652
+ get revision() {
2653
+ return revision;
2654
+ },
2655
+ get locales() {
2656
+ return locales;
2657
+ },
2658
+ get sourceLocale() {
2659
+ return sourceLocale;
2660
+ },
2661
+ get resources() {
2662
+ return [...resourceStates.values()].map((state) => state.resource);
2663
+ },
2664
+ get namespaced() {
2665
+ return namespaced;
2666
+ },
2667
+ reporters: registry.reporters,
2668
+ placeholderSyntaxes: registry.placeholders,
2669
+ extractTemplates: [...(config.extract.templates ?? []).map((template) => typeof template === "string" ? { template } : template), ...registry.extractors.flatMap((extractor) => extractor.templates)],
2670
+ keys,
2671
+ get,
2672
+ message,
2673
+ parseKey(input) {
2674
+ const separator = config.namespace.separator;
2675
+ if (!namespaced) return input;
2676
+ if (input.indexOf(":") > 0) return input;
2677
+ const custom = separator !== ":" ? input.indexOf(separator) : -1;
2678
+ if (custom > 0) return `${input.slice(0, custom)}:${input.slice(custom + separator.length)}`;
2679
+ return config.namespace.default ? `${config.namespace.default}:${input}` : input;
2680
+ },
2681
+ usages: usageMatches,
2682
+ keyAt,
2683
+ nodes: (path) => idx().nodes.get(path) ?? [],
2684
+ locations(key) {
2685
+ const order = new Map(locales.map((locale, i) => [locale, i]));
2686
+ const all = [...idx().nodes.values()].flat();
2687
+ let found = all.filter((node) => node.key === key);
2688
+ if (found.length === 0) {
2689
+ const derived = new Set(idx().derived.get(key) ?? []);
2690
+ found = all.filter((node) => derived.has(node.key));
2691
+ }
2692
+ return found.sort((a, b) => (order.get(a.locale) ?? 0) - (order.get(b.locale) ?? 0));
2693
+ },
2694
+ lookup(key, locale) {
2695
+ const exact = message(key, locale);
2696
+ if (exact) return exact;
2697
+ const derived = [...idx().derived.get(key) ?? []].sort();
2698
+ const other = derived.find((k) => /[._-]other$/.test(k)) ?? derived[0];
2699
+ return other === void 0 ? void 0 : message(other, locale);
2700
+ },
2701
+ keySlots: (path) => files.get(path)?.result.keySlots ?? [],
2702
+ hardStrings: (path) => [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).filter(([p]) => path === void 0 || p === path).flatMap(([p, state]) => (state.result.hardStrings ?? []).map((h) => ({
2703
+ ...h,
2704
+ path: p
2705
+ }))),
2706
+ analyze: () => analyzer.analyze(),
2707
+ async loadHistory(historyOptions = {}) {
2708
+ await ready;
2709
+ await history.load(historyOptions);
2710
+ },
2711
+ history: history.get,
2712
+ valueAt: history.valueAt,
2713
+ prompt: (kind, locale, promptOptions = {}) => buildPrompt(project, kind, locale, promptOptions, {
2714
+ config,
2715
+ hooks: registry.plugins.flatMap((plugin) => plugin.translatePrompt ? [plugin.translatePrompt] : [])
2716
+ }),
2717
+ explain,
2718
+ localeInfo,
2719
+ text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
2720
+ owns: (path) => fileTexts.has(path) || files.has(path) || usageEligible(path),
2721
+ plan: (ops) => planner.plan(ops),
2722
+ apply: (plan, applyOptions) => planner.apply(plan, applyOptions?.answers ?? {}),
2723
+ update,
2724
+ watch() {
2725
+ if (watcher || !host.watch) return;
2726
+ watcher = host.watch(["**/*"], (events) => {
2727
+ update(events);
2728
+ });
2729
+ },
2730
+ onChange(listener) {
2731
+ listeners.add(listener);
2732
+ return () => listeners.delete(listener);
2733
+ },
2734
+ dispose() {
2735
+ watcher?.dispose();
2736
+ history.dispose();
2737
+ listeners.clear();
2738
+ }
2739
+ };
2740
+ return project;
2741
+ }
2742
+ //#endregion
2743
+ //#region src/resolve.ts
2744
+ const defaultExclude = [
2745
+ "**/node_modules/**",
2746
+ "**/.git/**",
2747
+ "**/dist/**",
2748
+ "**/build/**",
2749
+ "**/coverage/**",
2750
+ "**/.nuxt/**",
2751
+ "**/.output/**",
2752
+ "**/.next/**",
2753
+ "**/.svelte-kit/**",
2754
+ "**/*.d.ts",
2755
+ "**/*.min.js"
2756
+ ];
2757
+ function defaultDraft() {
2758
+ return {
2759
+ sourceLocale: "en",
2760
+ locales: void 0,
2761
+ ignoreLocales: [],
2762
+ resources: [],
2763
+ formats: {},
2764
+ keyStyle: "auto",
2765
+ namespace: {
2766
+ separator: ".",
2767
+ default: void 0,
2768
+ fromPath: "dot"
2769
+ },
2770
+ usage: {
2771
+ include: ["**/*"],
2772
+ exclude: defaultExclude,
2773
+ calls: [],
2774
+ regex: [],
2775
+ ignoreCallees: [],
2776
+ keysInUse: [],
2777
+ derivedKeys: []
2778
+ },
2779
+ write: {
2780
+ sort: false,
2781
+ indent: void 0,
2782
+ keepFilled: false
2783
+ },
2784
+ extract: {},
2785
+ checks: {},
2786
+ strict: false,
2787
+ ignoreKeys: [],
2788
+ glossary: [],
2789
+ history: { providers: [] },
2790
+ pluginConfigs: {}
2791
+ };
2792
+ }
2793
+ function serializablePart(config) {
2794
+ const { presets, plugins, pluginConfigs, history, ...rest } = config;
2795
+ if (typeof history !== "object") return {
2796
+ ...rest,
2797
+ history
2798
+ };
2799
+ const { providers, ...options } = history;
2800
+ return {
2801
+ ...rest,
2802
+ history: options
2803
+ };
2804
+ }
2805
+ /** Applies a user config layer: scalars replace, lists of rules append. */
2806
+ function applyLayer(draft, layer, origin, origins) {
2807
+ const mark = (key) => {
2808
+ origins[key] = origin;
2809
+ };
2810
+ if (layer.sourceLocale !== void 0) {
2811
+ draft.sourceLocale = layer.sourceLocale;
2812
+ mark("sourceLocale");
2813
+ }
2814
+ if (layer.locales !== void 0) {
2815
+ draft.locales = layer.locales;
2816
+ mark("locales");
2817
+ }
2818
+ if (layer.ignoreLocales !== void 0) {
2819
+ draft.ignoreLocales = [...draft.ignoreLocales, ...layer.ignoreLocales];
2820
+ mark("ignoreLocales");
2821
+ }
2822
+ if (layer.resources !== void 0) {
2823
+ draft.resources = layer.resources;
2824
+ mark("resources");
2825
+ }
2826
+ if (layer.formats !== void 0) {
2827
+ draft.formats = {
2828
+ ...draft.formats,
2829
+ ...layer.formats
2830
+ };
2831
+ mark("formats");
2832
+ }
2833
+ if (layer.keyStyle !== void 0) {
2834
+ draft.keyStyle = layer.keyStyle;
2835
+ mark("keyStyle");
2836
+ }
2837
+ if (layer.namespace !== void 0) {
2838
+ draft.namespace = {
2839
+ separator: layer.namespace.separator ?? draft.namespace.separator,
2840
+ default: layer.namespace.default ?? draft.namespace.default,
2841
+ fromPath: layer.namespace.fromPath ?? draft.namespace.fromPath
2842
+ };
2843
+ mark("namespace");
2844
+ }
2845
+ const usage = layer.usage;
2846
+ if (usage !== void 0) {
2847
+ draft.usage = {
2848
+ include: usage.include ?? draft.usage.include,
2849
+ exclude: [...draft.usage.exclude, ...usage.exclude ?? []],
2850
+ calls: [...draft.usage.calls, ...usage.calls ?? []],
2851
+ regex: [...draft.usage.regex, ...usage.regex ?? []],
2852
+ ignoreCallees: [...draft.usage.ignoreCallees, ...usage.ignoreCallees ?? []],
2853
+ keysInUse: [...draft.usage.keysInUse, ...usage.keysInUse ?? []],
2854
+ derivedKeys: [...draft.usage.derivedKeys, ...usage.derivedKeys ?? []]
2855
+ };
2856
+ mark("usage");
2857
+ }
2858
+ if (layer.write !== void 0) {
2859
+ draft.write = {
2860
+ sort: layer.write.sort ?? draft.write.sort,
2861
+ indent: layer.write.indent ?? draft.write.indent,
2862
+ keepFilled: layer.write.keepFilled ?? draft.write.keepFilled
2863
+ };
2864
+ mark("write");
2865
+ }
2866
+ if (layer.extract !== void 0) {
2867
+ draft.extract = {
2868
+ ...draft.extract,
2869
+ ...layer.extract,
2870
+ templates: [...draft.extract.templates ?? [], ...layer.extract.templates ?? []]
2871
+ };
2872
+ mark("extract");
2873
+ }
2874
+ if (layer.checks !== void 0) {
2875
+ draft.checks = {
2876
+ ...draft.checks,
2877
+ ...layer.checks
2878
+ };
2879
+ mark("checks");
2880
+ }
2881
+ if (layer.strict !== void 0) {
2882
+ draft.strict = layer.strict;
2883
+ mark("strict");
2884
+ }
2885
+ if (layer.ignoreKeys !== void 0) {
2886
+ draft.ignoreKeys = [...draft.ignoreKeys, ...layer.ignoreKeys];
2887
+ mark("ignoreKeys");
2888
+ }
2889
+ if (layer.glossary !== void 0) {
2890
+ draft.glossary = [...draft.glossary, ...layer.glossary];
2891
+ mark("glossary");
2892
+ }
2893
+ if (layer.history !== void 0) {
2894
+ const previous = draft.history || { providers: [] };
2895
+ draft.history = layer.history === false ? false : layer.history === true ? previous : {
2896
+ ...previous,
2897
+ ...layer.history,
2898
+ providers: [...previous.providers, ...layer.history.providers ?? []]
2899
+ };
2900
+ mark("history");
2901
+ }
2902
+ if (layer.pluginConfigs !== void 0) {
2903
+ draft.pluginConfigs = {
2904
+ ...draft.pluginConfigs,
2905
+ ...layer.pluginConfigs
2906
+ };
2907
+ mark("pluginConfigs");
2908
+ }
2909
+ }
2910
+ async function createDetectContext(host, readers, roots) {
2911
+ const dependencies = /* @__PURE__ */ new Set();
2912
+ const seen = /* @__PURE__ */ new Set();
2913
+ for (const root of ["", ...roots]) for (const reader of readers) {
2914
+ const path = root ? `${root.replace(/\/$/, "")}/${reader.file}` : reader.file;
2915
+ if (seen.has(path)) continue;
2916
+ seen.add(path);
2917
+ const text = await host.readFile(path);
2918
+ if (text === void 0) continue;
2919
+ try {
2920
+ for (const name of reader.read(text)) dependencies.add(name);
2921
+ } catch {
2922
+ host.logger?.warn(`Could not read manifest ${path}`);
2923
+ }
2924
+ }
2925
+ return {
2926
+ host,
2927
+ dependencies,
2928
+ hasDependency: (name) => dependencies.has(name),
2929
+ hasAnyDependency: (names) => names.some((name) => dependencies.has(name)),
2930
+ fileExists: async (pattern) => (await host.glob([pattern], { gitignore: true })).length > 0
2931
+ };
2932
+ }
2933
+ function validateLayers(layers) {
2934
+ for (const layer of layers) {
2935
+ if (!layer) continue;
2936
+ const result = validate(ConfigSchema, serializablePart(layer));
2937
+ if (!result.success) throw new X18nError("X18N0001", result.issues.map((issue) => `${issue.path || "(root)"}: ${issue.message}`).join("; "));
2938
+ }
2939
+ }
2940
+ function createLoader(options) {
2941
+ return async (option) => {
2942
+ if (option === false || option === void 0) return void 0;
2943
+ if (typeof option === "object" && !Array.isArray(option)) return option;
2944
+ const [specifier, pluginOptions] = typeof option === "string" ? [option, void 0] : [option[0], option[1]];
2945
+ const factory = options.builtins.presets[specifier];
2946
+ if (factory) return factory(pluginOptions);
2947
+ if (!options.loadPlugin) throw new X18nError("X18N1003", specifier);
2948
+ return options.loadPlugin(specifier, pluginOptions);
2949
+ };
2950
+ }
2951
+ async function loadAll(load, list) {
2952
+ const out = [];
2953
+ for (const option of list) {
2954
+ const loaded = await load(option);
2955
+ if (loaded) out.push(loaded);
2956
+ }
2957
+ return out;
2958
+ }
2959
+ /** Expands presets depth-first: listed presets always apply, others only when detected; an exclusive one stops the rest. */
2960
+ async function expandPresets(presets, userPlugins, listed, detectCtx) {
2961
+ const explanations = [];
2962
+ const plugins = [];
2963
+ const flatten = async (item, force) => {
2964
+ if (!isPreset(item)) {
2965
+ if (!plugins.some((plugin) => plugin.name === item.name)) plugins.push(item);
2966
+ return false;
2967
+ }
2968
+ const applies = force || (item.detect ? await item.detect(detectCtx) : false);
2969
+ explanations.push({
2970
+ name: item.name,
2971
+ reason: applies ? force ? "listed" : "detected" : "not-detected"
2972
+ });
2973
+ if (!applies) return false;
2974
+ for (const child of item.plugins) await flatten(child, false);
2975
+ return item.exclusive === true;
2976
+ };
2977
+ let stopped = false;
2978
+ for (const preset of presets) if (stopped) explanations.push({
2979
+ name: preset.name,
2980
+ reason: "after-exclusive"
2981
+ });
2982
+ else stopped = await flatten(preset, listed);
2983
+ for (const plugin of userPlugins) await flatten(plugin, true);
2984
+ return {
2985
+ plugins,
2986
+ explanations
2987
+ };
2988
+ }
2989
+ async function runConfigHooks(plugins, draft, ctx, origins) {
2990
+ for (const plugin of plugins) {
2991
+ if (!plugin.config) continue;
2992
+ const snapshot = Object.fromEntries(Object.entries(draft).map(([key, value]) => [key, JSON.stringify(value)]));
2993
+ await plugin.config(draft, ctx);
2994
+ for (const [key, value] of Object.entries(draft)) if (snapshot[key] !== JSON.stringify(value)) origins[key] = `plugin:${plugin.name}`;
2995
+ }
2996
+ }
2997
+ async function resolveConfig(options) {
2998
+ const { host, builtins } = options;
2999
+ const user = options.config;
3000
+ validateLayers([options.parent, user]);
3001
+ const load = createLoader(options);
3002
+ const userPlugins = await loadAll(load, [...options.parent?.plugins ?? [], ...user.plugins ?? []]);
3003
+ const presetOption = user.presets ?? options.parent?.presets ?? "auto";
3004
+ const auto = presetOption === "auto";
3005
+ const presets = auto ? Object.values(builtins.presets).map((factory) => factory()) : (await loadAll(load, presetOption)).map((item) => isPreset(item) ? item : {
3006
+ name: item.name,
3007
+ plugins: [item]
3008
+ });
3009
+ const detectCtx = await createDetectContext(host, [...builtins.plugins, ...userPlugins].flatMap((item) => isPreset(item) ? [] : item.manifests ?? []), options.manifestRoots ?? []);
3010
+ const expanded = await expandPresets(presets, userPlugins, !auto, detectCtx);
3011
+ const skipped = [];
3012
+ const finalPlugins = [...builtins.plugins, ...expanded.plugins].filter((plugin) => {
3013
+ const skip = options.runtime === "browser" && plugin.runtime === "node";
3014
+ if (skip) skipped.push({
3015
+ name: plugin.name,
3016
+ reason: "node only"
3017
+ });
3018
+ return !skip;
3019
+ });
3020
+ const draft = defaultDraft();
3021
+ const origins = {};
3022
+ await runConfigHooks(finalPlugins, draft, {
3023
+ host,
3024
+ user: serializablePart(user)
3025
+ }, origins);
3026
+ if (options.parent) applyLayer(draft, options.parent, "parent", origins);
3027
+ applyLayer(draft, user, "user", origins);
3028
+ const config = Object.freeze({
3029
+ ...draft,
3030
+ name: user.name ?? options.name ?? "default",
3031
+ usage: {
3032
+ ...draft.usage,
3033
+ derivedKeys: [...draft.usage.derivedKeys, ...finalPlugins.flatMap((plugin) => plugin.derivedKeys ?? [])]
3034
+ },
3035
+ origins
3036
+ });
3037
+ for (const plugin of finalPlugins) plugin.configResolved?.(config);
3038
+ return {
3039
+ config,
3040
+ registry: createRegistry(finalPlugins, config),
3041
+ presets: expanded.explanations,
3042
+ skipped
3043
+ };
3044
+ }
3045
+ //#endregion
3046
+ export { ConfigSchema, PluginFileSchema, X18nError, applyMessageOps, applyTextEdits, buildTree, contractVersion, createLineIndex, createMemoryHost, createOverlayHost, createPlaceholderComparer, createProject, createRegistry, defineConfig, definePlugin, definePreset, diffText, envelope, errorCodes, extensionOf, globToRegExp, hashText, isJsonObject, isKeyUnder, isMessageLeaf, isMessageTree, isPreset, isX18nError, joinKey, keyMatcher, localeSpellingKey, parseLocaleInfo, parseMessageBatch, pathId, pathMatcher, placeholderProblems, rangeContains, renderPrompt, replaceKeyPrefix, resolveConfig, sortTree, splitKey, toWireDiagnostic, toWireRange, validate, walkLeaves };