@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,651 @@
|
|
|
1
|
+
import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
|
|
2
|
+
import { n as printICU } from "./codec-DuvU2VQy.js";
|
|
3
|
+
import { u as parseModule } from "./worker-84U6uYB3.js";
|
|
4
|
+
import { f as readString, g as skipSpace, h as skipBalanced, o as importLineEdit, p as readTemplate, r as argumentEnd, y as vueRegions } from "./migrate-IpLDMXmq.js";
|
|
5
|
+
import { i as analyzeModule, l as identifierName, s as childEntries, t as ModuleBuilder, v as memberParts, x as unwrap, y as objectProperties } from "./rewrite-EsIw_lsq.js";
|
|
6
|
+
import { b as pushLiteral, m as flattenTree, s as entry, t as MODULE_EXTENSIONS, v as parseJson, x as yamlKeyLines, y as parseYaml } from "./i18next-BEJgv_S8.js";
|
|
7
|
+
import { extname } from "node:path";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
//#region src/cli/commands/migrate/vue-i18n.ts
|
|
10
|
+
/**
|
|
11
|
+
* `vi18n migrate --from vue-i18n` (INTERNALS §2.14): vue-i18n 9–11 (and the
|
|
12
|
+
* v8 legacy API it keeps).
|
|
13
|
+
*
|
|
14
|
+
* Catalogs are JSON or YAML per locale, or one file keyed by locale; nested
|
|
15
|
+
* keys join with `.`. Messages convert to ICU: `{name}` and `{0}` stay
|
|
16
|
+
* arguments, `{'@'}` literals become text, linked messages (`@:key`,
|
|
17
|
+
* `@.lower:key`, `@:(key)`) are inlined from the same locale, and `|`
|
|
18
|
+
* pluralization becomes a plural on `count` (two forms `one`/`other`, three
|
|
19
|
+
* `=0`/`one`/`other`; `{count}` and `{n}` inside are `#`). Call sites:
|
|
20
|
+
* `t()`/`$t()`/`tc()`/`$tc()` in templates (interpolations and bound
|
|
21
|
+
* attributes) and scripts, `i18n.global.t()`, `this.$t()`. `useI18n()`
|
|
22
|
+
* bindings and imports go once unused; `<i18n-t>`, `v-t`, `$d`/`$n` and the
|
|
23
|
+
* setup are reported, their messages kept in the `msg()` table.
|
|
24
|
+
*/
|
|
25
|
+
function isOldSource(source) {
|
|
26
|
+
return source === "vue-i18n" || source.startsWith("vue-i18n/") || source === "@intlify/core-base" || source === "petite-vue-i18n";
|
|
27
|
+
}
|
|
28
|
+
const MODIFIERS = {
|
|
29
|
+
upper: (s) => s.toUpperCase(),
|
|
30
|
+
lower: (s) => s.toLowerCase(),
|
|
31
|
+
capitalize: (s) => s === "" ? s : s[0].toUpperCase() + s.slice(1)
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* One vue-i18n message as ICU. `linked(key)` returns the raw text of a
|
|
35
|
+
* linked message in the same locale.
|
|
36
|
+
*/
|
|
37
|
+
function convertVueMessage(raw, linked, depth = 0) {
|
|
38
|
+
const forms = [[]];
|
|
39
|
+
let current = forms[0];
|
|
40
|
+
let i = 0;
|
|
41
|
+
const literalText = (value) => pushLiteral(current, value);
|
|
42
|
+
while (i < raw.length) {
|
|
43
|
+
const c = raw[i];
|
|
44
|
+
if (c === "{") {
|
|
45
|
+
const quote = /^\{\s*'(?:[^'\\]|\\.)*'\s*\}/.exec(raw.slice(i));
|
|
46
|
+
const close = quote === null ? raw.indexOf("}", i + 1) : i + quote[0].length - 1;
|
|
47
|
+
if (close === -1) return { error: "an unclosed {" };
|
|
48
|
+
const body = raw.slice(i + 1, close).trim();
|
|
49
|
+
i = close + 1;
|
|
50
|
+
const literal = /^'((?:[^'\\]|\\.)*)'$/.exec(body);
|
|
51
|
+
if (literal !== null) {
|
|
52
|
+
literalText(literal[1].replace(/\\(.)/g, "$1"));
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!/^[A-Za-z_$][\w$]*$|^\d+$/.test(body)) return { error: `{${body}} is not a named or list interpolation` };
|
|
56
|
+
current.push({
|
|
57
|
+
type: "argument",
|
|
58
|
+
name: body
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (c === "@") {
|
|
63
|
+
const m = /^@(?:\.(\w+))?:(?:\(([^)]*)\)|([\w$\-.]+))/.exec(raw.slice(i));
|
|
64
|
+
if (m === null) {
|
|
65
|
+
literalText("@");
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const modifier = m[1];
|
|
70
|
+
let key = (m[2] ?? m[3] ?? "").trim();
|
|
71
|
+
let consumed = m[0].length;
|
|
72
|
+
if (m[3] !== void 0 && key.endsWith(".") && linked(key) === void 0 && linked(key.slice(0, -1)) !== void 0) {
|
|
73
|
+
key = key.slice(0, -1);
|
|
74
|
+
consumed--;
|
|
75
|
+
}
|
|
76
|
+
i += consumed;
|
|
77
|
+
if (depth > 8) return { error: "linked messages nest deeper than 8 levels" };
|
|
78
|
+
const text = linked(key);
|
|
79
|
+
if (text === void 0) return { error: `@:${key} links to no message` };
|
|
80
|
+
const inner = convertVueMessage(text, linked, depth + 1);
|
|
81
|
+
if ("error" in inner) return inner;
|
|
82
|
+
if (inner.nodes.some((n) => n.type === "plural")) return { error: `@:${key} links to a plural message` };
|
|
83
|
+
if (modifier !== void 0) {
|
|
84
|
+
const apply = MODIFIERS[modifier];
|
|
85
|
+
if (apply === void 0) return { error: `@.${modifier}: is not a built-in modifier` };
|
|
86
|
+
if (inner.nodes.some((n) => n.type !== "literal")) return { error: `@.${modifier}:${key} changes a message with placeholders, which ICU cannot` };
|
|
87
|
+
literalText(apply(inner.nodes.map((n) => n.value).join("")));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const node of inner.nodes) if (node.type === "literal") literalText(node.value);
|
|
91
|
+
else current.push(node);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (c === "|") {
|
|
95
|
+
current = [];
|
|
96
|
+
forms.push(current);
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
literalText(c);
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
if (forms.length === 1) return { nodes: forms[0] };
|
|
104
|
+
const trimmed = forms.map((form) => trimForm(form));
|
|
105
|
+
const keys = trimmed.length === 2 ? ["one", "other"] : trimmed.length === 3 ? [
|
|
106
|
+
"=0",
|
|
107
|
+
"one",
|
|
108
|
+
"other"
|
|
109
|
+
] : null;
|
|
110
|
+
if (keys === null) return { error: `${trimmed.length} plural forms need custom pluralRules; write the ICU plural by hand` };
|
|
111
|
+
return { nodes: [{
|
|
112
|
+
type: "plural",
|
|
113
|
+
pluralType: "cardinal",
|
|
114
|
+
name: "count",
|
|
115
|
+
offset: 0,
|
|
116
|
+
options: trimmed.map((form, index) => ({
|
|
117
|
+
key: keys[index],
|
|
118
|
+
value: form.map((node) => node.type === "argument" && (node.name === "count" || node.name === "n") ? { type: "pound" } : node)
|
|
119
|
+
}))
|
|
120
|
+
}] };
|
|
121
|
+
}
|
|
122
|
+
function trimForm(form) {
|
|
123
|
+
const out = form.map((n) => ({ ...n }));
|
|
124
|
+
const first = out[0];
|
|
125
|
+
if (first?.type === "literal") {
|
|
126
|
+
first.value = first.value.replace(/^\s+/, "");
|
|
127
|
+
if (first.value === "") out.shift();
|
|
128
|
+
}
|
|
129
|
+
const last = out[out.length - 1];
|
|
130
|
+
if (last?.type === "literal") {
|
|
131
|
+
last.value = last.value.replace(/\s+$/, "");
|
|
132
|
+
if (last.value === "") out.pop();
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function createVueI18nAdapter() {
|
|
137
|
+
/** locale → key → text. */
|
|
138
|
+
const catalogs = /* @__PURE__ */ new Map();
|
|
139
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
140
|
+
let sourceLocale = "";
|
|
141
|
+
const textOf = (locale, key) => catalogs.get(locale)?.get(key);
|
|
142
|
+
const add = (locale, key, value, file) => {
|
|
143
|
+
let texts = catalogs.get(locale);
|
|
144
|
+
if (texts === void 0) catalogs.set(locale, texts = /* @__PURE__ */ new Map());
|
|
145
|
+
texts.set(key, {
|
|
146
|
+
...value,
|
|
147
|
+
file
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const readCatalog = (file, env) => {
|
|
151
|
+
let text;
|
|
152
|
+
try {
|
|
153
|
+
text = readFileSync(file.file, "utf8");
|
|
154
|
+
} catch (error) {
|
|
155
|
+
env.report("catalog-parse", `cannot read: ${error.message}`, { file: file.file }, "error");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const yaml = extname(file.file) === ".yaml" || extname(file.file) === ".yml";
|
|
159
|
+
const parsed = yaml ? parseYaml(text) : parseJson(text);
|
|
160
|
+
if (parsed.problem !== null) {
|
|
161
|
+
env.report("catalog-parse", parsed.problem.message, {
|
|
162
|
+
file: file.file,
|
|
163
|
+
line: parsed.problem.line
|
|
164
|
+
}, "error");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const keyLines = yaml ? yamlKeyLines(text) : null;
|
|
168
|
+
const read = (tree, locale) => {
|
|
169
|
+
const flat = flattenTree(tree, yaml ? "" : text);
|
|
170
|
+
for (const problem of flat.problems) env.report("migrate-unconverted", problem.message, {
|
|
171
|
+
file: file.file,
|
|
172
|
+
line: problem.line
|
|
173
|
+
});
|
|
174
|
+
for (const [key, value] of flat.texts) {
|
|
175
|
+
const line = keyLines === null ? value.line : keyLines.get(key.split(".").pop()) ?? 0;
|
|
176
|
+
add(locale, key, {
|
|
177
|
+
value: value.value,
|
|
178
|
+
line
|
|
179
|
+
}, file.file);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const value = parsed.value;
|
|
183
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
184
|
+
const keys = Object.keys(value);
|
|
185
|
+
const locales = keys.map((k) => env.config.locales.find((l) => l.toLowerCase() === k.toLowerCase().replace(/_/g, "-")));
|
|
186
|
+
if (keys.length > 0 && locales.every((l) => l !== void 0) && Object.values(value).every((v) => typeof v === "object" && v !== null)) {
|
|
187
|
+
keys.forEach((k, index) => read(value[k], locales[index]));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
read(value, file.locale);
|
|
192
|
+
};
|
|
193
|
+
/** Registers a message; its registry key, or why it cannot be. */
|
|
194
|
+
const define = (env, key, fallback) => {
|
|
195
|
+
referenced.add(key);
|
|
196
|
+
if (env.message(key) !== void 0) return { key };
|
|
197
|
+
const convert = (locale, raw) => convertVueMessage(raw, (linkedKey) => {
|
|
198
|
+
referenced.add(linkedKey);
|
|
199
|
+
return textOf(locale, linkedKey)?.value;
|
|
200
|
+
});
|
|
201
|
+
const sourceText = textOf(sourceLocale, key);
|
|
202
|
+
const raw = sourceText?.value ?? fallback;
|
|
203
|
+
if (raw === null) return { problem: `${key} is in no ${sourceLocale} catalog` };
|
|
204
|
+
const source = convert(sourceLocale, raw);
|
|
205
|
+
if ("error" in source) return { problem: `${key}: ${source.error}` };
|
|
206
|
+
const translations = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const [locale, texts] of catalogs) {
|
|
208
|
+
if (locale === sourceLocale) continue;
|
|
209
|
+
const found = texts.get(key);
|
|
210
|
+
if (found === void 0) continue;
|
|
211
|
+
const converted = convert(locale, found.value);
|
|
212
|
+
translations.set(locale, "error" in converted ? {
|
|
213
|
+
unconverted: found.value,
|
|
214
|
+
reason: converted.error,
|
|
215
|
+
file: found.file,
|
|
216
|
+
line: found.line
|
|
217
|
+
} : { icu: printICU(converted.nodes) });
|
|
218
|
+
}
|
|
219
|
+
return env.define({
|
|
220
|
+
key,
|
|
221
|
+
named: true,
|
|
222
|
+
source: printICU(source.nodes),
|
|
223
|
+
context: null,
|
|
224
|
+
hint: null,
|
|
225
|
+
translations,
|
|
226
|
+
file: sourceText?.file ?? "",
|
|
227
|
+
line: sourceText?.line ?? 0
|
|
228
|
+
}) ? { key } : { problem: `${key} could not be converted` };
|
|
229
|
+
};
|
|
230
|
+
const isPluralKey = (env, key) => {
|
|
231
|
+
const message = env.message(key);
|
|
232
|
+
return message !== void 0 && message.canonical.startsWith("{count, plural,") && message.canonical.endsWith("}");
|
|
233
|
+
};
|
|
234
|
+
/** Keys a computed key can produce (string literals and matching templates in it). */
|
|
235
|
+
const keysIn = (code) => {
|
|
236
|
+
const keys = [...catalogs.get(sourceLocale)?.keys() ?? []];
|
|
237
|
+
const found = /* @__PURE__ */ new Set();
|
|
238
|
+
for (let i = 0; i < code.length; i++) {
|
|
239
|
+
const c = code[i];
|
|
240
|
+
if (c === "\"" || c === "'") {
|
|
241
|
+
const s = readString(code, i);
|
|
242
|
+
if (s === null) break;
|
|
243
|
+
if (keys.includes(s.value)) found.add(s.value);
|
|
244
|
+
i = s.end - 1;
|
|
245
|
+
} else if (c === "`") {
|
|
246
|
+
const s = readTemplate(code, i);
|
|
247
|
+
if (s === null) break;
|
|
248
|
+
if (/[A-Za-z]/.test(s.quasis.join(""))) {
|
|
249
|
+
const pattern = new RegExp("^" + s.quasis.map((q) => q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^]+?") + "$");
|
|
250
|
+
for (const key of keys) if (pattern.test(key)) found.add(key);
|
|
251
|
+
}
|
|
252
|
+
i = s.end - 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return [...found];
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* The values of a `t`-family call from its arguments as written (default
|
|
259
|
+
* message strings left out): `t(key, named | list | values)`,
|
|
260
|
+
* `t(key, count)` and `t(key, named | list, count)` for a plural message
|
|
261
|
+
* (`tc` always pluralizes), or why vi18n migrate cannot read them.
|
|
262
|
+
*/
|
|
263
|
+
const callValues = (env, key, args, tc) => {
|
|
264
|
+
const plural = tc || isPluralKey(env, key);
|
|
265
|
+
let named = null;
|
|
266
|
+
let list = null;
|
|
267
|
+
let count = null;
|
|
268
|
+
const [a, b] = args;
|
|
269
|
+
if (args.length > 2) return { problem: "more than two value arguments" };
|
|
270
|
+
if (a !== void 0 && b === void 0) {
|
|
271
|
+
if (a.kind === "object") named = 0;
|
|
272
|
+
else if (a.kind === "array") list = 0;
|
|
273
|
+
else if (plural) count = 0;
|
|
274
|
+
else named = 0;
|
|
275
|
+
} else if (a !== void 0 && b !== void 0) {
|
|
276
|
+
if ((a.kind === "object" || a.kind === "array") && b.kind === "other") {
|
|
277
|
+
if (a.kind === "object") named = 0;
|
|
278
|
+
else list = 0;
|
|
279
|
+
count = 1;
|
|
280
|
+
} else return { problem: "options vi18n has no counterpart for" };
|
|
281
|
+
}
|
|
282
|
+
const namedArg = named === null ? void 0 : args[named];
|
|
283
|
+
const namesCount = namedArg?.kind === "object" && /[{,]\s*count\s*[:,}]/.test(namedArg.code);
|
|
284
|
+
if (plural && count === null && !namesCount) return { problem: "a plural message without a count" };
|
|
285
|
+
return { values: (src) => {
|
|
286
|
+
const entries = [];
|
|
287
|
+
if (count !== null) entries.push(entry("count", src(count).trim()));
|
|
288
|
+
if (named !== null) {
|
|
289
|
+
const code = src(named).trim();
|
|
290
|
+
if (args[named]?.kind === "object") {
|
|
291
|
+
const inner = code.slice(1, -1).trim();
|
|
292
|
+
if (inner !== "") entries.push(inner);
|
|
293
|
+
} else if (entries.length === 0) return `, ${code}`;
|
|
294
|
+
else entries.push(`...${code}`);
|
|
295
|
+
}
|
|
296
|
+
if (list !== null) splitTopLevel(src(list).trim().slice(1, -1)).forEach((item, index) => entries.push(`${index}: ${item.trim()}`));
|
|
297
|
+
return entries.length === 0 ? "" : `, { ${entries.join(", ")} }`;
|
|
298
|
+
} };
|
|
299
|
+
};
|
|
300
|
+
const planFile = (file, text, env) => {
|
|
301
|
+
const vue = extname(file) === ".vue";
|
|
302
|
+
const regions = vue ? vueRegions(text) : null;
|
|
303
|
+
const scripts = [];
|
|
304
|
+
if (vue) for (const m of text.matchAll(/<script\b([^>]*)>/g)) {
|
|
305
|
+
const attrs = m[1];
|
|
306
|
+
if (/\bsrc\s*=/.test(attrs)) continue;
|
|
307
|
+
const start = m.index + m[0].length;
|
|
308
|
+
const close = text.indexOf("<\/script>", start);
|
|
309
|
+
if (close === -1) continue;
|
|
310
|
+
scripts.push({
|
|
311
|
+
start,
|
|
312
|
+
end: close,
|
|
313
|
+
setup: /\bsetup\b/.test(attrs),
|
|
314
|
+
lang: /\blang\s*=\s*["']([^"']+)["']/.exec(attrs)?.[1] ?? null,
|
|
315
|
+
open: m.index
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
let code = text;
|
|
319
|
+
if (vue) {
|
|
320
|
+
code = text.replace(/[^\n]/g, " ");
|
|
321
|
+
for (const s of scripts) code = code.slice(0, s.start) + text.slice(s.start, s.end) + code.slice(s.end);
|
|
322
|
+
}
|
|
323
|
+
const lang = vue ? scripts.some((s) => s.lang === "ts" || s.lang === "tsx") ? "ts" : "js" : extname(file).includes("ts") ? "ts" : "js";
|
|
324
|
+
let program;
|
|
325
|
+
try {
|
|
326
|
+
program = parseModule(code, lang, file).program;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (!isDiagnosticError(error)) throw error;
|
|
329
|
+
env.diagnostics.push({
|
|
330
|
+
...error.diagnostic,
|
|
331
|
+
file: env.rel(file)
|
|
332
|
+
});
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const info = analyzeModule(program);
|
|
336
|
+
const outside = /* @__PURE__ */ new Map();
|
|
337
|
+
const templateStart = text.indexOf("<template");
|
|
338
|
+
const templateEnd = text.lastIndexOf("</template>");
|
|
339
|
+
const inTemplate = (offset) => regions !== null && templateStart !== -1 && offset > templateStart && offset < templateEnd && !regions.code.some((r) => r.start <= offset && offset < r.end);
|
|
340
|
+
const templateCalls = [];
|
|
341
|
+
if (vue && regions !== null) {
|
|
342
|
+
const pattern = /(?<![\w$.-])(\$tc?|tc?|\$te|\$d|\$n|\$rt)\s*\(/g;
|
|
343
|
+
for (let m = pattern.exec(text); m !== null; m = pattern.exec(text)) {
|
|
344
|
+
if (!inTemplate(m.index) || !inExpression(text, m.index, regions)) continue;
|
|
345
|
+
const callee = m[1];
|
|
346
|
+
if (callee === "t" || callee === "tc") {
|
|
347
|
+
let list = outside.get(callee);
|
|
348
|
+
if (list === void 0) outside.set(callee, list = []);
|
|
349
|
+
list.push(m.index);
|
|
350
|
+
}
|
|
351
|
+
const call = lexicalCall(text, m.index, callee, m.index + m[0].length);
|
|
352
|
+
if (call !== null) templateCalls.push(call);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const blockOf = (offset) => scripts.find((s) => s.start <= offset && offset <= s.end) ?? null;
|
|
356
|
+
let plainSites = false;
|
|
357
|
+
/**
|
|
358
|
+
* The import goes into `<script setup>`, which the template sees, unless
|
|
359
|
+
* Options API code in a plain `<script>` calls `t` too: then into that
|
|
360
|
+
* block, whose imports the template sees beside a `<script setup>` (one
|
|
361
|
+
* is added, empty, when the component has none).
|
|
362
|
+
*/
|
|
363
|
+
const placeImport = vue ? (line, removed) => {
|
|
364
|
+
const setup = scripts.find((s) => s.setup);
|
|
365
|
+
const plain = scripts.find((s) => !s.setup);
|
|
366
|
+
const target = plainSites && plain !== void 0 ? plain : setup ?? null;
|
|
367
|
+
if (target === null) return [importLineEdit(file, text, line, null)];
|
|
368
|
+
const lastImport = info.imports.filter((d) => d.node.start >= target.start && d.node.end <= target.end && !removed.has(d.node)).pop();
|
|
369
|
+
const at = lastImport === void 0 ? target.start : lastImport.node.end;
|
|
370
|
+
const edits = [{
|
|
371
|
+
start: at,
|
|
372
|
+
end: at,
|
|
373
|
+
text: `\n${line}`
|
|
374
|
+
}];
|
|
375
|
+
if (target === plain && setup === void 0 && templateCalls.length > 0) {
|
|
376
|
+
const close = text.indexOf("<\/script>", plain.end) + 9;
|
|
377
|
+
const langAttr = plain.lang === null ? "" : ` lang="${plain.lang}"`;
|
|
378
|
+
edits.push({
|
|
379
|
+
start: close,
|
|
380
|
+
end: close,
|
|
381
|
+
text: `\n<script setup${langAttr}><\/script>`
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return edits;
|
|
385
|
+
} : void 0;
|
|
386
|
+
const builder = new ModuleBuilder({
|
|
387
|
+
file,
|
|
388
|
+
text,
|
|
389
|
+
info,
|
|
390
|
+
isOldSource,
|
|
391
|
+
outside,
|
|
392
|
+
...placeImport === void 0 ? {} : { placeImport }
|
|
393
|
+
});
|
|
394
|
+
const useI18n = /* @__PURE__ */ new Set();
|
|
395
|
+
for (const declaration of info.imports) {
|
|
396
|
+
if (!isOldSource(declaration.source)) continue;
|
|
397
|
+
for (const s of declaration.specifiers) if (s.imported === "useI18n") useI18n.add(s.local);
|
|
398
|
+
}
|
|
399
|
+
const tBindings = /* @__PURE__ */ new Set();
|
|
400
|
+
for (const binding of info.bindings) {
|
|
401
|
+
const init = binding.declarator?.init;
|
|
402
|
+
if (init === null || init === void 0) continue;
|
|
403
|
+
const call = unwrap(init);
|
|
404
|
+
const callee = call.type === "CallExpression" ? identifierName(call.callee) : null;
|
|
405
|
+
if (callee === null || !useI18n.has(callee)) continue;
|
|
406
|
+
builder.hooks.push(binding);
|
|
407
|
+
const slot = binding.slot;
|
|
408
|
+
const name = slot !== null && slot.type === "Property" ? slot.key.name : null;
|
|
409
|
+
if (name === "t" || name === "tc") tBindings.add(binding);
|
|
410
|
+
const options = call.arguments[0];
|
|
411
|
+
if (options !== void 0 && objectProperties(options)?.properties.has("messages") === true) builder.note(call.start, "useI18n({ messages }) holds component-local messages, which were not migrated: move them into the locale catalogs");
|
|
412
|
+
}
|
|
413
|
+
if (vue && templateStart !== -1) for (const binding of builder.hooks) {
|
|
414
|
+
const pattern = new RegExp(String.raw`(?<![\w$.])${binding.name.replace(/\$/g, "$")}(?![\w$])`, "g");
|
|
415
|
+
for (const m of text.slice(templateStart, templateEnd).matchAll(pattern)) {
|
|
416
|
+
const offset = templateStart + m.index;
|
|
417
|
+
let list = outside.get(binding.name);
|
|
418
|
+
if (list === void 0) outside.set(binding.name, list = []);
|
|
419
|
+
if (!list.includes(offset)) list.push(offset);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const resolve = (name, offset) => {
|
|
423
|
+
let best = null;
|
|
424
|
+
for (const b of info.bindings) {
|
|
425
|
+
if (b.name !== name || offset < b.scope.start || offset >= b.scope.end) continue;
|
|
426
|
+
if (best === null || b.scope.end - b.scope.start < best.scope.end - best.scope.start) best = b;
|
|
427
|
+
}
|
|
428
|
+
return best;
|
|
429
|
+
};
|
|
430
|
+
/** One `t`-family call: rewrite, note, or nothing when it is not vue-i18n's. */
|
|
431
|
+
const site = (call, known, consume, markup) => {
|
|
432
|
+
const callee = call.callee.replace(/^\$/, "");
|
|
433
|
+
if (callee === "te" || callee === "d" || callee === "n" || callee === "rt") {
|
|
434
|
+
if (known || call.callee.startsWith("$")) {
|
|
435
|
+
const hint = callee === "d" ? "use fmt.date from @menapo/vi18n" : callee === "n" ? "use fmt.number from @menapo/vi18n" : callee === "te" ? "vi18n needs no existence check, since every message the code names exists" : "translate the text with t() where it is known";
|
|
436
|
+
builder.note(call.start, `${call.callee}() is not rewritten: ${hint}`);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const first = call.args[0];
|
|
441
|
+
if (first === void 0) return;
|
|
442
|
+
const firstCode = text.slice(first.start, first.end).trim();
|
|
443
|
+
const literal = /^["']/.test(firstCode) ? readString(firstCode, 0) : null;
|
|
444
|
+
let key = literal !== null && literal.end === firstCode.length ? literal.value : null;
|
|
445
|
+
if (key === null && firstCode.startsWith("`")) {
|
|
446
|
+
const t = readTemplate(firstCode, 0);
|
|
447
|
+
if (t !== null && t.quasis.length === 1 && t.end === firstCode.length) key = t.quasis[0];
|
|
448
|
+
}
|
|
449
|
+
if (key === null) {
|
|
450
|
+
const keys = keysIn(firstCode);
|
|
451
|
+
for (const k of keys) define(env, k, null);
|
|
452
|
+
if (keys.length > 0 || known) builder.note(call.start, `${call.callee}(${firstCode}) uses a computed key and was not rewritten`, keys.filter((k) => env.message(k) !== void 0));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const rest = call.args.slice(1).map((a) => {
|
|
456
|
+
const argCode = text.slice(a.start, a.end).trim();
|
|
457
|
+
return {
|
|
458
|
+
code: argCode,
|
|
459
|
+
kind: argCode.startsWith("{") ? "object" : argCode.startsWith("[") ? "array" : /^["'`]/.test(argCode) ? "string" : "other",
|
|
460
|
+
range: a
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
const fallback = rest.find((a) => a.kind === "string");
|
|
464
|
+
const fallbackText = fallback === void 0 ? null : readString(fallback.code, 0)?.value ?? null;
|
|
465
|
+
if (textOf(sourceLocale, key) === void 0 && fallbackText === null) {
|
|
466
|
+
if (known) builder.note(call.start, `${call.callee}(${JSON.stringify(key)}): the key is in no ${sourceLocale} catalog; not rewritten`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const defined = define(env, key, fallbackText);
|
|
470
|
+
if ("problem" in defined) {
|
|
471
|
+
builder.note(call.start, `${defined.problem}; ${call.callee}() was not rewritten`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const values = callValues(env, key, rest.filter((a) => a.kind !== "string"), callee === "tc");
|
|
475
|
+
if ("problem" in values) {
|
|
476
|
+
builder.note(call.start, `${call.callee}(${JSON.stringify(key)}, …) passes ${values.problem}; not rewritten`, [defined.key]);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const argRanges = rest.filter((a) => a.kind !== "string").map((a) => a.range);
|
|
480
|
+
for (const offset of consume.offsets) builder.consumed.add(offset);
|
|
481
|
+
if (blockOf(call.start)?.setup === false) plainSites = true;
|
|
482
|
+
builder.site(call, (r) => {
|
|
483
|
+
const src = (index) => {
|
|
484
|
+
const range = argRanges[index];
|
|
485
|
+
return r.src(range.start, range.end);
|
|
486
|
+
};
|
|
487
|
+
return `${r.fn.t}(${r.message(defined.key)}${values.values(src)})`;
|
|
488
|
+
}, {
|
|
489
|
+
keys: [defined.key],
|
|
490
|
+
uses: ["t"],
|
|
491
|
+
consume: consume.nodes,
|
|
492
|
+
...markup === null ? {} : { markup }
|
|
493
|
+
});
|
|
494
|
+
};
|
|
495
|
+
const visit = (node) => {
|
|
496
|
+
if (node.type === "CallExpression") {
|
|
497
|
+
const callee = unwrap(node.callee);
|
|
498
|
+
const args = node.arguments.map((a) => ({
|
|
499
|
+
start: a.start,
|
|
500
|
+
end: a.end
|
|
501
|
+
}));
|
|
502
|
+
let name = null;
|
|
503
|
+
let known = false;
|
|
504
|
+
let consume = [];
|
|
505
|
+
if (callee.type === "Identifier") {
|
|
506
|
+
const binding = resolve(callee.name, callee.start);
|
|
507
|
+
if (binding !== null && tBindings.has(binding)) {
|
|
508
|
+
name = callee.name;
|
|
509
|
+
known = true;
|
|
510
|
+
consume = [callee];
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
const member = memberParts(callee);
|
|
514
|
+
if (member !== null && /^\$?(?:tc?|te|d|n|rt)$/.test(member.property)) {
|
|
515
|
+
const object = unwrap(member.object);
|
|
516
|
+
const thisCall = object.type === "ThisExpression" && member.property.startsWith("$");
|
|
517
|
+
const global = object.type === "MemberExpression" && memberParts(object)?.property === "global";
|
|
518
|
+
if (thisCall || global || member.property === "t" || member.property === "tc") {
|
|
519
|
+
name = member.property.startsWith("$") || thisCall || global ? member.property : member.property;
|
|
520
|
+
known = thisCall || global;
|
|
521
|
+
let root = object;
|
|
522
|
+
while (root.type === "MemberExpression") root = unwrap(root.object);
|
|
523
|
+
if (root.type === "Identifier") consume = [root];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (name !== null && node.arguments.every((a) => a.type !== "SpreadElement")) site({
|
|
528
|
+
callee: name,
|
|
529
|
+
calleeStart: callee.start,
|
|
530
|
+
start: node.start,
|
|
531
|
+
end: node.end,
|
|
532
|
+
args
|
|
533
|
+
}, known, {
|
|
534
|
+
nodes: consume,
|
|
535
|
+
offsets: []
|
|
536
|
+
}, null);
|
|
537
|
+
const member = memberParts(callee);
|
|
538
|
+
const calleeName = identifierName(callee);
|
|
539
|
+
if (calleeName === "createI18n" || member?.property === "use" && node.arguments.some((a) => /^i18n$/i.test(identifierName(a) ?? ""))) builder.note(node.start, calleeName === "createI18n" ? "createI18n(): delete this setup once nothing uses vue-i18n; vi18n needs only its Vite plugin and vi18n.config.ts" : "app.use(i18n): remove it with the vue-i18n setup");
|
|
540
|
+
}
|
|
541
|
+
for (const [, child] of childEntries(node)) visit(child);
|
|
542
|
+
};
|
|
543
|
+
visit(program);
|
|
544
|
+
if (vue && regions !== null) {
|
|
545
|
+
for (const call of templateCalls) {
|
|
546
|
+
const bare = call.callee === "t" || call.callee === "tc";
|
|
547
|
+
if (bare) {
|
|
548
|
+
const binding = resolve(call.callee, scripts.find((s) => s.setup)?.start ?? 0);
|
|
549
|
+
if (binding === null || !tBindings.has(binding)) {
|
|
550
|
+
if (binding !== null) continue;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const markup = { quote: regions.attributes.find((a) => a.start <= call.start && call.start < a.end)?.quote ?? null };
|
|
554
|
+
site(call, true, {
|
|
555
|
+
nodes: [],
|
|
556
|
+
offsets: bare ? [call.calleeStart] : []
|
|
557
|
+
}, markup);
|
|
558
|
+
}
|
|
559
|
+
for (const m of text.matchAll(/<(i18n-t|i18n|I18nT|i18n-n|i18n-d)\b([^>]*)>/g)) {
|
|
560
|
+
if (!inTemplate(m.index)) continue;
|
|
561
|
+
const attrs = m[2];
|
|
562
|
+
const keypath = /\b(?:keypath|path)\s*=\s*"([^"]*)"|\b(?:keypath|path)\s*=\s*'([^']*)'/.exec(attrs);
|
|
563
|
+
const key = keypath?.[1] ?? keypath?.[2] ?? null;
|
|
564
|
+
const keys = [];
|
|
565
|
+
if (key !== null && "key" in define(env, key, null)) keys.push(key);
|
|
566
|
+
builder.note(m.index, `<${m[1]}> is not rewritten: render rich() from @menapo/vi18n in a component (see the rich text guide)${key === null ? "" : ""}`, keys);
|
|
567
|
+
}
|
|
568
|
+
for (const m of text.matchAll(/\sv-t\s*=\s*("[^"]*"|'[^']*')/g)) {
|
|
569
|
+
if (!inTemplate(m.index)) continue;
|
|
570
|
+
const value = m[1].slice(1, -1).trim();
|
|
571
|
+
const literal = /^'([^']*)'$|^"([^"]*)"$/.exec(value);
|
|
572
|
+
const key = literal?.[1] ?? literal?.[2] ?? null;
|
|
573
|
+
const keys = key !== null && "key" in define(env, key, null) ? [key] : [];
|
|
574
|
+
builder.note(m.index + 1, "v-t is not rewritten: write {{ t(…) }} with the message", keys);
|
|
575
|
+
}
|
|
576
|
+
for (const m of text.matchAll(/^<i18n\b[^>]*>/gm)) {
|
|
577
|
+
if (inTemplate(m.index)) continue;
|
|
578
|
+
builder.note(m.index, "an <i18n> block holds component-local messages, which were not migrated: move them into the locale catalogs");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return builder.finish();
|
|
582
|
+
};
|
|
583
|
+
return {
|
|
584
|
+
catalogExtensions: [
|
|
585
|
+
".json",
|
|
586
|
+
".yaml",
|
|
587
|
+
".yml"
|
|
588
|
+
],
|
|
589
|
+
catalogsRequired: true,
|
|
590
|
+
extensions: /* @__PURE__ */ new Set([...MODULE_EXTENSIONS, ".vue"]),
|
|
591
|
+
mentions: (text) => /vue-i18n|\$tc?\s*\(|\bt\s*\(|i18n/.test(text),
|
|
592
|
+
load(files, env) {
|
|
593
|
+
sourceLocale = env.config.sourceLocale;
|
|
594
|
+
for (const file of files) readCatalog(file, env);
|
|
595
|
+
},
|
|
596
|
+
planFile,
|
|
597
|
+
unusedKeys() {
|
|
598
|
+
return [...catalogs.get(sourceLocale)?.keys() ?? []].filter((key) => !referenced.has(key));
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
/** Whether a template offset is inside `{{ }}` or the value of a bound attribute or directive. */
|
|
603
|
+
function inExpression(text, offset, regions) {
|
|
604
|
+
const attribute = regions.attributes.find((a) => a.start <= offset && offset < a.end);
|
|
605
|
+
if (attribute !== void 0) {
|
|
606
|
+
const name = /([^\s"'<>/=]+)\s*=\s*$/.exec(text.slice(Math.max(0, attribute.start - 200), attribute.start - 1))?.[1];
|
|
607
|
+
return name !== void 0 && /^(?::|@|#|v-)/.test(name);
|
|
608
|
+
}
|
|
609
|
+
const open = text.lastIndexOf("{{", offset);
|
|
610
|
+
if (open === -1) return false;
|
|
611
|
+
return text.lastIndexOf("}}", offset) < open;
|
|
612
|
+
}
|
|
613
|
+
/** A call at `start` whose `(` ends at `open`: argument ranges as written. */
|
|
614
|
+
function lexicalCall(text, start, callee, open) {
|
|
615
|
+
const close = skipBalanced(text, open - 1);
|
|
616
|
+
if (close === null) return null;
|
|
617
|
+
const args = [];
|
|
618
|
+
let at = skipSpace(text, open);
|
|
619
|
+
while (at < close - 1) {
|
|
620
|
+
const end = argumentEnd(text, at);
|
|
621
|
+
if (end === null || end > close - 1) return null;
|
|
622
|
+
args.push({
|
|
623
|
+
start: at,
|
|
624
|
+
end
|
|
625
|
+
});
|
|
626
|
+
at = skipSpace(text, end + 1);
|
|
627
|
+
if (text[end] === ")") break;
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
callee,
|
|
631
|
+
calleeStart: start,
|
|
632
|
+
start,
|
|
633
|
+
end: close,
|
|
634
|
+
args
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/** Splits `a, b, c` at top-level commas. */
|
|
638
|
+
function splitTopLevel(code) {
|
|
639
|
+
const out = [];
|
|
640
|
+
let at = 0;
|
|
641
|
+
while (at < code.length) {
|
|
642
|
+
const end = argumentEnd(code + ")", at);
|
|
643
|
+
if (end === null) break;
|
|
644
|
+
const part = code.slice(at, end).trim();
|
|
645
|
+
if (part !== "") out.push(part);
|
|
646
|
+
at = end + 1;
|
|
647
|
+
}
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
export { createVueI18nAdapter };
|