@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.
- package/LICENSE +49 -0
- package/README.md +110 -2
- package/dist/apply-Bre-lY9J.js +228 -0
- package/dist/captured-Bj8bO6vd.js +1034 -0
- package/dist/check-BIKEuu6B.js +201 -0
- package/dist/check-HsmmpJZg.js +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +41 -0
- package/dist/codec-DuvU2VQy.js +207 -0
- package/dist/collect-Cx4_SKOe.js +13 -0
- package/dist/collisions-CfpZU8e4.js +217 -0
- package/dist/compile-BaDh0cHG.js +245 -0
- package/dist/compile-Cc97JaW2.js +2502 -0
- package/dist/concurrency-C9UUWuDC.js +96 -0
- package/dist/concurrency-DONL7t4V.js +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +39 -0
- package/dist/dir-zw6bGMKQ.js +290 -0
- package/dist/duplicates-yuRljQ6h.js +185 -0
- package/dist/engine-C2ENBRJe.js +531 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +1241 -0
- package/dist/exclude-Du-Zd_qI.js +26 -0
- package/dist/export-C9Sy6UWv.js +103 -0
- package/dist/extract-C3S8btx0.js +298 -0
- package/dist/formats-9unqj_fZ.js +781 -0
- package/dist/i18next-9HAoxwUI.js +1 -0
- package/dist/i18next-BEJgv_S8.js +1596 -0
- package/dist/import-nANvQutw.js +225 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +239 -0
- package/dist/init-C2ScuW6a.js +792 -0
- package/dist/library-D5CWvTQs.js +1882 -0
- package/dist/lingui-zUiCl9Um.js +833 -0
- package/dist/lint.d.ts +105 -0
- package/dist/lint.js +971 -0
- package/dist/load-CQRz4PpM.js +2950 -0
- package/dist/load-gW-oJt1Q.js +747 -0
- package/dist/locales-DX4MOBFE.js +827 -0
- package/dist/lsp-RMMMc88P.js +31 -0
- package/dist/match-BGr8EXB3.js +89 -0
- package/dist/mcp-CHVwBoRP.js +28 -0
- package/dist/merge-driver-BTX87aVm.js +422 -0
- package/dist/messages-BfjIExyB.js +166 -0
- package/dist/migrate-IpLDMXmq.js +1484 -0
- package/dist/migrate-rCQuOGsf.js +1 -0
- package/dist/openai-CTaMDcl-.js +346 -0
- package/dist/output-DWE3SIUA.js +164 -0
- package/dist/parse.d.ts +62 -0
- package/dist/parse.js +156 -0
- package/dist/pipeline-C9Dbotkh.js +2031 -0
- package/dist/pipeline-DbX6rlo5.js +1 -0
- package/dist/project-Bw9UPIdx.js +1046 -0
- package/dist/project-C4GpaN-n.js +381 -0
- package/dist/pseudo-DRv30iN5.js +145 -0
- package/dist/raw-C4Y6vyGY.js +4708 -0
- package/dist/react-intl-BP0u-D5m.js +519 -0
- package/dist/restore-CiYv-jxS.js +175 -0
- package/dist/rewrite-EsIw_lsq.js +803 -0
- package/dist/rich-DCUKl5HM.js +97 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/run-BEcnBbu-.js +694 -0
- package/dist/runtime-DTMmliT1.js +64 -0
- package/dist/scan-worker.d.ts +62 -0
- package/dist/scan-worker.js +1 -0
- package/dist/scoped-BMYpSthj.js +44 -0
- package/dist/server-Da1Ot9IN.js +10485 -0
- package/dist/server-deBOq2vq.js +11823 -0
- package/dist/server.d.ts +169 -0
- package/dist/server.js +1234 -0
- package/dist/sitemap-DJXzKoza.js +159 -0
- package/dist/sources-D19czDSA.js +2242 -0
- package/dist/status-zr4jY59Y.js +149 -0
- package/dist/stdio-CVaJfxA7.js +29 -0
- package/dist/stdio-FPOKQAgu.js +95 -0
- package/dist/sync-B9V2KFwb.js +658 -0
- package/dist/t-DC-c9zE5.js +454 -0
- package/dist/test.d.ts +116 -0
- package/dist/test.js +319 -0
- package/dist/translate-CQnJfLjO.js +299 -0
- package/dist/ts-BaU5yOf0.js +4863 -0
- package/dist/types-CgUCkFU1.d.ts +707 -0
- package/dist/types-D3QGM8XO.js +146 -0
- package/dist/types-DsdWBG0b.d.ts +558 -0
- package/dist/types-DtvQ7UdD.js +1948 -0
- package/dist/types-wSh9fCBY.js +11 -0
- package/dist/units-CQ5Q99RC.js +150 -0
- package/dist/url-CRfCOhUI.js +172 -0
- package/dist/url-CsBkRaJy.js +225 -0
- package/dist/validate-BxpupdAf.js +2615 -0
- package/dist/validate-D6DJepZv.js +365 -0
- package/dist/version-Byqi2guX.js +28 -0
- package/dist/vite-scan-CT_vsp4g.js +242 -0
- package/dist/vite.d.ts +34 -0
- package/dist/vite.js +7558 -0
- package/dist/vue-i18n-g0iGy6CZ.js +651 -0
- package/dist/which-BOemZ5Q8.js +116 -0
- package/dist/worker-84U6uYB3.js +1656 -0
- package/package.json +97 -3
- package/schemas/catalog.json +57 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { d as diagnosticError, f as isDiagnosticError, i as normalizeSource, l as positionAt, o as isMF2Form, u as quoteStarts } from "./compile-Cc97JaW2.js";
|
|
2
|
+
//#region src/core/icu/structure.ts
|
|
3
|
+
/**
|
|
4
|
+
* What a translation must keep from its source (INTERNALS §2.1.2).
|
|
5
|
+
*
|
|
6
|
+
* A name has one kind per message, with two exceptions that render the same
|
|
7
|
+
* value with the same meaning: a plain `{x}` goes with any kind, and
|
|
8
|
+
* `{x, number}` goes with a plural or selectordinal of `x` (the count the
|
|
9
|
+
* plural selects on, as in `{count, plural, other {{count, number} items}}`).
|
|
10
|
+
* Everything else changes what the value means and is a mismatch: `date` with
|
|
11
|
+
* `time`, `plural` (cardinal) with `selectordinal`, `number` with `date`.
|
|
12
|
+
*/
|
|
13
|
+
/** Whether two uses of one name can share a value. */
|
|
14
|
+
function compatibleKinds(a, b) {
|
|
15
|
+
return a === b || a === "argument" || b === "argument" || a === "number" && isPlural(b) || b === "number" && isPlural(a);
|
|
16
|
+
}
|
|
17
|
+
function isPlural(kind) {
|
|
18
|
+
return kind === "plural" || kind === "selectordinal";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @throws {DiagnosticError} `icu-argument-mismatch` when a name is used with
|
|
22
|
+
* incompatible kinds, `icu-tag-mismatch` when a tag is both paired and standalone.
|
|
23
|
+
*/
|
|
24
|
+
function structureOf(message) {
|
|
25
|
+
const structure = {
|
|
26
|
+
args: /* @__PURE__ */ new Map(),
|
|
27
|
+
optionalArgs: /* @__PURE__ */ new Set(),
|
|
28
|
+
tags: /* @__PURE__ */ new Map()
|
|
29
|
+
};
|
|
30
|
+
const required = /* @__PURE__ */ new Set();
|
|
31
|
+
collect(message, false, structure, required);
|
|
32
|
+
for (const name of structure.args.keys()) if (!required.has(name)) structure.optionalArgs.add(name);
|
|
33
|
+
return structure;
|
|
34
|
+
}
|
|
35
|
+
function collect(nodes, inPlural, structure, required) {
|
|
36
|
+
for (const node of nodes) switch (node.type) {
|
|
37
|
+
case "literal":
|
|
38
|
+
case "pound": break;
|
|
39
|
+
case "argument":
|
|
40
|
+
use(node.name, "argument", inPlural, structure, required);
|
|
41
|
+
break;
|
|
42
|
+
case "number":
|
|
43
|
+
case "date":
|
|
44
|
+
case "time":
|
|
45
|
+
use(node.name, node.type, inPlural, structure, required);
|
|
46
|
+
break;
|
|
47
|
+
case "select":
|
|
48
|
+
use(node.name, "select", inPlural, structure, required);
|
|
49
|
+
for (const option of node.options) collect(option.value, inPlural, structure, required);
|
|
50
|
+
break;
|
|
51
|
+
case "plural":
|
|
52
|
+
use(node.name, node.pluralType === "cardinal" ? "plural" : "selectordinal", inPlural, structure, required);
|
|
53
|
+
for (const option of node.options) collect(option.value, true, structure, required);
|
|
54
|
+
break;
|
|
55
|
+
case "tag": {
|
|
56
|
+
const kind = node.children === null ? "standalone" : "paired";
|
|
57
|
+
const seen = structure.tags.get(node.name);
|
|
58
|
+
if (seen !== void 0 && seen !== kind) throw diagnosticError({
|
|
59
|
+
code: "icu-tag-mismatch",
|
|
60
|
+
severity: "error",
|
|
61
|
+
message: `<${node.name}> is used both as <${node.name}>…</${node.name}> and as <${node.name}/>: use one form`
|
|
62
|
+
});
|
|
63
|
+
structure.tags.set(node.name, kind);
|
|
64
|
+
if (node.children !== null) collect(node.children, inPlural, structure, required);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function use(name, kind, inPlural, structure, required) {
|
|
70
|
+
if (!inPlural) required.add(name);
|
|
71
|
+
const seen = structure.args.get(name);
|
|
72
|
+
if (seen === void 0 || seen === "argument" || seen === "number" && isPlural(kind)) structure.args.set(name, kind);
|
|
73
|
+
else if (!compatibleKinds(seen, kind)) throw diagnosticError({
|
|
74
|
+
code: "icu-argument-mismatch",
|
|
75
|
+
severity: "error",
|
|
76
|
+
message: `{${name}} is used both as ${describe(seen)} and as ${describe(kind)}: rename one of them`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function describe(kind) {
|
|
80
|
+
return kind === "argument" ? "a plain argument" : `{…, ${kind}}`;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/core/icu/validate.ts
|
|
84
|
+
/**
|
|
85
|
+
* Message validation (INTERNALS §2.1.3, PLAN §5.4), run at extract, compile,
|
|
86
|
+
* AI post-check and CI. Parse errors are thrown by the parser; everything
|
|
87
|
+
* here is found on a parsed AST and returned, so one call reports every
|
|
88
|
+
* problem of a message at once.
|
|
89
|
+
*/
|
|
90
|
+
const CLDR_ORDER = [
|
|
91
|
+
"zero",
|
|
92
|
+
"one",
|
|
93
|
+
"two",
|
|
94
|
+
"few",
|
|
95
|
+
"many",
|
|
96
|
+
"other"
|
|
97
|
+
];
|
|
98
|
+
const categoryCache = /* @__PURE__ */ new Map();
|
|
99
|
+
function categoriesOf(locale) {
|
|
100
|
+
let entry = categoryCache.get(locale);
|
|
101
|
+
if (entry === void 0) {
|
|
102
|
+
const sorted = (type) => {
|
|
103
|
+
const found = new Intl.PluralRules(locale, { type }).resolvedOptions().pluralCategories;
|
|
104
|
+
return CLDR_ORDER.filter((category) => found.includes(category));
|
|
105
|
+
};
|
|
106
|
+
entry = {
|
|
107
|
+
cardinal: sorted("cardinal"),
|
|
108
|
+
ordinal: sorted("ordinal")
|
|
109
|
+
};
|
|
110
|
+
categoryCache.set(locale, entry);
|
|
111
|
+
}
|
|
112
|
+
return entry;
|
|
113
|
+
}
|
|
114
|
+
function validateSource(message, options) {
|
|
115
|
+
const diagnostics = [];
|
|
116
|
+
checkMessage(message, options.locale, options.text, false, diagnostics);
|
|
117
|
+
return diagnostics;
|
|
118
|
+
}
|
|
119
|
+
function validateTranslation(translation, source, options) {
|
|
120
|
+
if (options.maxLength === void 0 && isLiteralOnly(translation) && isLiteralOnly(source) && hasQuietText(options.text)) return [];
|
|
121
|
+
const diagnostics = [];
|
|
122
|
+
const ai = options.ai === true;
|
|
123
|
+
const own = checkMessage(translation, options.locale, options.text, ai, diagnostics);
|
|
124
|
+
const expected = structureOrNull(source);
|
|
125
|
+
if (own !== null && expected !== null) compareStructure(own, expected, diagnostics);
|
|
126
|
+
if (options.maxLength !== void 0) {
|
|
127
|
+
const length = graphemeLength(longestRendering(translation));
|
|
128
|
+
if (length > options.maxLength) diagnostics.push({
|
|
129
|
+
code: "icu-max-length",
|
|
130
|
+
severity: ai ? "error" : "warning",
|
|
131
|
+
message: `Translation is ${length} characters long at its longest, over the limit of ${options.maxLength}: shorten it`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return diagnostics;
|
|
135
|
+
}
|
|
136
|
+
function isLiteralOnly(message) {
|
|
137
|
+
for (const node of message) if (node.type !== "literal") return false;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
/** No apostrophe (the quote lint) and no character `scanLiteral` looks for. */
|
|
141
|
+
function hasQuietText(text) {
|
|
142
|
+
for (let i = 0; i < text.length; i++) {
|
|
143
|
+
const c = text.charCodeAt(i);
|
|
144
|
+
if (c === 39 || c >= 8234 && c <= 8297) return false;
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
/** Checks shared by sources and translations; returns the structure when it is valid. */
|
|
149
|
+
function checkMessage(message, locale, text, ai, diagnostics) {
|
|
150
|
+
const state = {
|
|
151
|
+
locale,
|
|
152
|
+
ai,
|
|
153
|
+
diagnostics,
|
|
154
|
+
isolateDepth: 0,
|
|
155
|
+
isolateUnderflow: false,
|
|
156
|
+
bidiControl: -1
|
|
157
|
+
};
|
|
158
|
+
walk(message, state);
|
|
159
|
+
if (state.bidiControl !== -1) {
|
|
160
|
+
const char = String.fromCharCode(state.bidiControl);
|
|
161
|
+
diagnostics.push({
|
|
162
|
+
code: "icu-bidi-control",
|
|
163
|
+
severity: "error",
|
|
164
|
+
message: `Literal text contains the bidi control U+${hex(state.bidiControl)}, which can reorder the text around it: remove it (vi18n isolates values itself)`,
|
|
165
|
+
...positionOf(text, char)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (state.isolateUnderflow || state.isolateDepth !== 0) diagnostics.push({
|
|
169
|
+
code: "icu-unbalanced-isolate",
|
|
170
|
+
severity: "error",
|
|
171
|
+
message: state.isolateUnderflow ? "U+2069 (pop directional isolate) closes an isolate that was never opened: remove it" : "A directional isolate (U+2066, U+2067 or U+2068) is never closed: add U+2069"
|
|
172
|
+
});
|
|
173
|
+
const structure = structureOrDiagnostic(message, diagnostics);
|
|
174
|
+
if (text.includes("'") && !isMF2Form(text)) {
|
|
175
|
+
const normalized = normalizeSource(text);
|
|
176
|
+
for (const offset of quoteStarts(normalized)) diagnostics.push({
|
|
177
|
+
code: "icu-apostrophe",
|
|
178
|
+
severity: "warning",
|
|
179
|
+
message: `This apostrophe starts quoted text, so what follows is printed literally: write ’ for an apostrophe, or '' if you need a straight one`,
|
|
180
|
+
...positionAt(normalized, offset)
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return structure;
|
|
184
|
+
}
|
|
185
|
+
/** Bidi controls and isolates in literal text; options; plural categories. */
|
|
186
|
+
function walk(nodes, state) {
|
|
187
|
+
for (const node of nodes) switch (node.type) {
|
|
188
|
+
case "literal":
|
|
189
|
+
scanLiteral(node.value, state);
|
|
190
|
+
break;
|
|
191
|
+
case "tag":
|
|
192
|
+
if (node.children !== null) walk(node.children, state);
|
|
193
|
+
break;
|
|
194
|
+
case "select":
|
|
195
|
+
requireOther(node.name, "select", node.options, state);
|
|
196
|
+
for (const option of node.options) walk(option.value, state);
|
|
197
|
+
break;
|
|
198
|
+
case "plural": {
|
|
199
|
+
const type = node.pluralType === "cardinal" ? "plural" : "selectordinal";
|
|
200
|
+
requireOther(node.name, type, node.options, state);
|
|
201
|
+
checkCategories(node.name, type, node.options, state);
|
|
202
|
+
for (const option of node.options) walk(option.value, state);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function scanLiteral(value, state) {
|
|
208
|
+
for (let i = 0; i < value.length; i++) {
|
|
209
|
+
const c = value.charCodeAt(i);
|
|
210
|
+
if (c < 8234 || c > 8297) continue;
|
|
211
|
+
if (c <= 8238) {
|
|
212
|
+
if (state.bidiControl === -1) state.bidiControl = c;
|
|
213
|
+
} else if (c >= 8294 && c <= 8296) state.isolateDepth++;
|
|
214
|
+
else if (c === 8297) {
|
|
215
|
+
if (state.isolateDepth === 0) state.isolateUnderflow = true;
|
|
216
|
+
else state.isolateDepth--;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** The parser enforces this; hand-built and AI-produced ASTs may not have been parsed. */
|
|
221
|
+
function requireOther(name, type, options, state) {
|
|
222
|
+
if (!options.some((option) => option.key === "other")) state.diagnostics.push({
|
|
223
|
+
code: "icu-missing-other",
|
|
224
|
+
severity: "error",
|
|
225
|
+
message: `{${name}, ${type}} needs an other {…} option`
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function checkCategories(name, type, options, state) {
|
|
229
|
+
const { locale, ai, diagnostics } = state;
|
|
230
|
+
const categories = type === "plural" ? categoriesOf(locale).cardinal : categoriesOf(locale).ordinal;
|
|
231
|
+
const missing = categories.filter((category) => category !== "other" && !options.some((o) => o.key === category));
|
|
232
|
+
if (missing.length > 0) diagnostics.push({
|
|
233
|
+
code: "icu-plural-missing-category",
|
|
234
|
+
severity: ai ? "error" : "warning",
|
|
235
|
+
message: `{${name}, ${type}} has no ${missing.join(", ")} option, which ${locale} needs: add ${missing.length > 1 ? "them" : "it"}`
|
|
236
|
+
});
|
|
237
|
+
for (const option of options) {
|
|
238
|
+
const key = option.key;
|
|
239
|
+
if (key === "other" || key.startsWith("=")) continue;
|
|
240
|
+
if (!categories.includes(key)) diagnostics.push({
|
|
241
|
+
code: "icu-plural-unused-category",
|
|
242
|
+
severity: ai ? "error" : "warning",
|
|
243
|
+
message: `{${name}, ${type}} has a ${key} option, which ${locale} never selects: remove it`
|
|
244
|
+
});
|
|
245
|
+
else if (key === "one" && /(?<![\d.,])1(?![\d]|[.,]\d)/.test(directText(option.value))) diagnostics.push({
|
|
246
|
+
code: "icu-plural-one-literal",
|
|
247
|
+
severity: "warning",
|
|
248
|
+
message: `The one option of {${name}, ${type}} contains a literal 1, but one also matches other numbers in many languages: write # instead`
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Literal text of an option, tags included, nested arguments excluded. */
|
|
253
|
+
function directText(nodes) {
|
|
254
|
+
let text = "";
|
|
255
|
+
for (const node of nodes) if (node.type === "literal") text += node.value;
|
|
256
|
+
else if (node.type === "tag" && node.children !== null) text += directText(node.children);
|
|
257
|
+
else text += " ";
|
|
258
|
+
return text;
|
|
259
|
+
}
|
|
260
|
+
function structureOrDiagnostic(message, diagnostics) {
|
|
261
|
+
try {
|
|
262
|
+
return structureOf(message);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (!isDiagnosticError(error)) throw error;
|
|
265
|
+
diagnostics.push(error.diagnostic);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function structureOrNull(message) {
|
|
270
|
+
try {
|
|
271
|
+
return structureOf(message);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (!isDiagnosticError(error)) throw error;
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A translation may write any argument plainly (`{n}` for a plural count), a
|
|
279
|
+
* plural's count as `{n, number}` (languages without plurals) and a source
|
|
280
|
+
* `{n, number}` as a plural; any other kind change alters the meaning. It may
|
|
281
|
+
* drop arguments the source uses only inside plural options.
|
|
282
|
+
*/
|
|
283
|
+
function compareStructure(own, source, diagnostics) {
|
|
284
|
+
const mismatch = (code, message) => diagnostics.push({
|
|
285
|
+
code,
|
|
286
|
+
severity: "error",
|
|
287
|
+
message
|
|
288
|
+
});
|
|
289
|
+
for (const [name, kind] of own.args) {
|
|
290
|
+
const expected = source.args.get(name);
|
|
291
|
+
if (expected === void 0) mismatch("icu-argument-mismatch", `The translation uses {${name}}, which the source does not have: remove it or use a source argument`);
|
|
292
|
+
else if (!translationKindFits(expected, kind)) mismatch("icu-argument-mismatch", `The translation uses {${name}} as ${describe(kind)}, but the source uses it as ${describe(expected)}: use the source's kind`);
|
|
293
|
+
}
|
|
294
|
+
for (const name of source.args.keys()) if (!own.args.has(name) && !source.optionalArgs.has(name)) mismatch("icu-argument-mismatch", `The translation lacks {${name}}: add it`);
|
|
295
|
+
for (const [name, kind] of source.tags) {
|
|
296
|
+
const ownKind = own.tags.get(name);
|
|
297
|
+
if (ownKind === void 0) mismatch("icu-tag-mismatch", `The translation lacks the tag ${tagForm(name, kind)}: add it`);
|
|
298
|
+
else if (ownKind !== kind) mismatch("icu-tag-mismatch", `The translation writes ${tagForm(name, ownKind)} where the source has ${tagForm(name, kind)}`);
|
|
299
|
+
}
|
|
300
|
+
for (const [name, kind] of own.tags) if (!source.tags.has(name)) mismatch("icu-tag-mismatch", `The translation adds the tag ${tagForm(name, kind)}, which the source does not have: remove it`);
|
|
301
|
+
}
|
|
302
|
+
function translationKindFits(source, translation) {
|
|
303
|
+
return translation === "argument" || source !== "argument" && compatibleKinds(source, translation);
|
|
304
|
+
}
|
|
305
|
+
function tagForm(name, kind) {
|
|
306
|
+
return kind === "paired" ? `<${name}>…</${name}>` : `<${name}/>`;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The longest text a message can render, with each argument standing for its
|
|
310
|
+
* name and `#` for one character; select and plural take their longest option.
|
|
311
|
+
*/
|
|
312
|
+
function longestRendering(nodes) {
|
|
313
|
+
let text = "";
|
|
314
|
+
for (const node of nodes) text += longestNode(node);
|
|
315
|
+
return text;
|
|
316
|
+
}
|
|
317
|
+
function longestNode(node) {
|
|
318
|
+
switch (node.type) {
|
|
319
|
+
case "literal": return node.value;
|
|
320
|
+
case "pound": return "#";
|
|
321
|
+
case "tag": return node.children === null ? "" : longestRendering(node.children);
|
|
322
|
+
case "select":
|
|
323
|
+
case "plural": {
|
|
324
|
+
let best = "";
|
|
325
|
+
let bestLength = -1;
|
|
326
|
+
for (const option of node.options) {
|
|
327
|
+
const text = longestRendering(option.value);
|
|
328
|
+
const length = graphemeLength(text);
|
|
329
|
+
if (length > bestLength) {
|
|
330
|
+
best = text;
|
|
331
|
+
bestLength = length;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return best;
|
|
335
|
+
}
|
|
336
|
+
default: return node.name;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
let segmenter;
|
|
340
|
+
/** User-perceived characters; strings below U+0300 without CR need no segmentation. */
|
|
341
|
+
function graphemeLength(text) {
|
|
342
|
+
let simple = true;
|
|
343
|
+
for (let i = 0; i < text.length; i++) {
|
|
344
|
+
const c = text.charCodeAt(i);
|
|
345
|
+
if (c >= 768 || c === 13) {
|
|
346
|
+
simple = false;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (simple) return text.length;
|
|
351
|
+
segmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
352
|
+
let count = 0;
|
|
353
|
+
for (const _ of segmenter.segment(text)) count++;
|
|
354
|
+
return count;
|
|
355
|
+
}
|
|
356
|
+
function positionOf(text, char) {
|
|
357
|
+
const normalized = normalizeSource(text);
|
|
358
|
+
const offset = normalized.indexOf(char);
|
|
359
|
+
return offset === -1 ? {} : positionAt(normalized, offset);
|
|
360
|
+
}
|
|
361
|
+
function hex(code) {
|
|
362
|
+
return code.toString(16).toUpperCase().padStart(4, "0");
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
export { validateTranslation as n, validateSource as t };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:crypto";
|
|
3
|
+
import "node:fs";
|
|
4
|
+
import "node:url";
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/core/version.ts
|
|
7
|
+
const VERSION = "0.1.1";
|
|
8
|
+
let fingerprint;
|
|
9
|
+
let cacheVersionMemo;
|
|
10
|
+
/**
|
|
11
|
+
* A fingerprint of the code this process runs. Packed, the hash the pack
|
|
12
|
+
* inlined, which costs nothing. From source (the workspace's examples, the
|
|
13
|
+
* bench and the tests), a hash of every `.ts` file under the `src/` this
|
|
14
|
+
* module lives in, computed on first use and kept for the process: about
|
|
15
|
+
* 10 ms for the 1.3 MB of source, paid only by processes that key a cache.
|
|
16
|
+
*/
|
|
17
|
+
function codeFingerprint() {
|
|
18
|
+
if (fingerprint !== void 0) return fingerprint;
|
|
19
|
+
fingerprint = "0UPRQnKMBktq7h16";
|
|
20
|
+
return fingerprint;
|
|
21
|
+
}
|
|
22
|
+
/** `VERSION` and `codeFingerprint()`: the version every cache key and `configHash` carries. */
|
|
23
|
+
function cacheVersion() {
|
|
24
|
+
cacheVersionMemo ??= `${VERSION}+${codeFingerprint()}`;
|
|
25
|
+
return cacheVersionMemo;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { cacheVersion as n, VERSION as t };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { a as toPosix, i as listProjectFiles, o as aggregateOccurrences, t as refMapper } from "./project-Bw9UPIdx.js";
|
|
2
|
+
import { t as ABORT_CODES } from "./worker-84U6uYB3.js";
|
|
3
|
+
import { n as setCollector } from "./collect-Cx4_SKOe.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
//#region src/cli/vite-scan.ts
|
|
9
|
+
/**
|
|
10
|
+
* `vi18n extract --vite` (PLAN §4.5 P2, INTERNALS §2.5.9, §6 D234–D235):
|
|
11
|
+
* the include scan through the project's own Vite pipeline, for projects
|
|
12
|
+
* whose plugins change code before vi18n sees it (auto-imports, macros,
|
|
13
|
+
* generated components). A Vite dev server is created in middleware mode
|
|
14
|
+
* from the project's Vite config, every included file is transformed by
|
|
15
|
+
* its plugins, and what `vi18n:extract` records for each module is
|
|
16
|
+
* collected (`core/extract/collect.ts`). Nothing is bundled, served or
|
|
17
|
+
* written by the server; the command then syncs the catalogs as usual.
|
|
18
|
+
*/
|
|
19
|
+
/** Files transformed at once: plugins await compilers and the file system. */
|
|
20
|
+
const CONCURRENCY = 8;
|
|
21
|
+
/** The project's `vite`, else the one vi18n itself resolves (its peer dependency). */
|
|
22
|
+
async function loadVite(root) {
|
|
23
|
+
for (const from of [join(root, "package.json"), import.meta.url]) {
|
|
24
|
+
let path;
|
|
25
|
+
try {
|
|
26
|
+
path = createRequire(from).resolve("vite");
|
|
27
|
+
} catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
return await import(pathToFileURL(path).href);
|
|
31
|
+
}
|
|
32
|
+
throw new Error("vite is not installed in the project");
|
|
33
|
+
}
|
|
34
|
+
/** How Vite requests a file: root-relative inside the root, `/@fs/` outside it. */
|
|
35
|
+
function urlOf(root, file) {
|
|
36
|
+
const rel = toPosix(relative(root, file));
|
|
37
|
+
return rel.startsWith("../") || rel === ".." || /^[a-zA-Z]:/.test(rel) ? "/@fs" + (toPosix(file).startsWith("/") ? "" : "/") + toPosix(file) : "/" + rel;
|
|
38
|
+
}
|
|
39
|
+
function messageOf(error) {
|
|
40
|
+
return (error instanceof Error ? error.message : String(error)).split("\n")[0];
|
|
41
|
+
}
|
|
42
|
+
function collectorInto(collected, active) {
|
|
43
|
+
return {
|
|
44
|
+
module(id, messages) {
|
|
45
|
+
if (!active()) return;
|
|
46
|
+
let path = id.charCodeAt(0) === 0 ? id.slice(1) : id;
|
|
47
|
+
const query = path.indexOf("?");
|
|
48
|
+
if (query !== -1) path = path.slice(0, query);
|
|
49
|
+
path = toPosix(path);
|
|
50
|
+
let byId = collected.modules.get(path);
|
|
51
|
+
if (byId === void 0) collected.modules.set(path, byId = /* @__PURE__ */ new Map());
|
|
52
|
+
byId.set(id, messages);
|
|
53
|
+
},
|
|
54
|
+
diagnostic(diagnostic, path) {
|
|
55
|
+
if (!active()) return;
|
|
56
|
+
if (path !== void 0) collected.failed.add(toPosix(path));
|
|
57
|
+
collected.diagnostics.push(diagnostic);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Transforms `file` in the first environment whose pipeline accepts it
|
|
63
|
+
* (client, then the others: server-only code may import what a client
|
|
64
|
+
* build refuses), then the modules the same file split into (`?vue&type=`,
|
|
65
|
+
* Astro's `?astro&type=script`). Resolves with the failure to report, or
|
|
66
|
+
* null.
|
|
67
|
+
*/
|
|
68
|
+
async function transformFile(server, root, file, collected) {
|
|
69
|
+
const path = toPosix(file);
|
|
70
|
+
const url = urlOf(root, file);
|
|
71
|
+
const names = Object.keys(server.environments).sort((a, b) => a === "client" ? -1 : b === "client" ? 1 : 0);
|
|
72
|
+
let firstError = null;
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
const environment = server.environments[name];
|
|
75
|
+
try {
|
|
76
|
+
await environment.transformRequest(url);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (collected.failed.has(path)) return null;
|
|
79
|
+
if (collected.modules.has(path)) return null;
|
|
80
|
+
firstError ??= error;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const main = await environment.moduleGraph.getModuleByUrl(url);
|
|
84
|
+
for (const part of main?.importedModules ?? []) {
|
|
85
|
+
if (part.file === null || toPosix(part.file) !== path || part.id === main?.id) continue;
|
|
86
|
+
try {
|
|
87
|
+
await environment.transformRequest(part.url);
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return firstError === null ? null : messageOf(firstError);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The project scan of `vi18n extract --vite`: the same `ProjectScan` as
|
|
96
|
+
* `scanProject`, from what `vi18n:extract` records while the project's Vite
|
|
97
|
+
* plugins transform each included file.
|
|
98
|
+
*/
|
|
99
|
+
async function scanWithVite(config) {
|
|
100
|
+
const ref = refMapper(config);
|
|
101
|
+
const listed = await listProjectFiles(config);
|
|
102
|
+
const leading = listed.emptyRoots.map((root) => ({
|
|
103
|
+
code: "extract-empty-root",
|
|
104
|
+
severity: "error",
|
|
105
|
+
message: `The include entry ${ref(root)} matches no file; fix project.include or project.exclude in the config`,
|
|
106
|
+
file: ref(root)
|
|
107
|
+
}));
|
|
108
|
+
let vite;
|
|
109
|
+
try {
|
|
110
|
+
vite = await loadVite(config.root);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return failedScan(leading, `vi18n extract --vite needs vite: ${messageOf(error)}`);
|
|
113
|
+
}
|
|
114
|
+
const collected = {
|
|
115
|
+
modules: /* @__PURE__ */ new Map(),
|
|
116
|
+
diagnostics: [],
|
|
117
|
+
failed: /* @__PURE__ */ new Set()
|
|
118
|
+
};
|
|
119
|
+
let active = false;
|
|
120
|
+
setCollector(collectorInto(collected, () => active));
|
|
121
|
+
let server;
|
|
122
|
+
try {
|
|
123
|
+
server = await vite.createServer({
|
|
124
|
+
configFile: viteConfigFile(config.root),
|
|
125
|
+
logLevel: "error",
|
|
126
|
+
clearScreen: false,
|
|
127
|
+
appType: "custom",
|
|
128
|
+
server: {
|
|
129
|
+
middlewareMode: true,
|
|
130
|
+
hmr: false,
|
|
131
|
+
ws: false,
|
|
132
|
+
watch: null,
|
|
133
|
+
preTransformRequests: false
|
|
134
|
+
},
|
|
135
|
+
optimizeDeps: {
|
|
136
|
+
noDiscovery: true,
|
|
137
|
+
include: []
|
|
138
|
+
},
|
|
139
|
+
plugins: [{
|
|
140
|
+
name: "vi18n:extract-vite",
|
|
141
|
+
enforce: "pre",
|
|
142
|
+
config: {
|
|
143
|
+
order: "pre",
|
|
144
|
+
handler(user) {
|
|
145
|
+
return user.root === void 0 ? { root: config.root } : {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}]
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
setCollector(null);
|
|
152
|
+
return failedScan(leading, `the Vite config could not be loaded: ${messageOf(error)}`);
|
|
153
|
+
}
|
|
154
|
+
const failures = /* @__PURE__ */ new Map();
|
|
155
|
+
try {
|
|
156
|
+
if (!server.config.plugins.some((plugin) => plugin.name === "vi18n:extract")) return failedScan(leading, "vi18n extract --vite found no vi18n() plugin in the Vite config; add it, or run vi18n extract without --vite");
|
|
157
|
+
active = true;
|
|
158
|
+
const viteRoot = server.config.root ?? config.root;
|
|
159
|
+
let next = 0;
|
|
160
|
+
const lanes = [];
|
|
161
|
+
for (let lane = 0; lane < Math.min(CONCURRENCY, listed.files.length); lane++) lanes.push((async () => {
|
|
162
|
+
while (next < listed.files.length) {
|
|
163
|
+
const file = listed.files[next++];
|
|
164
|
+
const failure = await transformFile(server, viteRoot, file, collected);
|
|
165
|
+
if (failure !== null) failures.set(file, failure);
|
|
166
|
+
}
|
|
167
|
+
})());
|
|
168
|
+
await Promise.all(lanes);
|
|
169
|
+
} finally {
|
|
170
|
+
active = false;
|
|
171
|
+
setCollector(null);
|
|
172
|
+
await server.close();
|
|
173
|
+
}
|
|
174
|
+
const files = [];
|
|
175
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const diagnostic of collected.diagnostics) {
|
|
177
|
+
const key = diagnostic.file ?? "";
|
|
178
|
+
let list = byRef.get(key);
|
|
179
|
+
if (list === void 0) byRef.set(key, list = []);
|
|
180
|
+
list.push(diagnostic);
|
|
181
|
+
}
|
|
182
|
+
const diagnostics = [...leading];
|
|
183
|
+
for (const file of listed.files) {
|
|
184
|
+
const refFile = ref(file);
|
|
185
|
+
const fileDiagnostics = byRef.get(refFile) ?? [];
|
|
186
|
+
byRef.delete(refFile);
|
|
187
|
+
const failure = failures.get(file);
|
|
188
|
+
if (failure !== void 0) fileDiagnostics.push({
|
|
189
|
+
code: "extract-parse-error",
|
|
190
|
+
severity: "error",
|
|
191
|
+
message: `Cannot transform ${refFile} through the Vite config: ${failure}`,
|
|
192
|
+
file: refFile
|
|
193
|
+
});
|
|
194
|
+
const occurrences = [...(collected.modules.get(toPosix(file)) ?? /* @__PURE__ */ new Map()).entries()].sort(([a], [b]) => (a.includes("?") ? 1 : 0) - (b.includes("?") ? 1 : 0) || (a < b ? -1 : a > b ? 1 : 0)).flatMap(([, messages]) => [...messages]);
|
|
195
|
+
files.push({
|
|
196
|
+
file,
|
|
197
|
+
refFile,
|
|
198
|
+
occurrences,
|
|
199
|
+
diagnostics: fileDiagnostics,
|
|
200
|
+
fromCache: false
|
|
201
|
+
});
|
|
202
|
+
diagnostics.push(...fileDiagnostics);
|
|
203
|
+
}
|
|
204
|
+
for (const rest of byRef.values()) diagnostics.push(...rest);
|
|
205
|
+
return {
|
|
206
|
+
files,
|
|
207
|
+
messages: aggregateOccurrences(files.flatMap((scan) => scan.occurrences)),
|
|
208
|
+
diagnostics,
|
|
209
|
+
aborted: diagnostics.some((diagnostic) => ABORT_CODES.has(diagnostic.code))
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const VITE_CONFIGS = [
|
|
213
|
+
"vite.config.js",
|
|
214
|
+
"vite.config.mjs",
|
|
215
|
+
"vite.config.ts",
|
|
216
|
+
"vite.config.cjs",
|
|
217
|
+
"vite.config.mts",
|
|
218
|
+
"vite.config.cts"
|
|
219
|
+
];
|
|
220
|
+
/** Vite's own lookup, from the vi18n config's root instead of the process's directory. */
|
|
221
|
+
function viteConfigFile(root) {
|
|
222
|
+
for (const name of VITE_CONFIGS) {
|
|
223
|
+
const file = join(root, name);
|
|
224
|
+
if (existsSync(file)) return file;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
function failedScan(leading, message) {
|
|
229
|
+
const diagnostics = [...leading, {
|
|
230
|
+
code: "extract-parse-error",
|
|
231
|
+
severity: "error",
|
|
232
|
+
message
|
|
233
|
+
}];
|
|
234
|
+
return {
|
|
235
|
+
files: [],
|
|
236
|
+
messages: /* @__PURE__ */ new Map(),
|
|
237
|
+
diagnostics,
|
|
238
|
+
aborted: true
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
export { scanWithVite };
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { n as OutputOptions } from "./types-CgUCkFU1.js";
|
|
2
|
+
import { Plugin, Rolldown } from "vite";
|
|
3
|
+
//#region src/vite/types.d.ts
|
|
4
|
+
/** Options of `vi18n()`. Everything else comes from `vi18n.config.ts`. */
|
|
5
|
+
interface Vi18nPluginOptions {
|
|
6
|
+
/** Path of the config file, relative to the Vite root. Default: search upward for `vi18n.config.ts`. */
|
|
7
|
+
config?: string;
|
|
8
|
+
/** Build-only overrides, merged over the config's `output`. */
|
|
9
|
+
output?: OutputOptions;
|
|
10
|
+
/**
|
|
11
|
+
* Removed before 1.0: `vi18n()` adds its own plugins to Vite's
|
|
12
|
+
* `worker.plugins` (INTERNALS §2.9.9), so a worker build needs no second
|
|
13
|
+
* instance. `true` is rejected with `config-invalid` naming that.
|
|
14
|
+
*/
|
|
15
|
+
worker?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Options of `vi18nTransform()`, the call rewrite alone for Rollup/Rolldown pipelines (Nitro). */
|
|
18
|
+
interface Vi18nTransformOptions {
|
|
19
|
+
config?: string;
|
|
20
|
+
/** Default `"build"` (ids only). */
|
|
21
|
+
mode?: "dev" | "build";
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/vite/index.d.ts
|
|
25
|
+
export declare function vi18n(options?: Vi18nPluginOptions): Plugin[];
|
|
26
|
+
/**
|
|
27
|
+
* The extraction rewrite for a plain Rollup/Rolldown build (§2.8.3, last
|
|
28
|
+
* paragraph): steps 1–6 in `build` mode (ids) unless `mode: "dev"`, without
|
|
29
|
+
* the boot import and the store. Such code runs in a process where the
|
|
30
|
+
* Vite-built server bundle or `registerCatalogs` registered catalogs.
|
|
31
|
+
*/
|
|
32
|
+
export declare function vi18nTransform(options?: Vi18nTransformOptions): Rolldown.Plugin;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { type Vi18nPluginOptions, type Vi18nTransformOptions, vi18n as default };
|