@menapo/vi18n 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +110 -2
  3. package/dist/apply-Bre-lY9J.js +228 -0
  4. package/dist/captured-Bj8bO6vd.js +1034 -0
  5. package/dist/check-BIKEuu6B.js +201 -0
  6. package/dist/check-HsmmpJZg.js +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +41 -0
  9. package/dist/codec-DuvU2VQy.js +207 -0
  10. package/dist/collect-Cx4_SKOe.js +13 -0
  11. package/dist/collisions-CfpZU8e4.js +217 -0
  12. package/dist/compile-BaDh0cHG.js +245 -0
  13. package/dist/compile-Cc97JaW2.js +2502 -0
  14. package/dist/concurrency-C9UUWuDC.js +96 -0
  15. package/dist/concurrency-DONL7t4V.js +1 -0
  16. package/dist/config.d.ts +21 -0
  17. package/dist/config.js +39 -0
  18. package/dist/dir-zw6bGMKQ.js +290 -0
  19. package/dist/duplicates-yuRljQ6h.js +185 -0
  20. package/dist/engine-C2ENBRJe.js +531 -0
  21. package/dist/engine.d.ts +43 -0
  22. package/dist/engine.js +1241 -0
  23. package/dist/exclude-Du-Zd_qI.js +26 -0
  24. package/dist/export-C9Sy6UWv.js +103 -0
  25. package/dist/extract-C3S8btx0.js +298 -0
  26. package/dist/formats-9unqj_fZ.js +781 -0
  27. package/dist/i18next-9HAoxwUI.js +1 -0
  28. package/dist/i18next-BEJgv_S8.js +1596 -0
  29. package/dist/import-nANvQutw.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-C2ScuW6a.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui-zUiCl9Um.js +833 -0
  35. package/dist/lint.d.ts +105 -0
  36. package/dist/lint.js +971 -0
  37. package/dist/load-CQRz4PpM.js +2950 -0
  38. package/dist/load-gW-oJt1Q.js +747 -0
  39. package/dist/locales-DX4MOBFE.js +827 -0
  40. package/dist/lsp-RMMMc88P.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-CHVwBoRP.js +28 -0
  43. package/dist/merge-driver-BTX87aVm.js +422 -0
  44. package/dist/messages-BfjIExyB.js +166 -0
  45. package/dist/migrate-IpLDMXmq.js +1484 -0
  46. package/dist/migrate-rCQuOGsf.js +1 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DWE3SIUA.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C9Dbotkh.js +2031 -0
  52. package/dist/pipeline-DbX6rlo5.js +1 -0
  53. package/dist/project-Bw9UPIdx.js +1046 -0
  54. package/dist/project-C4GpaN-n.js +381 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-BP0u-D5m.js +519 -0
  58. package/dist/restore-CiYv-jxS.js +175 -0
  59. package/dist/rewrite-EsIw_lsq.js +803 -0
  60. package/dist/rich-DCUKl5HM.js +97 -0
  61. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  62. package/dist/run-BEcnBbu-.js +694 -0
  63. package/dist/runtime-DTMmliT1.js +64 -0
  64. package/dist/scan-worker.d.ts +62 -0
  65. package/dist/scan-worker.js +1 -0
  66. package/dist/scoped-BMYpSthj.js +44 -0
  67. package/dist/server-Da1Ot9IN.js +10485 -0
  68. package/dist/server-deBOq2vq.js +11823 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DJXzKoza.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-zr4jY59Y.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-B9V2KFwb.js +658 -0
  77. package/dist/t-DC-c9zE5.js +454 -0
  78. package/dist/test.d.ts +116 -0
  79. package/dist/test.js +319 -0
  80. package/dist/translate-CQnJfLjO.js +299 -0
  81. package/dist/ts-BaU5yOf0.js +4863 -0
  82. package/dist/types-CgUCkFU1.d.ts +707 -0
  83. package/dist/types-D3QGM8XO.js +146 -0
  84. package/dist/types-DsdWBG0b.d.ts +558 -0
  85. package/dist/types-DtvQ7UdD.js +1948 -0
  86. package/dist/types-wSh9fCBY.js +11 -0
  87. package/dist/units-CQ5Q99RC.js +150 -0
  88. package/dist/url-CRfCOhUI.js +172 -0
  89. package/dist/url-CsBkRaJy.js +225 -0
  90. package/dist/validate-BxpupdAf.js +2615 -0
  91. package/dist/validate-D6DJepZv.js +365 -0
  92. package/dist/version-Byqi2guX.js +28 -0
  93. package/dist/vite-scan-CT_vsp4g.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-g0iGy6CZ.js +651 -0
  97. package/dist/which-BOemZ5Q8.js +116 -0
  98. package/dist/worker-84U6uYB3.js +1656 -0
  99. package/package.json +97 -3
  100. package/schemas/catalog.json +57 -0
@@ -0,0 +1,658 @@
1
+ import { o as isDiagnosticError, t as CONFIG_FILE } from "./types-D3QGM8XO.js";
2
+ import { s as parseIdScheme } from "./validate-BxpupdAf.js";
3
+ import { S as entryKey, a as ownedMessages, b as emptyCatalog, f as sameCatalogText, n as libraryCoverage, o as catalogLayout, p as serializeCatalog, r as libraryIds, s as catalogPath, y as createEntry } from "./library-D5CWvTQs.js";
4
+ import { t as VERSION } from "./version-Byqi2guX.js";
5
+ import { n as mergeDuplicates } from "./duplicates-yuRljQ6h.js";
6
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
7
+ import { randomBytes } from "node:crypto";
8
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
9
+ //#region src/types-gen/gen.ts
10
+ /**
11
+ * `vi18n.gen.ts` (docs/INTERNALS.md §2.16, PLAN §5.18): the generated
12
+ * `Register` augmentation that turns `Locale` into the configured union.
13
+ * Written beside `vi18n.config.ts` by the plugin and by `vi18n init`/`extract`,
14
+ * and committed like `routeTree.gen.ts`.
15
+ */
16
+ /** The generated file's name, in the directory of `vi18n.config.ts`. */
17
+ const GEN_FILE = "vi18n.gen.ts";
18
+ /** The `locales` union: configured locales in config order, pseudo-locales left out. */
19
+ function localesUnion(config) {
20
+ const pseudo = config.pseudoLocales;
21
+ let union = "";
22
+ for (const locale of config.locales) {
23
+ if (pseudo.includes(locale)) continue;
24
+ union += (union ? " | " : "") + JSON.stringify(locale);
25
+ }
26
+ return union || "never";
27
+ }
28
+ /**
29
+ * `messages`: the manual-id messages of the packages `extraction.dependencies`
30
+ * opts in (their `vi18n.messages.json`, §2.16), by id, so `msg({ id })` is
31
+ * typed from the library's text. Empty without any.
32
+ */
33
+ function messagesMember(config) {
34
+ if (config.extraction.dependencies.length === 0) return "";
35
+ const ids = [...libraryIds(config)].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
36
+ if (ids.length === 0) return "";
37
+ let out = ` messages: {\n`;
38
+ for (const [id, message] of ids) out += ` ${JSON.stringify(id)}: ${JSON.stringify(message)};\n`;
39
+ return out + ` };\n`;
40
+ }
41
+ /**
42
+ * The file's exact content: configured locales in config order, pseudo-locales
43
+ * left out (they are switched on per build and never part of the app's API),
44
+ * then the opted-in packages' manual-id messages, if any.
45
+ */
46
+ function genFileContent(config) {
47
+ return `// Generated by @menapo/vi18n from ${CONFIG_FILE}. Do not edit; commit it.\nimport type {} from "@menapo/vi18n";\n\ndeclare module "@menapo/vi18n" {\n interface Register {\n locales: ${localesUnion(config)};\n` + messagesMember(config) + " }\n}\n";
48
+ }
49
+ /**
50
+ * What earlier releases wrote for the same locales (no import, `export {}`
51
+ * last). The drift checks accept it: like the catalogs' `X-Generator` line
52
+ * (INTERNALS §2.6.3), a vi18n upgrade alone must not fail `extract --check`
53
+ * or make `compile` refuse. Any write replaces it with the current content.
54
+ */
55
+ function previousGenFileContent(config) {
56
+ return `// Generated by @menapo/vi18n from ${CONFIG_FILE}. Do not edit; commit it.\ndeclare module "@menapo/vi18n" {\n interface Register {\n locales: ${localesUnion(config)};\n }\n}\nexport {};\n`;
57
+ }
58
+ /**
59
+ * Whether `text` (the file as read) is up to date for the drift checks: the
60
+ * current content or an earlier release's for the same locales. CRLF line
61
+ * ends (a checkout with `core.autocrlf`) are not drift either.
62
+ */
63
+ function genFileUpToDate(config, text) {
64
+ const normalized = text.replace(/\r\n/g, "\n");
65
+ return normalized === genFileContent(config) || normalized === previousGenFileContent(config);
66
+ }
67
+ /**
68
+ * Writes `vi18n.gen.ts` beside the config when its content changed; true when
69
+ * written. Zero-config mode (no config file) writes nothing (INTERNALS
70
+ * §2.7.1). The plugin's environments and the CLI may write concurrently, so
71
+ * the file is replaced by an atomic rename and never seen half-written by the
72
+ * editor's TypeScript server.
73
+ */
74
+ async function writeGenFile(config) {
75
+ if (config.configFile === null) return false;
76
+ const path = join(dirname(config.configFile), GEN_FILE);
77
+ const content = genFileContent(config);
78
+ let current = null;
79
+ try {
80
+ current = await readFile(path, "utf8");
81
+ } catch (error) {
82
+ if (error.code !== "ENOENT") throw error;
83
+ }
84
+ if (current !== null && current.replace(/\r\n/g, "\n") === content) return false;
85
+ const temp = join(dirname(path), `.${GEN_FILE}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
86
+ try {
87
+ await writeFile(temp, content);
88
+ await rename(temp, path);
89
+ } catch (error) {
90
+ await rm(temp, { force: true });
91
+ throw error;
92
+ }
93
+ return true;
94
+ }
95
+ //#endregion
96
+ //#region src/core/sync.ts
97
+ /**
98
+ * Catalog sync (INTERNALS §2.6.8, PLAN §5.5): what `vi18n extract` (`full`)
99
+ * and the dev writer or `extract --watch` (`add-only`) do to each locale's
100
+ * catalog given the scanned messages. Pure: callers read the files, write
101
+ * the result through `updateCatalogFile` and re-run `syncCatalog` when a file
102
+ * changed under them.
103
+ *
104
+ * Only a one-shot `full` sync clears or deletes, so a branch switch with the
105
+ * dev server running can never destroy translations; `full` is in turn held
106
+ * back by the safety limit unless the user passes `--yes`.
107
+ */
108
+ /** Removals allowed without `--yes` whatever the catalog size (PLAN §5.5). */
109
+ const LIMIT_FLOOR = 50;
110
+ /** Share of a locale's translated entries one extract may clear or delete without `--yes`. */
111
+ const LIMIT_SHARE = .1;
112
+ function syncCatalogs(input) {
113
+ const { config, codec, messages, catalogs, options } = input;
114
+ const sourceCatalog = catalogs.get(config.sourceLocale)?.catalog ?? null;
115
+ const result = {
116
+ catalogs: /* @__PURE__ */ new Map(),
117
+ changed: [],
118
+ stats: {},
119
+ blocked: [],
120
+ deletedKeys: [],
121
+ diagnostics: []
122
+ };
123
+ const deleted = /* @__PURE__ */ new Set();
124
+ const limits = [];
125
+ let overLimit = false;
126
+ const coverage = libraryCoverage(catalogs, messages, config, codec);
127
+ for (const locale of config.locales) {
128
+ const loaded = catalogs.get(locale);
129
+ const read = loaded?.catalog ?? null;
130
+ const synced = syncLocale({
131
+ config,
132
+ codec,
133
+ messages,
134
+ locale,
135
+ catalog: read,
136
+ sourceCatalog,
137
+ options,
138
+ coverage
139
+ });
140
+ const file = displayPath(config.root, loaded?.path ?? catalogPath(config, locale));
141
+ for (const diagnostic of synced.diagnostics) {
142
+ diagnostic.file ??= file;
143
+ diagnostic.locale ??= locale;
144
+ result.diagnostics.push(diagnostic);
145
+ }
146
+ result.catalogs.set(locale, synced.catalog);
147
+ result.stats[locale] = synced.stats;
148
+ if (wouldChange(read, synced.catalog, locale === config.sourceLocale)) result.changed.push(locale);
149
+ for (const key of synced.deleted) deleted.add(key);
150
+ if (options.mode !== "full" || locale === config.sourceLocale) continue;
151
+ const limit = Math.max(LIMIT_FLOOR, Math.ceil(LIMIT_SHARE * translatedCount(read)));
152
+ limits.push({
153
+ locale,
154
+ removals: synced.removals,
155
+ limit
156
+ });
157
+ if (synced.removals > limit && !options.yes) {
158
+ overLimit = true;
159
+ result.diagnostics.push({
160
+ code: "catalog-safety-limit",
161
+ severity: "error",
162
+ message: `vi18n extract would clear or delete ${synced.removals} translated ${synced.removals === 1 ? "entry" : "entries"} in ${locale}, over its limit of ${limit} (10 % of its translated entries, at least ${LIMIT_FLOOR}); check the scan covers every app, then run vi18n extract --yes`,
163
+ file,
164
+ locale
165
+ });
166
+ }
167
+ }
168
+ if (overLimit) result.blocked = limits;
169
+ result.deletedKeys = [...deleted].sort(compareText);
170
+ return result;
171
+ }
172
+ /** One locale; what writers re-run inside `updateCatalogFile` when the file changed under them. */
173
+ function syncCatalog(input) {
174
+ const { catalog, stats, removals, diagnostics } = syncLocale(input);
175
+ return {
176
+ catalog,
177
+ stats,
178
+ removals,
179
+ diagnostics
180
+ };
181
+ }
182
+ function syncLocale(input) {
183
+ const { config, codec, locale } = input;
184
+ const layout = catalogLayout(config);
185
+ const messages = ownedMessages(input.messages, locale, input.catalog, input.coverage, {
186
+ sourceLocale: config.sourceLocale,
187
+ layout,
188
+ codec
189
+ });
190
+ const base = input.catalog ?? emptyCatalog(locale, layout, {
191
+ idScheme: codec.idScheme,
192
+ sourceLocale: config.sourceLocale
193
+ });
194
+ const recorded = base.header.idScheme;
195
+ const rekey = layout !== "source" && recorded !== null && recorded !== codec.idScheme;
196
+ const context = {
197
+ config,
198
+ codec,
199
+ layout,
200
+ isSource: locale === config.sourceLocale,
201
+ messages,
202
+ sourceCatalog: input.sourceCatalog,
203
+ sourceIndex: sourceIndex(input.sourceCatalog, layout, locale === config.sourceLocale),
204
+ icuFlag: config.catalogs.icuFlag && layout !== "json"
205
+ };
206
+ if (input.options.mode === "add-only") {
207
+ if (rekey) return {
208
+ catalog: base,
209
+ stats: emptyStats(),
210
+ removals: 0,
211
+ diagnostics: [schemeDiagnostic(recorded, codec)],
212
+ deleted: []
213
+ };
214
+ return addOnly(base, context);
215
+ }
216
+ return full(base, context, rekey);
217
+ }
218
+ function emptyStats() {
219
+ return {
220
+ added: 0,
221
+ deleted: 0,
222
+ cleared: 0,
223
+ moved: 0,
224
+ rekeyed: 0,
225
+ updated: 0
226
+ };
227
+ }
228
+ function addOnly(catalog, context) {
229
+ const { codec, layout, messages } = context;
230
+ const stats = emptyStats();
231
+ const index = /* @__PURE__ */ new Map();
232
+ const entries = catalog.entries;
233
+ for (let i = 0; i < entries.length; i++) {
234
+ const key = entryKey(entries[i], layout, codec);
235
+ if (key !== null && !index.has(key)) index.set(key, i);
236
+ }
237
+ let out = null;
238
+ for (const message of messages.values()) {
239
+ const at = index.get(message.id);
240
+ if (at === void 0) {
241
+ (out ??= entries.slice()).push(newEntry(message, context));
242
+ stats.added++;
243
+ continue;
244
+ }
245
+ const entry = entries[at];
246
+ if (classify(entry, message, context) !== "same") continue;
247
+ const grown = withMissingNotes(entry, message);
248
+ if (grown !== entry) {
249
+ (out ??= entries.slice())[at] = grown;
250
+ stats.updated++;
251
+ }
252
+ }
253
+ const header = completeHeader(catalog.header, context, false);
254
+ if (out === null && header === catalog.header) return {
255
+ catalog,
256
+ stats,
257
+ removals: 0,
258
+ diagnostics: [],
259
+ deleted: []
260
+ };
261
+ return {
262
+ catalog: {
263
+ ...catalog,
264
+ header,
265
+ entries: out ?? entries
266
+ },
267
+ stats,
268
+ removals: 0,
269
+ diagnostics: [],
270
+ deleted: []
271
+ };
272
+ }
273
+ /** Adds the scan's references and notes an entry lacks, keeping everything it has. */
274
+ function withMissingNotes(entry, message) {
275
+ let refs = null;
276
+ for (const ref of message.refs) if (!entry.refs.includes(ref)) (refs ??= entry.refs.slice()).push(ref);
277
+ let contexts = null;
278
+ for (const note of message.contexts) if (!entry.contexts.includes(note)) (contexts ??= entry.contexts.slice()).push(note);
279
+ if (refs === null && contexts === null) return entry;
280
+ return {
281
+ ...entry,
282
+ refs: refs === null ? entry.refs : refs.sort(compareText),
283
+ contexts: contexts ?? entry.contexts
284
+ };
285
+ }
286
+ function full(read, context, rekey) {
287
+ const { codec, layout, messages, isSource } = context;
288
+ const stats = emptyStats();
289
+ const diagnostics = [];
290
+ let catalog = read;
291
+ if (rekey) {
292
+ const rekeyed = rekeyCatalog(catalog, context);
293
+ catalog = rekeyed.catalog;
294
+ stats.rekeyed = rekeyed.count;
295
+ }
296
+ catalog = mergeDuplicates(catalog, codec);
297
+ const keep = new Set(context.config.extraction.keep);
298
+ const out = [];
299
+ const matched = /* @__PURE__ */ new Set();
300
+ const deletions = [];
301
+ let removals = 0;
302
+ for (const entry of catalog.entries) {
303
+ const key = entryKey(entry, layout, codec);
304
+ if (key === null) {
305
+ out.push(entry);
306
+ continue;
307
+ }
308
+ const message = messages.get(key);
309
+ if (message === void 0) {
310
+ if (keep.has(key)) out.push(entry);
311
+ else deletions.push({
312
+ entry,
313
+ key,
314
+ canonical: canonicalOf(sourceText(entry, context), codec),
315
+ movedTo: false
316
+ });
317
+ continue;
318
+ }
319
+ const kind = classify(entry, message, context);
320
+ if (kind === "other") {
321
+ deletions.push({
322
+ entry,
323
+ key,
324
+ canonical: canonicalOf(sourceText(entry, context), codec),
325
+ movedTo: false
326
+ });
327
+ continue;
328
+ }
329
+ matched.add(key);
330
+ if (kind === "changed") {
331
+ if (!isSource && hasTranslation(entry)) {
332
+ stats.cleared++;
333
+ removals++;
334
+ }
335
+ out.push(rewritten(entry, message, layout));
336
+ continue;
337
+ }
338
+ const refreshed = refresh(entry, message, layout);
339
+ if (refreshed.notesChanged) stats.updated++;
340
+ out.push(refreshed.entry);
341
+ }
342
+ const additions = [];
343
+ const addedTexts = /* @__PURE__ */ new Map();
344
+ for (const message of messages.values()) {
345
+ if (matched.has(message.id)) continue;
346
+ matched.add(message.id);
347
+ additions.push(newEntry(message, context));
348
+ addedTexts.set(message.canonical, (addedTexts.get(message.canonical) ?? 0) + 1);
349
+ }
350
+ let deletedTexts = null;
351
+ for (const deletion of deletions) {
352
+ if (deletion.canonical === null) continue;
353
+ deletedTexts ??= /* @__PURE__ */ new Map();
354
+ const list = deletedTexts.get(deletion.canonical);
355
+ if (list === void 0) deletedTexts.set(deletion.canonical, [deletion]);
356
+ else list.push(deletion);
357
+ }
358
+ for (let i = 0; i < additions.length; i++) {
359
+ const added = additions[i];
360
+ const canonical = added.source;
361
+ const from = deletedTexts?.get(canonical);
362
+ if (from === void 0 || from.length !== 1 || addedTexts.get(canonical) !== 1) {
363
+ stats.added++;
364
+ continue;
365
+ }
366
+ const source = from[0];
367
+ source.movedTo = true;
368
+ stats.moved++;
369
+ additions[i] = {
370
+ ...added,
371
+ translation: source.entry.translation,
372
+ state: source.entry.state,
373
+ fuzzy: source.entry.fuzzy
374
+ };
375
+ }
376
+ for (const deletion of deletions) {
377
+ if (deletion.movedTo) continue;
378
+ stats.deleted++;
379
+ if (!isSource && hasTranslation(deletion.entry)) removals++;
380
+ }
381
+ for (const added of additions) out.push(added);
382
+ for (let i = 0; i < out.length; i++) out[i] = withIcuFlag(out[i], context.icuFlag);
383
+ let obsolete = catalog.obsolete;
384
+ let droppedObsolete = catalog.droppedObsolete;
385
+ if (obsolete.length > 0) {
386
+ diagnostics.push({
387
+ code: "catalog-obsolete-dropped",
388
+ severity: "info",
389
+ message: `dropped ${obsolete.length} obsolete #~ ${obsolete.length === 1 ? "entry" : "entries"}; git history keeps them`
390
+ });
391
+ obsolete = [];
392
+ droppedObsolete = 0;
393
+ }
394
+ const header = completeHeader(catalog.header, context, true);
395
+ return {
396
+ catalog: header === read.header && obsolete === read.obsolete && out.length === read.entries.length && out.every((entry, i) => entry === read.entries[i]) ? read : {
397
+ ...catalog,
398
+ header,
399
+ entries: out,
400
+ obsolete,
401
+ droppedObsolete
402
+ },
403
+ stats,
404
+ removals,
405
+ diagnostics,
406
+ deleted: deletions.map((deletion) => deletion.key)
407
+ };
408
+ }
409
+ /** The matched entry with refs, notes, `maxLength` and the canonical source taken from the scan. */
410
+ function refresh(entry, message, layout) {
411
+ const maxLength = message.maxLength ?? null;
412
+ const notesChanged = entry.maxLength !== maxLength || !sameList(entry.refs, message.refs) || !sameList(entry.contexts, message.contexts);
413
+ const label = layout === "id" ? message.label : entry.label;
414
+ if (!notesChanged && entry.source === message.canonical && entry.label === label) return {
415
+ entry,
416
+ notesChanged
417
+ };
418
+ return {
419
+ entry: {
420
+ ...entry,
421
+ source: message.canonical,
422
+ label,
423
+ refs: message.refs.slice(),
424
+ contexts: message.contexts.slice(),
425
+ maxLength
426
+ },
427
+ notesChanged
428
+ };
429
+ }
430
+ /** A manual entry whose text changed: the new source, an empty translation, no `ai` or `fuzzy`. */
431
+ function rewritten(entry, message, layout) {
432
+ return {
433
+ ...entry,
434
+ source: message.canonical,
435
+ label: layout === "id" ? message.label : entry.label,
436
+ translation: "",
437
+ state: "untranslated",
438
+ fuzzy: false,
439
+ refs: message.refs.slice(),
440
+ contexts: message.contexts.slice(),
441
+ maxLength: message.maxLength ?? null
442
+ };
443
+ }
444
+ function newEntry(message, context) {
445
+ const { layout } = context;
446
+ return createEntry({
447
+ manualId: message.manual ? message.id : null,
448
+ label: layout === "id" ? message.label : layout === "json" && !message.manual ? message.id : null,
449
+ source: message.canonical,
450
+ refs: message.refs.slice(),
451
+ contexts: message.contexts.slice(),
452
+ maxLength: message.maxLength ?? null,
453
+ flags: context.icuFlag ? [{
454
+ name: ICU_FLAG,
455
+ line: "#,"
456
+ }] : []
457
+ });
458
+ }
459
+ const ICU_FLAG = "icu-message-format";
460
+ /**
461
+ * `catalogs.icuFlag` is vi18n's to set (PLAN §4.4): a full sync adds the
462
+ * flag to every entry, or takes it off when the option is false, so the
463
+ * file follows the config. A flag already present stays where it was read.
464
+ */
465
+ function withIcuFlag(entry, on) {
466
+ if (entry.foreignContext !== null) return entry;
467
+ if (entry.flags.some((flag) => flag.name === ICU_FLAG) === on) return entry;
468
+ return {
469
+ ...entry,
470
+ flags: on ? [{
471
+ name: ICU_FLAG,
472
+ line: "#,"
473
+ }, ...entry.flags] : entry.flags.filter((flag) => flag.name !== ICU_FLAG)
474
+ };
475
+ }
476
+ /**
477
+ * Re-key after an `ids.length` change (PLAN §5.5): each entry's text comes
478
+ * from its own source (JSON, the source locale) or, for `"id"`-layout
479
+ * targets, from the source locale's entry for the same old key (read at the
480
+ * recorded length), else the target's own `#. source:` note, and its
481
+ * label is recomputed at the new length. Translations stay, and nothing here
482
+ * counts against the safety limit. An entry whose text cannot be recovered
483
+ * keeps its old key, so the normal rules (and the limit) apply to it.
484
+ */
485
+ function rekeyCatalog(catalog, context) {
486
+ const { codec, layout, isSource } = context;
487
+ const joined = layout === "id" && !isSource ? oldLabelIndex(context.sourceCatalog, catalog.header.idScheme, codec) : null;
488
+ let count = 0;
489
+ const entries = catalog.entries.map((entry) => {
490
+ if (entry.foreignContext !== null) return entry;
491
+ if (layout === "json" && entry.manualId !== null) return entry;
492
+ let text = entry.source;
493
+ if (layout === "id" && !isSource) {
494
+ const key = entry.manualId ?? entry.label ?? "";
495
+ text = joined?.get(key) ?? context.sourceIndex?.get(key) ?? entry.source;
496
+ }
497
+ const canonical = canonicalOf(text, codec);
498
+ if (canonical === null) return entry;
499
+ const label = codec.label(canonical);
500
+ if (label !== entry.label) count++;
501
+ return {
502
+ ...entry,
503
+ label,
504
+ source: canonical
505
+ };
506
+ });
507
+ return {
508
+ catalog: {
509
+ ...catalog,
510
+ entries,
511
+ header: {
512
+ ...catalog.header,
513
+ idScheme: codec.idScheme
514
+ }
515
+ },
516
+ count
517
+ };
518
+ }
519
+ /**
520
+ * The source locale's text per key at the target's recorded length: manual
521
+ * ids as they are, generated entries by their text's hash cut to the old
522
+ * length. Hashing rather than reading the source file's labels keeps the
523
+ * join working when the source locale is already at the new scheme (an
524
+ * interrupted extract, a TMS export from before the change merged back),
525
+ * where the target's `#. source:` notes may be all that is left otherwise and
526
+ * a key-based TMS strips them.
527
+ */
528
+ function oldLabelIndex(sourceCatalog, recorded, codec) {
529
+ const old = recorded === null ? null : parseIdScheme(recorded);
530
+ if (old === null || sourceCatalog === null) return null;
531
+ const index = /* @__PURE__ */ new Map();
532
+ for (const entry of sourceCatalog.entries) {
533
+ if (entry.foreignContext !== null || entry.source === null) continue;
534
+ let key = entry.manualId;
535
+ if (key === null) {
536
+ const canonical = canonicalOf(entry.source, codec);
537
+ if (canonical === null) continue;
538
+ key = codec.sourceHash(canonical).slice(0, old.length);
539
+ }
540
+ if (!index.has(key)) index.set(key, entry.source);
541
+ }
542
+ return index;
543
+ }
544
+ /**
545
+ * How an entry found under a message's key relates to it:
546
+ * - `same`: the entry is for the message's text;
547
+ * - `changed`: a manual entry whose source text changed (D10);
548
+ * - `other`: a generated entry whose recorded text is a different text with
549
+ * the same id, i.e. a deleted message whose id the new one took.
550
+ *
551
+ * A generated entry's key is a fingerprint of its text, so a recorded source
552
+ * that no longer hashes to the key (a hand-edited JSON `source`, a stale
553
+ * `#. source:` note) is a damaged copy of the same text, not another text.
554
+ */
555
+ function classify(entry, message, context) {
556
+ const { codec, layout } = context;
557
+ if (entry.manualId !== null) {
558
+ if (layout === "id") return entry.label === message.label ? "same" : "changed";
559
+ if (entry.source === message.canonical) return "same";
560
+ return canonicalOf(entry.source, codec) === message.canonical ? "same" : "changed";
561
+ }
562
+ const text = sourceText(entry, context);
563
+ if (text === null || text === message.canonical) return "same";
564
+ const canonical = canonicalOf(text, codec);
565
+ if (canonical === null || canonical === message.canonical) return "same";
566
+ return codec.label(canonical) === message.id ? "other" : "same";
567
+ }
568
+ /**
569
+ * An entry's source text. `"id"`-layout targets only carry a translator
570
+ * note, so the source locale's text for the same key wins when there is one.
571
+ */
572
+ function sourceText(entry, context) {
573
+ if (context.sourceIndex !== null) {
574
+ const key = entry.manualId ?? entry.label;
575
+ const text = key === null ? void 0 : context.sourceIndex.get(key);
576
+ if (text !== void 0) return text;
577
+ }
578
+ return entry.source;
579
+ }
580
+ function sourceIndex(sourceCatalog, layout, isSource) {
581
+ if (layout !== "id" || isSource || sourceCatalog === null) return null;
582
+ const index = /* @__PURE__ */ new Map();
583
+ for (const entry of sourceCatalog.entries) {
584
+ if (entry.foreignContext !== null || entry.source === null) continue;
585
+ const key = entry.manualId ?? entry.label;
586
+ if (key !== null && !index.has(key)) index.set(key, entry.source);
587
+ }
588
+ return index;
589
+ }
590
+ /** The header with the scheme (and JSON source locale) a writer records; the same object when nothing changes. */
591
+ function completeHeader(header, context, full) {
592
+ const { codec, layout, config } = context;
593
+ const idScheme = full || header.idScheme === null ? codec.idScheme : header.idScheme;
594
+ const sourceLocale = layout === "json" && (full || header.sourceLocale === null) ? config.sourceLocale : header.sourceLocale;
595
+ if (idScheme === header.idScheme && sourceLocale === header.sourceLocale) return header;
596
+ return {
597
+ ...header,
598
+ idScheme,
599
+ sourceLocale
600
+ };
601
+ }
602
+ function schemeDiagnostic(recorded, codec) {
603
+ const old = parseIdScheme(recorded);
604
+ return {
605
+ code: "id-scheme-mismatch",
606
+ severity: "error",
607
+ message: `${old !== null ? `ids.length changed from ${old.length} to ${codec.idLength}` : `the catalog records id scheme ${JSON.stringify(recorded)}, not ${codec.idScheme}`}: run vi18n extract to re-key (until then this catalog is left untouched)`
608
+ };
609
+ }
610
+ function canonicalOf(source, codec) {
611
+ if (source === null) return null;
612
+ try {
613
+ return codec.canonicalize(source);
614
+ } catch (error) {
615
+ if (isDiagnosticError(error)) return null;
616
+ throw error;
617
+ }
618
+ }
619
+ /** Translated or `ai`: what the safety limit protects. */
620
+ function hasTranslation(entry) {
621
+ return entry.translation !== "" && entry.state !== "untranslated";
622
+ }
623
+ function translatedCount(catalog) {
624
+ if (catalog === null) return 0;
625
+ let count = 0;
626
+ for (const entry of catalog.entries) if (entry.foreignContext === null && hasTranslation(entry)) count++;
627
+ return count;
628
+ }
629
+ /**
630
+ * Whether writing `next` would change the file `read` came from. Serialising
631
+ * compares content, not object identity, and the writer unwraps wrapped
632
+ * strings, so a wrapped file changes even when its content does not.
633
+ */
634
+ function wouldChange(read, next, isSourceLocale) {
635
+ if (read === null) return true;
636
+ if (read.entries.some((entry) => entry.wrapped)) return true;
637
+ if (next === read) return false;
638
+ const options = {
639
+ isSourceLocale,
640
+ version: VERSION
641
+ };
642
+ return !sameCatalogText(serializeCatalog(read, options), serializeCatalog(next, options));
643
+ }
644
+ function sameList(a, b) {
645
+ if (a.length !== b.length) return false;
646
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
647
+ return true;
648
+ }
649
+ function compareText(a, b) {
650
+ return a < b ? -1 : a > b ? 1 : 0;
651
+ }
652
+ function displayPath(root, file) {
653
+ const rel = relative(root, file);
654
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return file.split(sep).join("/");
655
+ return rel.split(sep).join("/");
656
+ }
657
+ //#endregion
658
+ export { writeGenFile as a, genFileUpToDate as i, syncCatalogs as n, GEN_FILE as r, syncCatalog as t };