@menapo/vi18n 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-D34P-S--.js +1 -0
  6. package/dist/check-DyrAbv73.js +201 -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-B5Mz6WJm.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-B7p40M__.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-DGsMbcTZ.js +103 -0
  25. package/dist/extract-Dujlsgnk.js +298 -0
  26. package/dist/formats-KPIg-08V.js +781 -0
  27. package/dist/i18next-CrUnKFfh.js +1596 -0
  28. package/dist/i18next-Rgo-v9l0.js +1 -0
  29. package/dist/import-Cwdhcywt.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-BecCTvKi.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui--PcOOYry.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-Cq1Nuu-Q.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-Cf0T7qPW.js +28 -0
  43. package/dist/merge-driver-CpiiSVIc.js +422 -0
  44. package/dist/messages-Cxa-5tR0.js +166 -0
  45. package/dist/migrate-CntqvrW4.js +1 -0
  46. package/dist/migrate-dPgq9uzy.js +1484 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DsaHKHEt.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C7g11THL.js +2031 -0
  52. package/dist/pipeline-wxS0SApG.js +1 -0
  53. package/dist/project-Dwx7iPm6.js +381 -0
  54. package/dist/project-pXipT8iG.js +1046 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-CaBVwJ9u.js +519 -0
  58. package/dist/restore-Btq66Tid.js +175 -0
  59. package/dist/rewrite-CiTRAICl.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-DYD_VXKy.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-BDkykMjb.js +11823 -0
  68. package/dist/server-qv3mz70d.js +10485 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DV3o8QD7.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-DZArW5aE.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-CcVzger_.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-DZR3MkSA.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-BlyLGlOa.js +28 -0
  93. package/dist/vite-scan-B_YW4X_a.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-CEMnpv7k.js +651 -0
  97. package/dist/which-C6PFI1Dm.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 @@
1
+ export { t as check } from "./check-DyrAbv73.js";
@@ -0,0 +1,201 @@
1
+ import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
2
+ import { r as validateTranslation } from "./validate-BxpupdAf.js";
3
+ import { s as catalogPath } from "./library-D5CWvTQs.js";
4
+ import { r as resolveDuplicates, t as duplicateDiagnostics } from "./duplicates-yuRljQ6h.js";
5
+ import { t as findCollisions } from "./collisions-CfpZU8e4.js";
6
+ import { i as rootRelative, r as plural, t as countBySeverity } from "./output-DsaHKHEt.js";
7
+ import { t as EXIT } from "./types-wSh9fCBY.js";
8
+ import { a as loadCatalogs, c as ownStatus, d as referencedMessages, f as scan, o as loadProject, s as occurrencesOf } from "./project-Dwx7iPm6.js";
9
+ //#region src/cli/commands/check.ts
10
+ /**
11
+ * `vi18n check [missing] [placeholders] [syntax] [--ci] [--format …]`
12
+ * (INTERNALS §2.14, PLAN §4.14): validation of the catalogs against the
13
+ * code, with GitHub annotations at the catalog line. Exit 1 on errors, and
14
+ * with `--ci` on warnings of the selected categories too.
15
+ *
16
+ * - syntax: catalogs that do not parse, invalid sources and translations,
17
+ * wrapped lines, duplicates, foreign contexts, collisions, id-scheme drift,
18
+ * and the code's own extraction errors;
19
+ * - placeholders: everything `validateTranslation` finds in a translation
20
+ * (argument and tag mismatches, plural categories, length, apostrophes);
21
+ * - missing: referenced messages without a usable own translation per target
22
+ * locale, fuzzy and invalid ones included.
23
+ */
24
+ const ALL = [
25
+ "missing",
26
+ "placeholders",
27
+ "syntax"
28
+ ];
29
+ /** Missing ids named in each locale's summary; the count says how many more there are. */
30
+ const MISSING_LISTED = 20;
31
+ /** Scan findings that belong to `check syntax`: invalid source messages, and anything the build would fail on. */
32
+ function isSyntaxFinding(diagnostic) {
33
+ return diagnostic.severity === "error" || diagnostic.code.startsWith("icu-");
34
+ }
35
+ async function check(ctx, flags) {
36
+ const categories = flags.categories.length === 0 ? ALL.slice() : flags.categories;
37
+ const project = await loadProject(ctx);
38
+ const { config } = project;
39
+ const found = {
40
+ missing: [],
41
+ placeholders: [],
42
+ syntax: []
43
+ };
44
+ const scanned = await scan(project);
45
+ const loaded = await loadCatalogs(project);
46
+ const referenced = referencedMessages(project, scanned.messages, loaded.set);
47
+ if (categories.includes("syntax")) {
48
+ found.syntax.push(...scanned.diagnostics.filter(isSyntaxFinding));
49
+ found.syntax.push(...loaded.diagnostics);
50
+ found.syntax.push(...referenced.diagnostics);
51
+ found.syntax.push(...findCollisions(occurrencesOf(scanned), loaded.set, project.codec, { referenced: new Set(config.extraction.keep) }));
52
+ }
53
+ for (const locale of config.locales) {
54
+ const catalog = loaded.set.get(locale);
55
+ const file = rootRelative(config.root, catalog?.path ?? catalogPath(config, locale));
56
+ const read = catalog?.catalog ?? null;
57
+ if (categories.includes("syntax") && read !== null) {
58
+ for (const entry of read.entries) syntaxOfEntry(found.syntax, entry, file, locale);
59
+ found.syntax.push(...duplicateDiagnostics(resolveDuplicates(read, project.codec).duplicates, file));
60
+ if (read.header.idScheme !== null && read.header.idScheme !== project.codec.idScheme) found.syntax.push({
61
+ code: "id-scheme-mismatch",
62
+ severity: "error",
63
+ message: `${file} records ${read.header.idScheme}, but ids.length ${config.ids.length} gives ${project.codec.idScheme}; run vi18n extract to re-key it`,
64
+ file,
65
+ locale
66
+ });
67
+ }
68
+ if (locale === config.sourceLocale) continue;
69
+ const status = ownStatus(project, locale, catalog, referenced.identities);
70
+ const findings = translationFindings(project, status, referenced.identities, file);
71
+ if (categories.includes("syntax")) found.syntax.push(...findings.syntax);
72
+ if (categories.includes("placeholders")) found.placeholders.push(...findings.placeholders);
73
+ if (categories.includes("missing")) found.missing.push(...missing(project, status, file));
74
+ }
75
+ const data = {
76
+ categories,
77
+ counts: {}
78
+ };
79
+ let failed = false;
80
+ for (const category of categories) {
81
+ const list = found[category];
82
+ ctx.out.report(list);
83
+ const counts = countBySeverity(list);
84
+ data.counts[category] = counts;
85
+ if (counts.error > 0 || flags.ci && counts.warning > 0) failed = true;
86
+ }
87
+ ctx.out.result(categories.map((category) => {
88
+ const counts = data.counts[category];
89
+ return `${category}: ${plural(counts.error, "error")}, ${plural(counts.warning, "warning")}`;
90
+ }).join("; "));
91
+ return {
92
+ exitCode: failed ? EXIT.failure : EXIT.ok,
93
+ data
94
+ };
95
+ }
96
+ /** Wrapped lines and foreign contexts; the language server reports them on open catalogs too. */
97
+ function syntaxOfEntry(into, entry, file, locale) {
98
+ if (entry.wrapped) into.push({
99
+ code: "catalog-wrapped",
100
+ severity: "warning",
101
+ message: "a string is wrapped over several lines; vi18n extract rewrites it on one line",
102
+ file,
103
+ line: entry.line,
104
+ locale
105
+ });
106
+ if (entry.foreignContext !== null) into.push({
107
+ code: "catalog-foreign-context",
108
+ severity: "warning",
109
+ message: `msgctxt "${entry.foreignContext}" is not a manual id (no "."), so no message matches this entry; give it a manual id with a "." or remove it`,
110
+ file,
111
+ line: entry.line,
112
+ locale
113
+ });
114
+ }
115
+ /**
116
+ * Every referenced entry with a translation whose source still matches:
117
+ * a translation that does not parse is a syntax finding; otherwise every
118
+ * finding of `validateTranslation` is a placeholders finding
119
+ * (`usableTranslation` keeps only errors, `check` also reports the warnings:
120
+ * plural categories, `maxLength`, apostrophes). Located at the entry's line,
121
+ * since positions inside the text mean nothing in a catalog.
122
+ */
123
+ function translationFindings(project, status, identities, file) {
124
+ const { codec } = project;
125
+ const out = {
126
+ syntax: [],
127
+ placeholders: []
128
+ };
129
+ const at = (diagnostic, id, entry) => {
130
+ const located = {
131
+ ...diagnostic,
132
+ file,
133
+ locale: status.locale,
134
+ id
135
+ };
136
+ if (entry.line > 0) located.line = entry.line;
137
+ else delete located.line;
138
+ delete located.column;
139
+ return located;
140
+ };
141
+ for (const [id, entry] of status.entries) {
142
+ if (entry.translation === "") continue;
143
+ const reason = status.reasons.get(id);
144
+ if (reason === "source-mismatch" || reason === "scheme-mismatch") continue;
145
+ const identity = identities.get(id);
146
+ let parsed;
147
+ try {
148
+ parsed = codec.parse(entry.translation);
149
+ } catch (error) {
150
+ if (!isDiagnosticError(error)) throw error;
151
+ out.syntax.push(at(error.diagnostic, id, entry));
152
+ continue;
153
+ }
154
+ const options = {
155
+ locale: status.locale,
156
+ text: entry.translation
157
+ };
158
+ if (entry.maxLength !== null) options.maxLength = entry.maxLength;
159
+ for (const diagnostic of validateTranslation(parsed, codec.parse(identity.canonical), options)) out.placeholders.push(at(diagnostic, id, entry));
160
+ }
161
+ return out;
162
+ }
163
+ function missing(project, status, file) {
164
+ const level = project.config.strict.missing;
165
+ if (level === "off" || status.missing === 0) return [];
166
+ const severity = level === "error" ? "error" : "warning";
167
+ const out = [];
168
+ const ids = [];
169
+ let fuzzy = 0;
170
+ let invalid = 0;
171
+ for (const [id, reason] of status.reasons) {
172
+ if (ids.length < MISSING_LISTED) ids.push(id);
173
+ if (reason === "fuzzy") {
174
+ fuzzy++;
175
+ const entry = status.entries.get(id);
176
+ const diagnostic = {
177
+ code: "catalog-fuzzy",
178
+ severity,
179
+ message: `${id} is marked fuzzy, so its translation is not used; review it and remove the fuzzy flag`,
180
+ file,
181
+ locale: status.locale,
182
+ id
183
+ };
184
+ if (entry.line > 0) diagnostic.line = entry.line;
185
+ out.push(diagnostic);
186
+ } else if (reason === "invalid") invalid++;
187
+ }
188
+ const extra = [];
189
+ if (fuzzy > 0) extra.push(`${fuzzy} fuzzy`);
190
+ if (invalid > 0) extra.push(`${invalid} invalid`);
191
+ out.unshift({
192
+ code: "catalog-missing-translation",
193
+ severity,
194
+ message: `${status.locale} lacks a usable translation for ${status.missing} of ${status.total} messages${extra.length > 0 ? ` (${extra.join(", ")})` : ""}: ${ids.join(", ")}${status.missing > ids.length ? ", …" : ""}; run vi18n translate or edit ${file}`,
195
+ file,
196
+ locale: status.locale
197
+ });
198
+ return out;
199
+ }
200
+ //#endregion
201
+ export { syntaxOfEntry as n, check as t };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/cli.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { t as run } from "./run-DYD_VXKy.js";
3
+ import { createInterface } from "node:readline/promises";
4
+ //#region src/cli/bin.ts
5
+ /**
6
+ * The `vi18n` binary (docs/INTERNALS.md §3.5). In the workspace this `.ts`
7
+ * file runs with Node's type stripping; published, `dist/cli.js`. Everything
8
+ * but the process wiring lives in `run.ts`, so tests drive the commands
9
+ * in-process.
10
+ */
11
+ const controller = new AbortController();
12
+ process.once("SIGINT", () => {
13
+ controller.abort();
14
+ process.once("SIGINT", () => process.exit(130));
15
+ });
16
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
17
+ process.exitCode = await run(process.argv.slice(2), {
18
+ cwd: process.cwd(),
19
+ env: process.env,
20
+ stdout: (text) => process.stdout.write(text),
21
+ stderr: (text) => process.stderr.write(text),
22
+ signal: controller.signal,
23
+ streams: {
24
+ input: process.stdin,
25
+ output: process.stdout
26
+ },
27
+ ...interactive ? { async prompt(question, fallback) {
28
+ const rl = createInterface({
29
+ input: process.stdin,
30
+ output: process.stderr
31
+ });
32
+ try {
33
+ const answer = await rl.question(`${question} (${fallback}): `);
34
+ return answer.trim() === "" ? fallback : answer.trim();
35
+ } finally {
36
+ rl.close();
37
+ }
38
+ } } : {}
39
+ });
40
+ //#endregion
41
+ export {};
@@ -0,0 +1,207 @@
1
+ import { a as diagnosticError, o as isDiagnosticError } from "./types-D3QGM8XO.js";
2
+ import { c as sourceHash, d as parseSource, i as idScheme, l as isPlainText, u as normalizeSource } from "./validate-BxpupdAf.js";
3
+ //#region src/core/icu/print.ts
4
+ const HASH = 35;
5
+ const APOS = 39;
6
+ const SLASH = 47;
7
+ const LT = 60;
8
+ const GT = 62;
9
+ const LBRACE = 123;
10
+ const RBRACE = 125;
11
+ /** "Nothing follows": the end of the whole message. */
12
+ const END = -1;
13
+ /** Prints a message in canonical form. Literal text keeps every character. */
14
+ function printICU(message) {
15
+ return printNodes(message, false, END);
16
+ }
17
+ /**
18
+ * @param poundSensitive inside any plural/selectordinal option, where `#` must be quoted
19
+ * @param after first character printed after these nodes: `}` (option), `<` (closing tag) or END
20
+ */
21
+ function printNodes(nodes, poundSensitive, after) {
22
+ let out = "";
23
+ const count = nodes.length;
24
+ for (let i = 0; i < count; i++) {
25
+ const node = nodes[i];
26
+ if (node.type === "literal") {
27
+ let value = node.value;
28
+ while (i + 1 < count && nodes[i + 1].type === "literal") value += nodes[++i].value;
29
+ const next = i + 1 < count ? firstChar(nodes[i + 1]) : after;
30
+ out += printLiteral(value, poundSensitive, next);
31
+ continue;
32
+ }
33
+ out += printNode(node, poundSensitive);
34
+ }
35
+ return out;
36
+ }
37
+ function printNode(node, poundSensitive) {
38
+ switch (node.type) {
39
+ case "literal": return printLiteral(node.value, poundSensitive, END);
40
+ case "argument": return `{${node.name}}`;
41
+ case "number":
42
+ case "date":
43
+ case "time": {
44
+ const style = node.style;
45
+ if (style === null) return `{${node.name}, ${node.type}}`;
46
+ return `{${node.name}, ${node.type}, ${style.kind === "skeleton" ? "::" + style.value : style.value}}`;
47
+ }
48
+ case "select": return `{${node.name}, select, ${printOptions(node.options, poundSensitive)}}`;
49
+ case "plural": {
50
+ const type = node.pluralType === "cardinal" ? "plural" : "selectordinal";
51
+ const offset = node.offset === 0 ? "" : `offset:${node.offset} `;
52
+ return `{${node.name}, ${type}, ${offset}${printOptions(node.options, true)}}`;
53
+ }
54
+ case "pound": return "#";
55
+ case "tag": return node.children === null ? `<${node.name}/>` : `<${node.name}>${printNodes(node.children, poundSensitive, LT)}</${node.name}>`;
56
+ }
57
+ }
58
+ function printOptions(options, poundSensitive) {
59
+ let out = "";
60
+ for (let i = 0; i < options.length; i++) {
61
+ const option = options[i];
62
+ if (i > 0) out += " ";
63
+ out += `${option.key} {${printNodes(option.value, poundSensitive, RBRACE)}}`;
64
+ }
65
+ return out;
66
+ }
67
+ function firstChar(node) {
68
+ switch (node.type) {
69
+ case "tag": return LT;
70
+ case "pound": return HASH;
71
+ case "literal": return node.value.charCodeAt(0);
72
+ default: return LBRACE;
73
+ }
74
+ }
75
+ /**
76
+ * Minimal, deterministic quoting: a run starting at a special character
77
+ * (`{`, `}`, `#` where pound-sensitive, `<` before a letter or `/`) is wrapped
78
+ * in apostrophes together with any special characters and apostrophes that
79
+ * follow it; any other apostrophe is doubled only where a single one would
80
+ * open a quote.
81
+ */
82
+ function printLiteral(value, poundSensitive, after) {
83
+ if (!needsQuoting(value, poundSensitive)) return value;
84
+ const length = value.length;
85
+ let out = "";
86
+ let run = 0;
87
+ let i = 0;
88
+ while (i < length) {
89
+ const c = value.charCodeAt(i);
90
+ if (isSpecial(value, i, c, poundSensitive)) {
91
+ let j = i + 1;
92
+ while (j < length) {
93
+ const d = value.charCodeAt(j);
94
+ if (d !== APOS && !isSpecial(value, j, d, poundSensitive)) break;
95
+ j++;
96
+ }
97
+ out += value.slice(run, i) + "'" + value.slice(i, j).replaceAll("'", "''") + "'";
98
+ i = j;
99
+ run = j;
100
+ continue;
101
+ }
102
+ if (c === APOS) {
103
+ const next = i + 1 < length ? value.charCodeAt(i + 1) : after;
104
+ if (next === APOS || next === LBRACE || next === RBRACE || next === LT || next === GT || next === HASH && poundSensitive) {
105
+ out += value.slice(run, i + 1) + "'";
106
+ run = i + 1;
107
+ }
108
+ }
109
+ i++;
110
+ }
111
+ return out + value.slice(run);
112
+ }
113
+ function needsQuoting(value, poundSensitive) {
114
+ for (let i = 0; i < value.length; i++) {
115
+ const c = value.charCodeAt(i);
116
+ if (c === APOS || c === LBRACE || c === RBRACE || c === LT || c === HASH && poundSensitive) return true;
117
+ }
118
+ return false;
119
+ }
120
+ function isSpecial(value, i, c, poundSensitive) {
121
+ if (c === LBRACE || c === RBRACE) return true;
122
+ if (c === HASH) return poundSensitive;
123
+ if (c === LT) {
124
+ const next = value.charCodeAt(i + 1);
125
+ return next === SLASH || next >= 65 && next <= 90 || next >= 97 && next <= 122;
126
+ }
127
+ return false;
128
+ }
129
+ //#endregion
130
+ //#region src/core/codec.ts
131
+ /**
132
+ * The one place that turns source text into ids (INTERNALS §2.2). Extract,
133
+ * catalogs and compilation see the same message many times per run, so parses,
134
+ * canonical forms and hashes are memoised per string.
135
+ */
136
+ /**
137
+ * Memo entries past this count are dropped all at once: a long dev session
138
+ * sees every edit of every message, while one scan of a large project needs
139
+ * the whole set warm.
140
+ */
141
+ const MEMO_LIMIT = 2e5;
142
+ function createCodec(idLength) {
143
+ if (!Number.isInteger(idLength) || idLength < 8 || idLength > 16) throw diagnosticError({
144
+ code: "config-invalid",
145
+ severity: "error",
146
+ message: `ids.length must be an integer from 8 to 16, not ${String(idLength)}`
147
+ });
148
+ const parsed = /* @__PURE__ */ new Map();
149
+ const hashes = /* @__PURE__ */ new Map();
150
+ const entry = (source) => {
151
+ let found = parsed.get(source);
152
+ if (found === void 0) {
153
+ found = {
154
+ ast: null,
155
+ error: null,
156
+ canonical: null
157
+ };
158
+ try {
159
+ const normalized = normalizeSource(source);
160
+ found.ast = isPlainText(normalized) ? normalized === "" ? [] : [{
161
+ type: "literal",
162
+ value: normalized
163
+ }] : parseSource(normalized);
164
+ } catch (error) {
165
+ if (!isDiagnosticError(error)) throw error;
166
+ found.error = error;
167
+ }
168
+ if (parsed.size >= MEMO_LIMIT) parsed.clear();
169
+ parsed.set(source, found);
170
+ }
171
+ return found;
172
+ };
173
+ const hashOf = (canonical) => {
174
+ let found = hashes.get(canonical);
175
+ if (found === void 0) {
176
+ found = sourceHash(canonical);
177
+ if (hashes.size >= MEMO_LIMIT) hashes.clear();
178
+ hashes.set(canonical, found);
179
+ }
180
+ return found;
181
+ };
182
+ return {
183
+ idLength,
184
+ idScheme: idScheme(idLength),
185
+ canonicalize(source) {
186
+ const found = parsed.get(source);
187
+ if (found === void 0) {
188
+ const normalized = normalizeSource(source);
189
+ if (isPlainText(normalized)) return normalized;
190
+ }
191
+ const memo = found ?? entry(source);
192
+ if (memo.error !== null) throw memo.error;
193
+ return memo.canonical ??= printICU(memo.ast);
194
+ },
195
+ parse(source) {
196
+ const memo = entry(source);
197
+ if (memo.error !== null) throw memo.error;
198
+ return memo.ast;
199
+ },
200
+ sourceHash: hashOf,
201
+ label(canonical) {
202
+ return hashOf(canonical).slice(0, idLength);
203
+ }
204
+ };
205
+ }
206
+ //#endregion
207
+ export { printICU as n, createCodec as t };
@@ -0,0 +1,13 @@
1
+ //#region src/core/extract/collect.ts
2
+ const KEY = Symbol.for("@menapo/vi18n:extract-collector");
3
+ /** The collector `vi18n extract --vite` set, or null outside it. */
4
+ function activeCollector() {
5
+ return globalThis[KEY] ?? null;
6
+ }
7
+ /** Sets (or, with null, clears) the collector plugins created from now on report to. */
8
+ function setCollector(collector) {
9
+ if (collector === null) delete globalThis[KEY];
10
+ else globalThis[KEY] = collector;
11
+ }
12
+ //#endregion
13
+ export { setCollector as n, activeCollector as t };
@@ -0,0 +1,217 @@
1
+ import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
2
+ import { S as entryKey } from "./library-D5CWvTQs.js";
3
+ import { r as resolveDuplicates } from "./duplicates-yuRljQ6h.js";
4
+ //#region src/core/catalog/collisions.ts
5
+ /**
6
+ * Id collisions (PLAN §5.5, INTERNALS §2.6.7): two different texts behind one
7
+ * id are a hard error, never resolved automatically, because any automatic
8
+ * rule would make one message's id depend on another's existence. The check
9
+ * covers the messages being compiled and every entry of the catalog set, so
10
+ * apps sharing catalogs collide loudly instead of overwriting each other.
11
+ */
12
+ const DELETED_HINT = "; if that message was deleted, run vi18n extract first";
13
+ function posix(path) {
14
+ return path.replaceAll("\\", "/");
15
+ }
16
+ function where(file, line) {
17
+ return line > 0 ? `${file}:${line}` : file;
18
+ }
19
+ function messagePlace(message) {
20
+ const location = message.locations?.[0] ?? (message.file === void 0 ? void 0 : {
21
+ file: message.file,
22
+ line: message.line ?? 0
23
+ });
24
+ if (location === void 0) return { text: "" };
25
+ return {
26
+ file: location.file,
27
+ line: location.line,
28
+ text: ` (${where(location.file, location.line)})`
29
+ };
30
+ }
31
+ function canonicalOf(source, codec) {
32
+ try {
33
+ return codec.canonicalize(source);
34
+ } catch (error) {
35
+ if (isDiagnosticError(error)) return null;
36
+ throw error;
37
+ }
38
+ }
39
+ /**
40
+ * Canonical text → where it was first seen, per generated key. Only entries
41
+ * whose source hashes to their key take part: in the `"id"` layout and JSON a
42
+ * hand-edited or stripped source note is a stale entry (reported as a source
43
+ * mismatch elsewhere), not a second text behind the id, and a file still on
44
+ * another id length would match nothing anyway. PLAN §5.5 makes a collision
45
+ * two live texts with one id, and §5.6 says an edited `#. source:` never
46
+ * changes what ships, which a build-stopping collision would; this narrows
47
+ * the wording of INTERNALS §2.6.7 to that intent.
48
+ */
49
+ function indexEntries(set, codec, ignoreKeys) {
50
+ const byKey = /* @__PURE__ */ new Map();
51
+ for (const loaded of set.values()) {
52
+ if (loaded.catalog === null) continue;
53
+ const { catalog } = resolveDuplicates(loaded.catalog, codec);
54
+ const file = posix(loaded.path);
55
+ for (const entry of catalog.entries) {
56
+ if (entry.manualId !== null || entry.foreignContext !== null || entry.source === null) continue;
57
+ const key = entryKey(entry, catalog.layout, codec);
58
+ if (key === null || ignoreKeys !== void 0 && ignoreKeys.has(key)) continue;
59
+ const canonical = canonicalOf(entry.source, codec);
60
+ if (canonical === null || codec.label(canonical) !== key) continue;
61
+ let texts = byKey.get(key);
62
+ if (texts === void 0) byKey.set(key, texts = /* @__PURE__ */ new Map());
63
+ const known = texts.get(canonical);
64
+ if (known === void 0) texts.set(canonical, {
65
+ locale: loaded.locale,
66
+ file,
67
+ line: entry.line,
68
+ translated: isTranslated(entry)
69
+ });
70
+ else if (!known.translated && isTranslated(entry)) known.translated = true;
71
+ }
72
+ }
73
+ return byKey;
74
+ }
75
+ function isTranslated(entry) {
76
+ return entry.translation !== "" && entry.state !== "untranslated";
77
+ }
78
+ function fixes(a, aTranslated, b, bTranslated, codec) {
79
+ const longer = `use a longer ids.length (now ${codec.idLength})`;
80
+ if (aTranslated !== bTranslated) return `give ${JSON.stringify(aTranslated ? b : a)} a manual id (it has no translations yet), or ${longer}`;
81
+ return `give the message that has no translations yet a manual id, or ${longer}`;
82
+ }
83
+ /**
84
+ * `id-collision` errors for: two messages with one id and different
85
+ * canonical text; a generated message and an entry of any locale with its id
86
+ * but a different canonical source; two generated entries of the set with
87
+ * one key and different sources.
88
+ *
89
+ * `referenced`: ids that count as live beyond `messages` (`extraction.keep`,
90
+ * apps sharing the set that this scan does not cover). A colliding entry
91
+ * whose text no message uses and whose id is not in `referenced` gets the
92
+ * "run vi18n extract first" hint, since extract would delete it.
93
+ * `ignoreKeys`: entries extract is about to delete, which therefore never
94
+ * collide.
95
+ */
96
+ function findCollisions(messages, set, codec, options = {}) {
97
+ const diagnostics = [];
98
+ const reported = /* @__PURE__ */ new Set();
99
+ const report = (id, a, b) => {
100
+ const pair = a < b ? `${id}\0${a}\0${b}` : `${id}\0${b}\0${a}`;
101
+ if (reported.has(pair)) return false;
102
+ reported.add(pair);
103
+ return true;
104
+ };
105
+ const entries = indexEntries(set, codec, options.ignoreKeys);
106
+ const byId = /* @__PURE__ */ new Map();
107
+ const literals = [];
108
+ for (const message of messages) {
109
+ if (!message.manual) for (const literal of message.sources ?? (message.source === void 0 ? [] : [message.source])) literals.push([message, literal]);
110
+ const first = byId.get(message.id);
111
+ if (first === void 0) byId.set(message.id, message);
112
+ else if (first.canonical !== message.canonical && report(message.id, first.canonical, message.canonical)) {
113
+ const a = messagePlace(first);
114
+ const b = messagePlace(message);
115
+ const texts = entries.get(message.id);
116
+ const fix = message.manual ? "give each text its own manual id" : fixes(first.canonical, texts?.get(first.canonical)?.translated === true, message.canonical, texts?.get(message.canonical)?.translated === true, codec);
117
+ const diagnostic = {
118
+ code: "id-collision",
119
+ severity: "error",
120
+ message: `${message.id} is the id of two texts, ${JSON.stringify(first.canonical)}${a.text} and ${JSON.stringify(message.canonical)}${b.text}: ${fix}`,
121
+ id: message.id,
122
+ related: [{
123
+ ...b.file === void 0 ? {} : {
124
+ file: b.file,
125
+ line: b.line
126
+ },
127
+ message: message.canonical
128
+ }]
129
+ };
130
+ if (a.file !== void 0) {
131
+ diagnostic.file = a.file;
132
+ diagnostic.line = a.line;
133
+ }
134
+ diagnostics.push(diagnostic);
135
+ }
136
+ }
137
+ for (const [owner, literal] of literals) {
138
+ const other = byId.get(literal);
139
+ if (other === void 0 || other.id === owner.id) continue;
140
+ if (!report(literal, `\0source:${owner.id}`, other.canonical)) continue;
141
+ const a = messagePlace(owner);
142
+ const b = messagePlace(other);
143
+ const diagnostic = {
144
+ code: "id-collision",
145
+ severity: "error",
146
+ message: `${JSON.stringify(literal)} is the text of ${owner.id}${a.text} and the id of ${JSON.stringify(other.canonical)}${b.text}, so the server would render one for the other: ${other.manual ? "rename the manual id" : `give ${JSON.stringify(owner.canonical)} a manual id`}`,
147
+ id: literal,
148
+ related: [{
149
+ ...b.file === void 0 ? {} : {
150
+ file: b.file,
151
+ line: b.line
152
+ },
153
+ message: other.canonical
154
+ }]
155
+ };
156
+ if (a.file !== void 0) {
157
+ diagnostic.file = a.file;
158
+ diagnostic.line = a.line;
159
+ }
160
+ diagnostics.push(diagnostic);
161
+ }
162
+ for (const message of byId.values()) {
163
+ if (message.manual) continue;
164
+ const texts = entries.get(message.id);
165
+ if (texts === void 0) continue;
166
+ const own = texts.get(message.canonical);
167
+ for (const [canonical, entry] of texts) {
168
+ if (canonical === message.canonical || !report(message.id, message.canonical, canonical)) continue;
169
+ const place = messagePlace(message);
170
+ const live = options.referenced?.has(message.id) === true;
171
+ const diagnostic = {
172
+ code: "id-collision",
173
+ severity: "error",
174
+ message: `${message.id} is the id of ${JSON.stringify(message.canonical)}${place.text} and of ${JSON.stringify(canonical)} in the ${entry.locale} catalog (${where(entry.file, entry.line)}): ` + fixes(message.canonical, own?.translated === true, canonical, entry.translated, codec) + (live ? "" : DELETED_HINT),
175
+ id: message.id,
176
+ locale: entry.locale,
177
+ related: [{
178
+ file: entry.file,
179
+ line: entry.line,
180
+ message: canonical
181
+ }]
182
+ };
183
+ if (place.file !== void 0) {
184
+ diagnostic.file = place.file;
185
+ diagnostic.line = place.line;
186
+ }
187
+ diagnostics.push(diagnostic);
188
+ }
189
+ }
190
+ const keys = [...entries.keys()].filter((key) => !byId.has(key) && entries.get(key).size > 1);
191
+ keys.sort();
192
+ for (const key of keys) {
193
+ const texts = [...entries.get(key)];
194
+ const [firstText, firstEntry] = texts[0];
195
+ const live = options.referenced?.has(key) === true;
196
+ for (const [canonical, entry] of texts.slice(1)) {
197
+ if (!report(key, firstText, canonical)) continue;
198
+ diagnostics.push({
199
+ code: "id-collision",
200
+ severity: "error",
201
+ message: `${key} is the id of ${JSON.stringify(firstText)} in the ${firstEntry.locale} catalog (${where(firstEntry.file, firstEntry.line)}) and of ${JSON.stringify(canonical)} in the ${entry.locale} catalog (${where(entry.file, entry.line)}): ` + fixes(firstText, firstEntry.translated, canonical, entry.translated, codec) + (live ? "" : DELETED_HINT),
202
+ id: key,
203
+ locale: firstEntry.locale,
204
+ file: firstEntry.file,
205
+ line: firstEntry.line,
206
+ related: [{
207
+ file: entry.file,
208
+ line: entry.line,
209
+ message: canonical
210
+ }]
211
+ });
212
+ }
213
+ }
214
+ return diagnostics;
215
+ }
216
+ //#endregion
217
+ export { findCollisions as t };