@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,1596 @@
|
|
|
1
|
+
import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
|
|
2
|
+
import { t as pluralCategories } from "./validate-BxpupdAf.js";
|
|
3
|
+
import { n as printICU } from "./codec-DuvU2VQy.js";
|
|
4
|
+
import { l as langFromId, u as parseModule } from "./worker-84U6uYB3.js";
|
|
5
|
+
import { _ as keyName, a as attributeExpression, b as stringValue, c as cleanJsxText, d as isNode, f as jsString, g as jsxNameNodes, h as jsxName, i as analyzeModule, l as identifierName, m as jsxChildren, o as attributeString, p as jsxAttributes, s as childEntries, t as ModuleBuilder, u as inJsxChildren, v as memberParts, x as unwrap, y as objectProperties } from "./rewrite-EsIw_lsq.js";
|
|
6
|
+
import { extname } from "node:path";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
//#region src/cli/commands/migrate/formats.ts
|
|
10
|
+
/**
|
|
11
|
+
* Catalog formats of the libraries `vi18n migrate` converts from (INTERNALS
|
|
12
|
+
* §2.14): nested JSON flattened to keys, a YAML subset for vue-i18n (vi18n
|
|
13
|
+
* depends on no YAML parser, §6 D1), FormatJS's compiled message AST, and the
|
|
14
|
+
* id schemes of FormatJS and Lingui, so translations keyed by a generated id
|
|
15
|
+
* are found from the message text a call site holds. Every reader reports
|
|
16
|
+
* what it cannot read instead of guessing; nothing here executes a catalog.
|
|
17
|
+
*/
|
|
18
|
+
/** 1-based line of an offset. */
|
|
19
|
+
function lineAt(text, offset) {
|
|
20
|
+
let line = 1;
|
|
21
|
+
for (let i = text.indexOf("\n"); i !== -1 && i < offset; i = text.indexOf("\n", i + 1)) line++;
|
|
22
|
+
return line;
|
|
23
|
+
}
|
|
24
|
+
/** `JSON.parse`, with the position of a syntax error turned into a line. */
|
|
25
|
+
function parseJson(text) {
|
|
26
|
+
const body = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
27
|
+
try {
|
|
28
|
+
return {
|
|
29
|
+
value: JSON.parse(body),
|
|
30
|
+
problem: null
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
const at = /position (\d+)/.exec(message);
|
|
35
|
+
return {
|
|
36
|
+
value: void 0,
|
|
37
|
+
problem: {
|
|
38
|
+
message,
|
|
39
|
+
line: at === null ? 0 : lineAt(body, Number(at[1]))
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The line of each key's first occurrence in a JSON text (`"key":`), for diagnostics. */
|
|
45
|
+
function keyLines(text) {
|
|
46
|
+
const lines = /* @__PURE__ */ new Map();
|
|
47
|
+
const pattern = /"((?:[^"\\\n]|\\.)*)"\s*:/g;
|
|
48
|
+
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
49
|
+
let key;
|
|
50
|
+
try {
|
|
51
|
+
key = JSON.parse(`"${match[1]}"`);
|
|
52
|
+
} catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!lines.has(key)) lines.set(key, lineAt(text, match.index));
|
|
56
|
+
}
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Flattens a nested catalog: objects join their keys with `separator`,
|
|
61
|
+
* arrays use indexes (`list.0`), as i18next and vue-i18n address them.
|
|
62
|
+
* Leaves that are not strings are reported, never stringified.
|
|
63
|
+
*/
|
|
64
|
+
function flattenTree(tree, text, separator = ".") {
|
|
65
|
+
const texts = /* @__PURE__ */ new Map();
|
|
66
|
+
const problems = [];
|
|
67
|
+
const lines = keyLines(text);
|
|
68
|
+
const lineOf = (key) => {
|
|
69
|
+
const last = key.lastIndexOf(separator);
|
|
70
|
+
return lines.get(key) ?? lines.get(last === -1 ? key : key.slice(last + 1)) ?? 0;
|
|
71
|
+
};
|
|
72
|
+
const visit = (value, key) => {
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
texts.set(key, {
|
|
75
|
+
value,
|
|
76
|
+
line: lineOf(key)
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
value.forEach((item, index) => visit(item, key === "" ? String(index) : `${key}${separator}${index}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "object" && value !== null) {
|
|
85
|
+
for (const [name, item] of Object.entries(value)) visit(item, key === "" ? name : `${key}${separator}${name}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (value === null) return;
|
|
89
|
+
problems.push({
|
|
90
|
+
message: `"${key}" holds ${typeof value} ${JSON.stringify(value)}, not a string; it was skipped`,
|
|
91
|
+
line: lineOf(key)
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
if (typeof tree !== "object" || tree === null || Array.isArray(tree)) {
|
|
95
|
+
problems.push({
|
|
96
|
+
message: "a catalog is an object of messages",
|
|
97
|
+
line: 1
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
texts,
|
|
101
|
+
problems
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
visit(tree, "");
|
|
105
|
+
return {
|
|
106
|
+
texts,
|
|
107
|
+
problems
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
var YamlError = class extends Error {
|
|
111
|
+
line;
|
|
112
|
+
constructor(message, line) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.line = line;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Block mappings and sequences of scalars: plain, quoted (single and double,
|
|
119
|
+
* also over several lines) and block scalars (`|`, `>`, with chomping and an
|
|
120
|
+
* indentation indicator); comments; `{}` and `[]`. Anchors, aliases, tags,
|
|
121
|
+
* non-empty flow collections and complex keys are errors, since guessing
|
|
122
|
+
* would change a message silently. Plain scalars stay strings (`404`, `yes`):
|
|
123
|
+
* a message catalog has nothing else.
|
|
124
|
+
*/
|
|
125
|
+
function parseYaml(text) {
|
|
126
|
+
const lines = (text.charCodeAt(0) === 65279 ? text.slice(1) : text).split("\n").map((raw, index) => {
|
|
127
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
128
|
+
const indent = /^ */.exec(line)?.[0].length ?? 0;
|
|
129
|
+
return {
|
|
130
|
+
no: index + 1,
|
|
131
|
+
indent,
|
|
132
|
+
text: line.slice(indent),
|
|
133
|
+
raw: line
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
let at = 0;
|
|
137
|
+
const blank = (line) => line.text === "" || line.text.startsWith("#");
|
|
138
|
+
const skipBlank = () => {
|
|
139
|
+
while (at < lines.length && blank(lines[at])) at++;
|
|
140
|
+
};
|
|
141
|
+
skipBlank();
|
|
142
|
+
if (at < lines.length && /^---\s*(?:#.*)?$/.test(lines[at].raw)) {
|
|
143
|
+
at++;
|
|
144
|
+
skipBlank();
|
|
145
|
+
}
|
|
146
|
+
const fail = (message, line) => {
|
|
147
|
+
throw new YamlError(message, line?.no ?? 0);
|
|
148
|
+
};
|
|
149
|
+
/** A plain scalar's text with a trailing ` #comment` removed. */
|
|
150
|
+
const plain = (value) => {
|
|
151
|
+
const comment = /(?:^|\s)#/.exec(value);
|
|
152
|
+
return (comment === null ? value : value.slice(0, comment.index)).trim();
|
|
153
|
+
};
|
|
154
|
+
const checkScalarStart = (value, line) => {
|
|
155
|
+
if (/^[&*!]/.test(value)) fail(`anchors, aliases and tags are not supported (${value.split(/\s/)[0]})`, line);
|
|
156
|
+
if (/^[[{]/.test(value) && !/^(?:\{\s*\}|\[\s*\])\s*(?:#.*)?$/.test(value)) fail("flow collections are not supported: write the entries as a block", line);
|
|
157
|
+
if (/^[|>]/.test(value) === false && /^[?%@`]/.test(value)) fail(`a scalar cannot start with ${value[0]}: quote it`, line);
|
|
158
|
+
};
|
|
159
|
+
/** A quoted scalar starting at `value` on `line`, possibly continued on later lines; folds line breaks. */
|
|
160
|
+
const quoted = (value, line) => {
|
|
161
|
+
const quote = value[0];
|
|
162
|
+
let source = value.slice(1);
|
|
163
|
+
let out = "";
|
|
164
|
+
let current = line;
|
|
165
|
+
for (;;) {
|
|
166
|
+
for (let i = 0; i < source.length; i++) {
|
|
167
|
+
const c = source[i];
|
|
168
|
+
if (quote === "'" && c === "'") {
|
|
169
|
+
if (source[i + 1] === "'") {
|
|
170
|
+
out += "'";
|
|
171
|
+
i++;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
text: out,
|
|
176
|
+
rest: source.slice(i + 1)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (quote === "\"" && c === "\"") return {
|
|
180
|
+
text: out,
|
|
181
|
+
rest: source.slice(i + 1)
|
|
182
|
+
};
|
|
183
|
+
if (quote === "\"" && c === "\\") {
|
|
184
|
+
const next = source[i + 1];
|
|
185
|
+
if (next === void 0) {
|
|
186
|
+
source = "";
|
|
187
|
+
out += "\0";
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
const simple = {
|
|
191
|
+
n: "\n",
|
|
192
|
+
t: " ",
|
|
193
|
+
r: "\r",
|
|
194
|
+
"0": "\0",
|
|
195
|
+
"\"": "\"",
|
|
196
|
+
"\\": "\\",
|
|
197
|
+
"/": "/",
|
|
198
|
+
" ": " ",
|
|
199
|
+
b: "\b",
|
|
200
|
+
f: "\f",
|
|
201
|
+
e: "\x1B",
|
|
202
|
+
N: "
",
|
|
203
|
+
_: "\xA0"
|
|
204
|
+
};
|
|
205
|
+
if (next in simple) {
|
|
206
|
+
out += simple[next];
|
|
207
|
+
i++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const width = next === "x" ? 2 : next === "u" ? 4 : next === "U" ? 8 : 0;
|
|
211
|
+
if (width === 0) fail(`unknown escape \\${next}`, current);
|
|
212
|
+
const hex = source.slice(i + 2, i + 2 + width);
|
|
213
|
+
if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(hex)) fail(`malformed escape \\${next}${hex}`, current);
|
|
214
|
+
out += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
215
|
+
i += 1 + width;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
out += c;
|
|
219
|
+
}
|
|
220
|
+
at++;
|
|
221
|
+
if (at >= lines.length) fail("unterminated quoted scalar", current);
|
|
222
|
+
let breaks = 0;
|
|
223
|
+
while (at < lines.length && lines[at].text === "") {
|
|
224
|
+
breaks++;
|
|
225
|
+
at++;
|
|
226
|
+
}
|
|
227
|
+
if (at >= lines.length) fail("unterminated quoted scalar", current);
|
|
228
|
+
current = lines[at];
|
|
229
|
+
if (out.endsWith("\0")) out = out.slice(0, -1);
|
|
230
|
+
else {
|
|
231
|
+
out = out.replace(/[ \t]+$/, "");
|
|
232
|
+
out += breaks > 0 ? "\n".repeat(breaks) : " ";
|
|
233
|
+
}
|
|
234
|
+
source = current.text.replace(/^\s+/, "");
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
/** A block scalar whose header (`|`, `>-`, `|2`…) is `header`; the content follows `line`. */
|
|
238
|
+
const blockScalar = (header, parentIndent, line) => {
|
|
239
|
+
const m = /^([|>])([+-]?)([1-9]?)([+-]?)\s*(?:#.*)?$/.exec(header);
|
|
240
|
+
if (m === null) fail(`malformed block scalar header ${header}`, line);
|
|
241
|
+
const [, style, chompA, digit, chompB] = m;
|
|
242
|
+
const chomp = chompA || chompB;
|
|
243
|
+
at++;
|
|
244
|
+
let indent = digit === "" ? -1 : parentIndent + Number(digit);
|
|
245
|
+
const content = [];
|
|
246
|
+
while (at < lines.length) {
|
|
247
|
+
const current = lines[at];
|
|
248
|
+
if (current.raw.trim() === "") {
|
|
249
|
+
content.push("");
|
|
250
|
+
at++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (indent === -1) {
|
|
254
|
+
if (current.indent <= parentIndent) break;
|
|
255
|
+
indent = current.indent;
|
|
256
|
+
}
|
|
257
|
+
if (current.indent < indent) break;
|
|
258
|
+
content.push(current.raw.slice(indent));
|
|
259
|
+
at++;
|
|
260
|
+
}
|
|
261
|
+
let trailing = 0;
|
|
262
|
+
while (content.length > 0 && content[content.length - 1] === "") {
|
|
263
|
+
content.pop();
|
|
264
|
+
trailing++;
|
|
265
|
+
}
|
|
266
|
+
let value;
|
|
267
|
+
if (style === "|") value = content.join("\n");
|
|
268
|
+
else {
|
|
269
|
+
value = "";
|
|
270
|
+
for (let i = 0; i < content.length; i++) {
|
|
271
|
+
const piece = content[i];
|
|
272
|
+
if (i > 0) {
|
|
273
|
+
const previous = content[i - 1];
|
|
274
|
+
if (piece === "") value += "\n";
|
|
275
|
+
else if (previous !== "") value += /^\s/.test(piece) || /^\s/.test(previous) ? "\n" : " ";
|
|
276
|
+
}
|
|
277
|
+
value += piece;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (content.length === 0) return "";
|
|
281
|
+
if (chomp === "-") return value;
|
|
282
|
+
if (chomp === "+") return value + "\n".repeat(trailing + 1);
|
|
283
|
+
return value + "\n";
|
|
284
|
+
};
|
|
285
|
+
/** A plain scalar starting on `line`, with its continuation lines (more indented than `parentIndent`). */
|
|
286
|
+
const plainScalar = (first, parentIndent) => {
|
|
287
|
+
let value = plain(first);
|
|
288
|
+
if (/(?:^|\s)#/.test(first)) {
|
|
289
|
+
at++;
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
at++;
|
|
293
|
+
let breaks = 0;
|
|
294
|
+
while (at < lines.length) {
|
|
295
|
+
const current = lines[at];
|
|
296
|
+
if (current.text === "") {
|
|
297
|
+
breaks++;
|
|
298
|
+
at++;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (current.indent <= parentIndent || current.text.startsWith("#")) break;
|
|
302
|
+
if (/^[^"'#][^:#]*:(?:\s|$)/.test(current.text) || current.text.startsWith("- ")) break;
|
|
303
|
+
value += breaks > 0 ? "\n".repeat(breaks) : " ";
|
|
304
|
+
breaks = 0;
|
|
305
|
+
value += plain(current.text);
|
|
306
|
+
const ends = /(?:^|\s)#/.test(current.text);
|
|
307
|
+
at++;
|
|
308
|
+
if (ends) break;
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
};
|
|
312
|
+
/** The value after `key:` or `- ` (`rest`), at a node whose own indentation is `indent`. */
|
|
313
|
+
const valueAfter = (rest, indent, line) => {
|
|
314
|
+
const value = rest.trim();
|
|
315
|
+
if (value === "" || value.startsWith("#")) {
|
|
316
|
+
at++;
|
|
317
|
+
skipBlank();
|
|
318
|
+
const next = lines[at];
|
|
319
|
+
if (next === void 0 || next.indent <= indent) {
|
|
320
|
+
if (next !== void 0 && next.indent === indent && next.text.startsWith("- ")) return node(indent);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return node(next.indent);
|
|
324
|
+
}
|
|
325
|
+
checkScalarStart(value, line);
|
|
326
|
+
if (value === "{}" || /^\{\s*\}/.test(value)) {
|
|
327
|
+
at++;
|
|
328
|
+
return {};
|
|
329
|
+
}
|
|
330
|
+
if (/^\[\s*\]/.test(value)) {
|
|
331
|
+
at++;
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
if (value.startsWith("|") || value.startsWith(">")) return blockScalar(value, indent, line);
|
|
335
|
+
if (value.startsWith("\"") || value.startsWith("'")) {
|
|
336
|
+
const result = quoted(value, line);
|
|
337
|
+
if (plain(result.rest) !== "") fail(`unexpected text after a quoted scalar: ${result.rest.trim()}`, lines[at]);
|
|
338
|
+
at++;
|
|
339
|
+
return result.text;
|
|
340
|
+
}
|
|
341
|
+
if (value === "~" || value === "null") {
|
|
342
|
+
at++;
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return plainScalar(value, indent);
|
|
346
|
+
};
|
|
347
|
+
/** A mapping key at the start of `text`: its name and what follows the `:`. */
|
|
348
|
+
const keyOf = (text, line) => {
|
|
349
|
+
if (text.startsWith("\"") || text.startsWith("'")) {
|
|
350
|
+
const saved = at;
|
|
351
|
+
const result = quoted(text, line);
|
|
352
|
+
if (at !== saved) fail("a key cannot span lines", line);
|
|
353
|
+
const rest = result.rest.replace(/^\s*/, "");
|
|
354
|
+
if (!rest.startsWith(":")) return null;
|
|
355
|
+
return {
|
|
356
|
+
key: result.text,
|
|
357
|
+
rest: rest.slice(1)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (text.startsWith("? ")) fail("complex keys (? …) are not supported", line);
|
|
361
|
+
const m = /^([^#][^:]*?)\s*:(?:\s|$)/.exec(text);
|
|
362
|
+
if (m === null) return null;
|
|
363
|
+
return {
|
|
364
|
+
key: m[1],
|
|
365
|
+
rest: text.slice(m[0].length)
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
/** A block node whose lines are indented by exactly `indent`. */
|
|
369
|
+
const node = (indent) => {
|
|
370
|
+
skipBlank();
|
|
371
|
+
const first = lines[at];
|
|
372
|
+
if (first === void 0) return null;
|
|
373
|
+
if (first.text.startsWith("- ") || first.text === "-") {
|
|
374
|
+
const out = [];
|
|
375
|
+
while (at < lines.length) {
|
|
376
|
+
skipBlank();
|
|
377
|
+
const line = lines[at];
|
|
378
|
+
if (line === void 0 || line.indent < indent) break;
|
|
379
|
+
if (line.indent > indent) fail("unexpected indentation", line);
|
|
380
|
+
if (!(line.text.startsWith("- ") || line.text === "-")) break;
|
|
381
|
+
const rest = line.text.slice(1);
|
|
382
|
+
const inner = rest.trimStart();
|
|
383
|
+
if (inner !== "" && !/^["'|>]/.test(inner) && keyOf(inner, line) !== null) {
|
|
384
|
+
const shift = line.indent + 1 + (rest.length - inner.length);
|
|
385
|
+
lines[at] = {
|
|
386
|
+
...line,
|
|
387
|
+
indent: shift,
|
|
388
|
+
text: inner
|
|
389
|
+
};
|
|
390
|
+
out.push(node(shift));
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
out.push(valueAfter(rest, indent, line));
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
const out = {};
|
|
398
|
+
while (at < lines.length) {
|
|
399
|
+
skipBlank();
|
|
400
|
+
const line = lines[at];
|
|
401
|
+
if (line === void 0 || line.indent < indent) break;
|
|
402
|
+
if (line.indent > indent) fail("unexpected indentation", line);
|
|
403
|
+
if (line.text === "..." || line.text === "---") fail("only one YAML document is supported", line);
|
|
404
|
+
const entry = keyOf(line.text, line);
|
|
405
|
+
if (entry === null) fail(`expected "key: value", found ${line.text}`, line);
|
|
406
|
+
const { key, rest } = entry;
|
|
407
|
+
if (Object.hasOwn(out, key)) fail(`duplicate key ${key}`, line);
|
|
408
|
+
out[key] = valueAfter(rest, indent, line);
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
};
|
|
412
|
+
try {
|
|
413
|
+
skipBlank();
|
|
414
|
+
if (at >= lines.length) return {
|
|
415
|
+
value: {},
|
|
416
|
+
problem: null
|
|
417
|
+
};
|
|
418
|
+
const value = node(lines[at].indent);
|
|
419
|
+
skipBlank();
|
|
420
|
+
if (at < lines.length) fail("unexpected indentation", lines[at]);
|
|
421
|
+
return {
|
|
422
|
+
value,
|
|
423
|
+
problem: null
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (error instanceof YamlError) return {
|
|
427
|
+
value: void 0,
|
|
428
|
+
problem: {
|
|
429
|
+
message: error.message,
|
|
430
|
+
line: error.line
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/** Line numbers of YAML keys (`key:` at any depth), for diagnostics. */
|
|
437
|
+
function yamlKeyLines(text) {
|
|
438
|
+
const lines = /* @__PURE__ */ new Map();
|
|
439
|
+
text.split("\n").forEach((line, index) => {
|
|
440
|
+
const m = /^\s*(?:-\s+)?["']?([^"':#]+?)["']?\s*:(?:\s|$)/.exec(line);
|
|
441
|
+
if (m !== null && !lines.has(m[1])) lines.set(m[1], index + 1);
|
|
442
|
+
});
|
|
443
|
+
return lines;
|
|
444
|
+
}
|
|
445
|
+
const KEYWORD_STYLES = /* @__PURE__ */ new Set([
|
|
446
|
+
"integer",
|
|
447
|
+
"percent",
|
|
448
|
+
"short",
|
|
449
|
+
"medium",
|
|
450
|
+
"long",
|
|
451
|
+
"full"
|
|
452
|
+
]);
|
|
453
|
+
/**
|
|
454
|
+
* A message compiled by `formatjs compile --ast` (an array of typed
|
|
455
|
+
* elements) as vi18n's AST; null when an element is not one FormatJS writes.
|
|
456
|
+
*/
|
|
457
|
+
function formatjsAstToIcu(elements) {
|
|
458
|
+
if (!Array.isArray(elements)) return null;
|
|
459
|
+
const out = [];
|
|
460
|
+
for (const element of elements) {
|
|
461
|
+
if (typeof element !== "object" || element === null) return null;
|
|
462
|
+
const value = element.value;
|
|
463
|
+
switch (element.type) {
|
|
464
|
+
case 0:
|
|
465
|
+
if (typeof value !== "string") return null;
|
|
466
|
+
if (value !== "") out.push({
|
|
467
|
+
type: "literal",
|
|
468
|
+
value
|
|
469
|
+
});
|
|
470
|
+
break;
|
|
471
|
+
case 1:
|
|
472
|
+
out.push({
|
|
473
|
+
type: "argument",
|
|
474
|
+
name: value
|
|
475
|
+
});
|
|
476
|
+
break;
|
|
477
|
+
case 2:
|
|
478
|
+
case 3:
|
|
479
|
+
case 4: {
|
|
480
|
+
const style = formatjsStyle(element.style);
|
|
481
|
+
if (style === void 0) return null;
|
|
482
|
+
out.push({
|
|
483
|
+
type: element.type === 2 ? "number" : element.type === 3 ? "date" : "time",
|
|
484
|
+
name: value,
|
|
485
|
+
style,
|
|
486
|
+
options: null
|
|
487
|
+
});
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case 5:
|
|
491
|
+
case 6: {
|
|
492
|
+
const options = [];
|
|
493
|
+
const raw = element.options;
|
|
494
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
495
|
+
for (const [key, option] of Object.entries(raw)) {
|
|
496
|
+
const nested = formatjsAstToIcu(option?.value);
|
|
497
|
+
if (nested === null) return null;
|
|
498
|
+
options.push({
|
|
499
|
+
key,
|
|
500
|
+
value: nested
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (element.type === 5) out.push({
|
|
504
|
+
type: "select",
|
|
505
|
+
name: value,
|
|
506
|
+
options
|
|
507
|
+
});
|
|
508
|
+
else out.push({
|
|
509
|
+
type: "plural",
|
|
510
|
+
pluralType: element.pluralType === "ordinal" ? "ordinal" : "cardinal",
|
|
511
|
+
name: value,
|
|
512
|
+
offset: typeof element.offset === "number" ? element.offset : 0,
|
|
513
|
+
options
|
|
514
|
+
});
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case 7:
|
|
518
|
+
out.push({ type: "pound" });
|
|
519
|
+
break;
|
|
520
|
+
case 8: {
|
|
521
|
+
const children = formatjsAstToIcu(element.children);
|
|
522
|
+
if (children === null) return null;
|
|
523
|
+
out.push({
|
|
524
|
+
type: "tag",
|
|
525
|
+
name: value,
|
|
526
|
+
children
|
|
527
|
+
});
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
default: return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
function formatjsStyle(style) {
|
|
536
|
+
if (style === null || style === void 0) return null;
|
|
537
|
+
if (typeof style === "string") {
|
|
538
|
+
if (style.startsWith("::")) return {
|
|
539
|
+
kind: "skeleton",
|
|
540
|
+
value: style.slice(2).trim()
|
|
541
|
+
};
|
|
542
|
+
return KEYWORD_STYLES.has(style) ? {
|
|
543
|
+
kind: "keyword",
|
|
544
|
+
value: style
|
|
545
|
+
} : void 0;
|
|
546
|
+
}
|
|
547
|
+
if (typeof style !== "object") return void 0;
|
|
548
|
+
const skeleton = style;
|
|
549
|
+
if (typeof skeleton.pattern === "string") return {
|
|
550
|
+
kind: "skeleton",
|
|
551
|
+
value: skeleton.pattern
|
|
552
|
+
};
|
|
553
|
+
if (Array.isArray(skeleton.tokens)) return {
|
|
554
|
+
kind: "skeleton",
|
|
555
|
+
value: skeleton.tokens.map((token) => [token.stem, ...token.options ?? []].join("/")).join(" ")
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* The id `babel-plugin-formatjs` / `@formatjs/ts-transformer` give a
|
|
560
|
+
* descriptor without one, with the default `idInterpolationPattern`
|
|
561
|
+
* (`[sha512:contenthash:base64:6]` over `defaultMessage#description`).
|
|
562
|
+
*/
|
|
563
|
+
function formatjsId(defaultMessage, description) {
|
|
564
|
+
const content = description === null || description === "" ? defaultMessage : `${defaultMessage}#${description}`;
|
|
565
|
+
return createHash("sha512").update(content).digest("base64").slice(0, 6);
|
|
566
|
+
}
|
|
567
|
+
/** Lingui's `generateMessageId(message, context)` (v4+): sha256 over `message␟context`, base64, 6 characters. */
|
|
568
|
+
function linguiId(message, context) {
|
|
569
|
+
return createHash("sha256").update(message + "" + (context ?? "")).digest("base64").slice(0, 6);
|
|
570
|
+
}
|
|
571
|
+
/** Appends literal text, merging with a preceding literal. */
|
|
572
|
+
function pushLiteral(nodes, value) {
|
|
573
|
+
if (value === "") return;
|
|
574
|
+
const last = nodes[nodes.length - 1];
|
|
575
|
+
if (last?.type === "literal") last.value += value;
|
|
576
|
+
else nodes.push({
|
|
577
|
+
type: "literal",
|
|
578
|
+
value
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region src/cli/commands/migrate/react.ts
|
|
583
|
+
/**
|
|
584
|
+
* Rich text for the React codemods (i18next `<Trans>`, react-intl rich
|
|
585
|
+
* values, Lingui `<Trans>`): numbered tags (`<0>…</0>`, which vi18n reads as
|
|
586
|
+
* text since a tag name starts with a letter) are renamed after the element
|
|
587
|
+
* they stand for, in the source and every translation alike, and each
|
|
588
|
+
* element becomes a `rich()` handler that wraps the translated children.
|
|
589
|
+
*/
|
|
590
|
+
function newNaming() {
|
|
591
|
+
return {
|
|
592
|
+
byPath: /* @__PURE__ */ new Map(),
|
|
593
|
+
sourcePaths: /* @__PURE__ */ new Set()
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function baseName(element, path) {
|
|
597
|
+
if (element === "html") return path.replace(/^html:/, "");
|
|
598
|
+
if (element === null || element.type !== "JSXElement") return "c" + path.replace(/\W/g, "_");
|
|
599
|
+
const name = jsxName(element).split(/[.:]/).pop().replace(/^[A-Z]/, (c) => c.toLowerCase()).replace(/[^A-Za-z0-9_-]/g, "");
|
|
600
|
+
return /^[A-Za-z]/.test(name) ? name : `c${name}`;
|
|
601
|
+
}
|
|
602
|
+
/** Renames the tags of a message (source first, then translations) through `naming`. */
|
|
603
|
+
function renameTags(nodes, naming, pathOf, elementAt, source) {
|
|
604
|
+
const taken = new Set(naming.byPath.values());
|
|
605
|
+
const walk = (list, parent) => list.map((node) => {
|
|
606
|
+
if (node.type === "plural" || node.type === "select") return {
|
|
607
|
+
...node,
|
|
608
|
+
options: node.options.map((o) => ({
|
|
609
|
+
key: o.key,
|
|
610
|
+
value: walk(o.value, parent)
|
|
611
|
+
}))
|
|
612
|
+
};
|
|
613
|
+
if (node.type !== "tag") return node;
|
|
614
|
+
const path = pathOf(node.name, parent);
|
|
615
|
+
let name = node.name;
|
|
616
|
+
if (path !== null) {
|
|
617
|
+
const known = naming.byPath.get(path);
|
|
618
|
+
if (known !== void 0) name = known;
|
|
619
|
+
else {
|
|
620
|
+
const base = baseName(elementAt(path), path);
|
|
621
|
+
name = base;
|
|
622
|
+
for (let n = 2; taken.has(name); n++) name = `${base}${n}`;
|
|
623
|
+
naming.byPath.set(path, name);
|
|
624
|
+
taken.add(name);
|
|
625
|
+
}
|
|
626
|
+
if (source) naming.sourcePaths.add(path);
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
type: "tag",
|
|
630
|
+
name,
|
|
631
|
+
children: node.children === null ? null : walk(node.children, path ?? parent)
|
|
632
|
+
};
|
|
633
|
+
});
|
|
634
|
+
return walk(nodes, null);
|
|
635
|
+
}
|
|
636
|
+
/** This site's handler per tag name, or the paths it cannot map. */
|
|
637
|
+
function siteHandlers(naming, elementAt) {
|
|
638
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
639
|
+
const missing = [];
|
|
640
|
+
for (const path of naming.sourcePaths) {
|
|
641
|
+
const name = naming.byPath.get(path);
|
|
642
|
+
const element = elementAt(path);
|
|
643
|
+
if (element === "html") handlers.set(name, {
|
|
644
|
+
element: null,
|
|
645
|
+
html: path.replace(/^html:/, "")
|
|
646
|
+
});
|
|
647
|
+
else if (element !== null && element.type === "JSXElement") handlers.set(name, {
|
|
648
|
+
element,
|
|
649
|
+
html: null
|
|
650
|
+
});
|
|
651
|
+
else missing.push(path.replace(/^html:/, ""));
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
handlers,
|
|
655
|
+
missing
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
/** `ReactNode`, imported as a type in TSX so handlers and `rich<ReactNode>()` type-check. */
|
|
659
|
+
function reactTypes(builder) {
|
|
660
|
+
return { node: builder.typeImport("react", "ReactNode") };
|
|
661
|
+
}
|
|
662
|
+
/** A handler wrapping the translated children in the element's opening tag. */
|
|
663
|
+
function elementHandler(handler, selfClosing, r, types) {
|
|
664
|
+
const param = types === null ? "children" : `children: ${types.node}`;
|
|
665
|
+
if (handler.element === null) {
|
|
666
|
+
const tag = handler.html;
|
|
667
|
+
return selfClosing ? `() => <${tag} />` : `(${param}) => <${tag}>{children}</${tag}>`;
|
|
668
|
+
}
|
|
669
|
+
const element = handler.element;
|
|
670
|
+
const opening = element.openingElement;
|
|
671
|
+
if (selfClosing) return `() => ${r.src(opening.start, opening.end).replace(/\s*>$/, "").replace(/\s*\/$/, "")} />`;
|
|
672
|
+
return `(${param}) => ${r.src(opening.start, opening.end).replace(/\s*\/>$/, ">")}{children}</${jsxName(element)}>`;
|
|
673
|
+
}
|
|
674
|
+
/** Whether a message's tag is written self-closing (`<br/>`, `<0/>`). */
|
|
675
|
+
function selfClosingTags(nodes, out = /* @__PURE__ */ new Set()) {
|
|
676
|
+
for (const node of nodes) if (node.type === "tag") {
|
|
677
|
+
if (node.children === null) out.add(node.name);
|
|
678
|
+
else selfClosingTags(node.children, out);
|
|
679
|
+
} else if (node.type === "plural" || node.type === "select") for (const option of node.options) selfClosingTags(option.value, out);
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Annotates the first parameter of handlers in a values object
|
|
684
|
+
* (`(chunks) => …` → `(chunks: ReactNode) => …`): react-intl's types gave it
|
|
685
|
+
* a type from context, vi18n's values are `unknown`, so under
|
|
686
|
+
* `noImplicitAny` the handler would no longer type-check.
|
|
687
|
+
*/
|
|
688
|
+
function annotateHandlers(builder, values, typeName) {
|
|
689
|
+
const props = objectProperties(values);
|
|
690
|
+
if (props === null) return;
|
|
691
|
+
const text = builder.text;
|
|
692
|
+
for (const property of props.order) {
|
|
693
|
+
if (property.type === "SpreadElement") continue;
|
|
694
|
+
const fn = unwrap(property.value);
|
|
695
|
+
if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") continue;
|
|
696
|
+
const param = fn.params[0];
|
|
697
|
+
if (param === void 0 || param.type !== "Identifier" || isNode(param.typeAnnotation)) continue;
|
|
698
|
+
const name = param.name;
|
|
699
|
+
const parenthesized = text.slice(fn.start, param.start).includes("(");
|
|
700
|
+
builder.replace({
|
|
701
|
+
start: param.start,
|
|
702
|
+
end: param.start + name.length
|
|
703
|
+
}, parenthesized ? `${name}: ${typeName}` : `(${name}: ${typeName})`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** `rich<ReactNode>(message, { … })`. */
|
|
707
|
+
function richCall(r, key, entries, types) {
|
|
708
|
+
const typeArgument = types === null ? "" : `<${types.node}>`;
|
|
709
|
+
return `${r.fn.rich}${typeArgument}(${r.message(key)}, { ${entries.join(", ")} })`;
|
|
710
|
+
}
|
|
711
|
+
/** `name: value`, or the shorthand when the value is the name. */
|
|
712
|
+
function entry(name, code) {
|
|
713
|
+
if (code === name) return name;
|
|
714
|
+
return `${/^[A-Za-z_$][\w$]*$/.test(name) || /^(?:0|[1-9]\d*)$/.test(name) ? name : jsString(name)}: ${code}`;
|
|
715
|
+
}
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/cli/commands/migrate/i18next.ts
|
|
718
|
+
/**
|
|
719
|
+
* `vi18n migrate --from i18next` (INTERNALS §2.14): i18next and
|
|
720
|
+
* react-i18next.
|
|
721
|
+
*
|
|
722
|
+
* Catalogs are i18next JSON (v4): nested keys joined by `.`, one file per
|
|
723
|
+
* locale or one per namespace (`<locale>/<ns>.json`), plural suffixes
|
|
724
|
+
* (`_zero`, `_one`, …, `_other`, `_ordinal_*`, v3's `_plural`) folded into
|
|
725
|
+
* one ICU plural, `_<context>` variants into a select on `context`, `{{x}}`
|
|
726
|
+
* into `{x}` (`{{x, number}}`, `datetime`, `currency(EUR)` formatted), and
|
|
727
|
+
* `$t(key)` nesting inlined. Call sites: `t("key", options)` from
|
|
728
|
+
* `useTranslation`, `i18next.t`, a `t` prop; `<Trans>` (components from the
|
|
729
|
+
* `components` prop or the children, as react-i18next maps `<0>…</0>`)
|
|
730
|
+
* becomes `rich()`. `useTranslation` bindings, imports and `<I18nextProvider>`
|
|
731
|
+
* go once nothing else uses them; the rest is reported.
|
|
732
|
+
*/
|
|
733
|
+
const MODULE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
734
|
+
".js",
|
|
735
|
+
".jsx",
|
|
736
|
+
".mjs",
|
|
737
|
+
".cjs",
|
|
738
|
+
".ts",
|
|
739
|
+
".tsx",
|
|
740
|
+
".mts",
|
|
741
|
+
".cts"
|
|
742
|
+
]);
|
|
743
|
+
const PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
|
|
744
|
+
const ORDINAL_SUFFIX = /_ordinal_(zero|one|two|few|many|other)$/;
|
|
745
|
+
const CATEGORIES = [
|
|
746
|
+
"zero",
|
|
747
|
+
"one",
|
|
748
|
+
"two",
|
|
749
|
+
"few",
|
|
750
|
+
"many",
|
|
751
|
+
"other"
|
|
752
|
+
];
|
|
753
|
+
const DEFAULT_NS = "translation";
|
|
754
|
+
/** react-i18next's `transKeepBasicHtmlNodesFor`: rendered as elements when no component maps them. */
|
|
755
|
+
const BASIC_HTML = /* @__PURE__ */ new Set([
|
|
756
|
+
"br",
|
|
757
|
+
"strong",
|
|
758
|
+
"i",
|
|
759
|
+
"p"
|
|
760
|
+
]);
|
|
761
|
+
/** `t()` options that change what is looked up or returned, with no vi18n counterpart. */
|
|
762
|
+
const UNSUPPORTED_OPTIONS = /* @__PURE__ */ new Set([
|
|
763
|
+
"lng",
|
|
764
|
+
"lngs",
|
|
765
|
+
"fallbackLng",
|
|
766
|
+
"returnObjects",
|
|
767
|
+
"returnDetails",
|
|
768
|
+
"joinArrays",
|
|
769
|
+
"postProcess",
|
|
770
|
+
"interpolation",
|
|
771
|
+
"replace",
|
|
772
|
+
"skipInterpolation"
|
|
773
|
+
]);
|
|
774
|
+
/** `t()` options that only steer the lookup; the rest are values. */
|
|
775
|
+
const LOOKUP_OPTIONS = /* @__PURE__ */ new Set([
|
|
776
|
+
"defaultValue",
|
|
777
|
+
"ns",
|
|
778
|
+
"keyPrefix",
|
|
779
|
+
"ordinal"
|
|
780
|
+
]);
|
|
781
|
+
function isOldSource(source) {
|
|
782
|
+
return /^(?:react-|next-)?i18next(?:\/|$)/.test(source) || source.startsWith("i18next-");
|
|
783
|
+
}
|
|
784
|
+
/** Whether a key has plural or ordinal forms (`key_one`, `key_ordinal_two`, v3 `key_plural`). */
|
|
785
|
+
function hasForms(texts, key) {
|
|
786
|
+
return CATEGORIES.some((c) => texts.has(`${key}_${c}`) || texts.has(`${key}_ordinal_${c}`)) || texts.has(`${key}_plural`);
|
|
787
|
+
}
|
|
788
|
+
/** The base key of a plural or ordinal form (`item_one` → `item`). */
|
|
789
|
+
function baseKey(key) {
|
|
790
|
+
return key.replace(ORDINAL_SUFFIX, "").replace(PLURAL_SUFFIX, "").replace(/_plural$/, "");
|
|
791
|
+
}
|
|
792
|
+
/** One `{{…}}` as an ICU node: `count` in a plural branch is `#`. */
|
|
793
|
+
function interpolationNode(inner, inPlural) {
|
|
794
|
+
const body = inner.trim().replace(/^-\s*/, "");
|
|
795
|
+
const comma = body.indexOf(",");
|
|
796
|
+
const name = (comma === -1 ? body : body.slice(0, comma)).trim();
|
|
797
|
+
const format = comma === -1 ? null : body.slice(comma + 1).trim();
|
|
798
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) return { error: `{{${inner}}} reads a nested value, which an ICU argument cannot name` };
|
|
799
|
+
if (format === null) return { node: inPlural && name === "count" ? { type: "pound" } : {
|
|
800
|
+
type: "argument",
|
|
801
|
+
name
|
|
802
|
+
} };
|
|
803
|
+
const m = /^(\w+)(?:\((.*)\))?$/.exec(format);
|
|
804
|
+
const kind = m?.[1]?.toLowerCase();
|
|
805
|
+
const args = m?.[2]?.trim() ?? "";
|
|
806
|
+
if (kind === "number" && args === "") return { node: {
|
|
807
|
+
type: "number",
|
|
808
|
+
name,
|
|
809
|
+
style: null,
|
|
810
|
+
options: null
|
|
811
|
+
} };
|
|
812
|
+
if (kind === "datetime" && args === "") return { node: {
|
|
813
|
+
type: "date",
|
|
814
|
+
name,
|
|
815
|
+
style: null,
|
|
816
|
+
options: null
|
|
817
|
+
} };
|
|
818
|
+
if (kind === "currency") {
|
|
819
|
+
const code = /^(?:currency\s*:\s*)?["']?([A-Za-z]{3})["']?$/.exec(args)?.[1];
|
|
820
|
+
if (code !== void 0) return { node: {
|
|
821
|
+
type: "number",
|
|
822
|
+
name,
|
|
823
|
+
style: {
|
|
824
|
+
kind: "skeleton",
|
|
825
|
+
value: `currency/${code.toUpperCase()}`
|
|
826
|
+
},
|
|
827
|
+
options: null
|
|
828
|
+
} };
|
|
829
|
+
}
|
|
830
|
+
return { error: `{{${inner.trim()}}} uses the ${format} format, which has no ICU counterpart: format the value with fmt.* and pass the string` };
|
|
831
|
+
}
|
|
832
|
+
function createI18nextAdapter() {
|
|
833
|
+
/** locale → namespace → key → text. */
|
|
834
|
+
const catalogs = /* @__PURE__ */ new Map();
|
|
835
|
+
const namings = /* @__PURE__ */ new Map();
|
|
836
|
+
/** `ns:key` of every source text a call site names, its plural and context forms and nested `$t()` included. */
|
|
837
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
838
|
+
/** Marks a key family (`key`, `key_one`, `key_male_other`…) and what its texts nest as named by code. */
|
|
839
|
+
const reference = (ns, key, depth = 0) => {
|
|
840
|
+
if (depth > 8) return;
|
|
841
|
+
for (const [k, text] of namespaces(sourceLocale).get(ns) ?? []) {
|
|
842
|
+
if (k !== key && !k.startsWith(key + "_")) continue;
|
|
843
|
+
if (referenced.has(`${ns}:${k}`)) continue;
|
|
844
|
+
referenced.add(`${ns}:${k}`);
|
|
845
|
+
for (const m of text.value.matchAll(/\$t\(([^(),]*)/g)) {
|
|
846
|
+
const target = m[1].trim();
|
|
847
|
+
const colon = target.indexOf(":");
|
|
848
|
+
if (colon !== -1 && namespaces(sourceLocale).has(target.slice(0, colon))) reference(target.slice(0, colon), target.slice(colon + 1), depth + 1);
|
|
849
|
+
else reference(ns, target, depth + 1);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
let sourceLocale = "";
|
|
854
|
+
let defaultNs = null;
|
|
855
|
+
const namespaces = (locale) => catalogs.get(locale) ?? /* @__PURE__ */ new Map();
|
|
856
|
+
const textOf = (locale, ns, key) => namespaces(locale).get(ns)?.get(key);
|
|
857
|
+
/** Whether the source locale has `key` itself or one of its plural forms. */
|
|
858
|
+
const hasKey = (ns, key) => {
|
|
859
|
+
const texts = namespaces(sourceLocale).get(ns);
|
|
860
|
+
if (texts === void 0) return false;
|
|
861
|
+
return texts.has(key) || hasForms(texts, key);
|
|
862
|
+
};
|
|
863
|
+
/** The namespace holding `key`: the call's own, the default, or the only one that has it. */
|
|
864
|
+
const findNamespace = (key, preferred) => {
|
|
865
|
+
if (preferred !== null) return hasKey(preferred, key) ? preferred : null;
|
|
866
|
+
if (defaultNs !== null && hasKey(defaultNs, key)) return defaultNs;
|
|
867
|
+
const found = [...namespaces(sourceLocale).keys()].filter((ns) => hasKey(ns, key));
|
|
868
|
+
return found.length === 1 ? found[0] : null;
|
|
869
|
+
};
|
|
870
|
+
const messageKey = (ns, key) => ns === defaultNs ? key : `${ns}:${key}`;
|
|
871
|
+
/** Whether a source key only has plural forms, so a call must pass `count`. */
|
|
872
|
+
const isPlural = (ns, key) => {
|
|
873
|
+
const texts = namespaces(sourceLocale).get(ns);
|
|
874
|
+
return texts !== void 0 && !texts.has(key) && hasForms(texts, key);
|
|
875
|
+
};
|
|
876
|
+
/** `{{x}}`, `$t(…)` and, for `<Trans>`, `<0>…</0>` of one text as ICU nodes. */
|
|
877
|
+
const convert = (raw, locale, ns, tags, inPlural, depth = 0) => {
|
|
878
|
+
const root = [];
|
|
879
|
+
const stack = [];
|
|
880
|
+
const current = () => stack.length === 0 ? root : stack[stack.length - 1].nodes;
|
|
881
|
+
const token = /\{\{(.*?)\}\}|\$t\(([^()]*)\)|<(\/?)([A-Za-z0-9_-]+)\s*(\/?)>/g;
|
|
882
|
+
let at = 0;
|
|
883
|
+
for (let m = token.exec(raw); m !== null; m = token.exec(raw)) {
|
|
884
|
+
const [whole, interpolation, nested, closing, tag, selfClosing] = m;
|
|
885
|
+
if (tag !== void 0 && !tags) continue;
|
|
886
|
+
pushLiteral(current(), raw.slice(at, m.index));
|
|
887
|
+
at = m.index + whole.length;
|
|
888
|
+
if (interpolation !== void 0) {
|
|
889
|
+
const node = interpolationNode(interpolation, inPlural);
|
|
890
|
+
if ("error" in node) return node;
|
|
891
|
+
current().push(node.node);
|
|
892
|
+
} else if (nested !== void 0) {
|
|
893
|
+
if (depth > 8) return { error: "$t() nests deeper than 8 levels" };
|
|
894
|
+
if (nested.includes(",")) return { error: `$t(${nested}) passes options, which cannot be inlined` };
|
|
895
|
+
let target = nested.trim();
|
|
896
|
+
let targetNs = ns;
|
|
897
|
+
const colon = target.indexOf(":");
|
|
898
|
+
if (colon !== -1 && namespaces(locale).has(target.slice(0, colon))) {
|
|
899
|
+
targetNs = target.slice(0, colon);
|
|
900
|
+
target = target.slice(colon + 1);
|
|
901
|
+
}
|
|
902
|
+
const text = textOf(locale, targetNs, target);
|
|
903
|
+
if (text === void 0) return { error: `$t(${nested}) names no message in ${locale}` };
|
|
904
|
+
const inner = convert(text.value, locale, targetNs, tags, inPlural, depth + 1);
|
|
905
|
+
if ("error" in inner) return inner;
|
|
906
|
+
for (const node of inner.nodes) if (node.type === "literal") pushLiteral(current(), node.value);
|
|
907
|
+
else current().push(node);
|
|
908
|
+
} else if (closing === "/") {
|
|
909
|
+
const open = stack.pop();
|
|
910
|
+
if (open === void 0 || open.name !== tag) return { error: `</${tag}> closes no <${tag}>` };
|
|
911
|
+
current().push({
|
|
912
|
+
type: "tag",
|
|
913
|
+
name: open.name,
|
|
914
|
+
children: open.nodes
|
|
915
|
+
});
|
|
916
|
+
} else if (selfClosing === "/") current().push({
|
|
917
|
+
type: "tag",
|
|
918
|
+
name: tag,
|
|
919
|
+
children: null
|
|
920
|
+
});
|
|
921
|
+
else stack.push({
|
|
922
|
+
name: tag,
|
|
923
|
+
nodes: []
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
if (stack.length > 0) return { error: `<${stack[stack.length - 1].name}> is never closed` };
|
|
927
|
+
pushLiteral(current(), raw.slice(at));
|
|
928
|
+
return { nodes: root };
|
|
929
|
+
};
|
|
930
|
+
/** One key's text, or its plural forms as an ICU plural. */
|
|
931
|
+
const branch = (locale, ns, key, shape) => {
|
|
932
|
+
const texts = namespaces(locale).get(ns);
|
|
933
|
+
if (texts === void 0) return null;
|
|
934
|
+
if (shape.count) {
|
|
935
|
+
const categories = pluralCategories(locale)[shape.ordinal ? "ordinal" : "cardinal"];
|
|
936
|
+
const forms = [];
|
|
937
|
+
for (const category of CATEGORIES) {
|
|
938
|
+
const formKey = shape.ordinal ? `${key}_ordinal_${category}` : `${key}_${category}`;
|
|
939
|
+
const text = texts.get(formKey);
|
|
940
|
+
if (text === void 0) continue;
|
|
941
|
+
const converted = convert(text.value, locale, ns, shape.tags, true);
|
|
942
|
+
if ("error" in converted) return { error: `${formKey}: ${converted.error}` };
|
|
943
|
+
forms.push({
|
|
944
|
+
key: category === "zero" && !categories.includes("zero") ? "=0" : category,
|
|
945
|
+
value: converted.nodes
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
const legacy = texts.get(`${key}_plural`);
|
|
949
|
+
if (forms.length === 0 && legacy !== void 0 && !shape.ordinal) for (const [category, text] of [["one", texts.get(key)], ["other", legacy]]) {
|
|
950
|
+
if (text === void 0) continue;
|
|
951
|
+
const converted = convert(text.value, locale, ns, shape.tags, true);
|
|
952
|
+
if ("error" in converted) return converted;
|
|
953
|
+
forms.push({
|
|
954
|
+
key: category,
|
|
955
|
+
value: converted.nodes
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
if (forms.length > 0) {
|
|
959
|
+
if (!forms.some((f) => f.key === "other")) {
|
|
960
|
+
const plain = texts.get(key);
|
|
961
|
+
if (plain === void 0) return { error: `${key} has plural forms but no ${key}_other` };
|
|
962
|
+
const converted = convert(plain.value, locale, ns, shape.tags, true);
|
|
963
|
+
if ("error" in converted) return converted;
|
|
964
|
+
forms.push({
|
|
965
|
+
key: "other",
|
|
966
|
+
value: converted.nodes
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
const order = ["=0", ...CATEGORIES];
|
|
970
|
+
forms.sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key));
|
|
971
|
+
return { nodes: [{
|
|
972
|
+
type: "plural",
|
|
973
|
+
pluralType: shape.ordinal ? "ordinal" : "cardinal",
|
|
974
|
+
name: "count",
|
|
975
|
+
offset: 0,
|
|
976
|
+
options: forms
|
|
977
|
+
}] };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const text = texts.get(key);
|
|
981
|
+
return text === void 0 ? null : convert(text.value, locale, ns, shape.tags, false);
|
|
982
|
+
};
|
|
983
|
+
/** The message of a key and shape in one locale; null when the locale has none of its keys. */
|
|
984
|
+
const build = (locale, ns, key, shape) => {
|
|
985
|
+
if (!shape.context) return branch(locale, ns, key, shape);
|
|
986
|
+
const contexts = /* @__PURE__ */ new Set();
|
|
987
|
+
const keys = /* @__PURE__ */ new Set([...namespaces(sourceLocale).get(ns)?.keys() ?? [], ...namespaces(locale).get(ns)?.keys() ?? []]);
|
|
988
|
+
for (const k of keys) {
|
|
989
|
+
if (!k.startsWith(key + "_")) continue;
|
|
990
|
+
const rest = baseKey(k.slice(key.length + 1));
|
|
991
|
+
if (rest === "" || CATEGORIES.includes(rest) || rest === "plural" || rest.includes(".")) continue;
|
|
992
|
+
contexts.add(rest);
|
|
993
|
+
}
|
|
994
|
+
const options = [];
|
|
995
|
+
for (const context of [...contexts].sort()) {
|
|
996
|
+
const converted = branch(locale, ns, `${key}_${context}`, shape);
|
|
997
|
+
if (converted === null) continue;
|
|
998
|
+
if ("error" in converted) return converted;
|
|
999
|
+
options.push({
|
|
1000
|
+
key: context,
|
|
1001
|
+
value: converted.nodes
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
const other = branch(locale, ns, key, shape);
|
|
1005
|
+
if (other !== null && "error" in other) return other;
|
|
1006
|
+
if (options.length === 0 && other === null) return null;
|
|
1007
|
+
options.push({
|
|
1008
|
+
key: "other",
|
|
1009
|
+
value: other === null ? [] : other.nodes
|
|
1010
|
+
});
|
|
1011
|
+
return { nodes: [{
|
|
1012
|
+
type: "select",
|
|
1013
|
+
name: "context",
|
|
1014
|
+
options
|
|
1015
|
+
}] };
|
|
1016
|
+
};
|
|
1017
|
+
/** Every text of a key family in a locale, for a translator comment. */
|
|
1018
|
+
const originalTexts = (locale, ns, key) => {
|
|
1019
|
+
const parts = [];
|
|
1020
|
+
for (const [k, text] of namespaces(locale).get(ns) ?? []) if (k === key) parts.push(text.value);
|
|
1021
|
+
else if (k.startsWith(key + "_")) parts.push(`${k.slice(key.length + 1)}: ${text.value}`);
|
|
1022
|
+
return parts.join(" | ");
|
|
1023
|
+
};
|
|
1024
|
+
/**
|
|
1025
|
+
* Registers the message of a key and shape; its registry key, or why it
|
|
1026
|
+
* cannot be. Tags are named through `tags` (path rules and the defining
|
|
1027
|
+
* site's elements); a later site of the same message reuses the naming.
|
|
1028
|
+
*/
|
|
1029
|
+
const define = (env, ns, key, shape, fallback, tags) => {
|
|
1030
|
+
const id = messageKey(ns, key) + (shape.context ? " {context}" : "") + (shape.ordinal ? " {ordinal}" : "") + (shape.tags ? " <Trans>" : "");
|
|
1031
|
+
if (env.message(id) !== void 0) return { key: id };
|
|
1032
|
+
let source = build(sourceLocale, ns, key, shape);
|
|
1033
|
+
if (source === null && fallback !== null) source = convert(fallback, sourceLocale, ns, shape.tags, false);
|
|
1034
|
+
if (source === null) return { problem: `${messageKey(ns, key)} is in no ${sourceLocale} catalog` };
|
|
1035
|
+
if ("error" in source) return { problem: `${messageKey(ns, key)}: ${source.error}` };
|
|
1036
|
+
const naming = newNaming();
|
|
1037
|
+
const finish = (nodes, isSource) => printICU(tags === null ? nodes : renameTags(nodes, naming, tags.pathOf, tags.elementAt, isSource));
|
|
1038
|
+
const sourceText = finish(source.nodes, true);
|
|
1039
|
+
const translations = /* @__PURE__ */ new Map();
|
|
1040
|
+
for (const locale of catalogs.keys()) {
|
|
1041
|
+
if (locale === sourceLocale) continue;
|
|
1042
|
+
const converted = build(locale, ns, key, shape);
|
|
1043
|
+
if (converted === null) continue;
|
|
1044
|
+
const first = [...namespaces(locale).get(ns) ?? []].find(([k]) => k === key || k.startsWith(key + "_"))?.[1];
|
|
1045
|
+
translations.set(locale, "error" in converted ? {
|
|
1046
|
+
unconverted: originalTexts(locale, ns, key),
|
|
1047
|
+
reason: converted.error,
|
|
1048
|
+
...first === void 0 ? {} : {
|
|
1049
|
+
file: first.file,
|
|
1050
|
+
line: first.line
|
|
1051
|
+
}
|
|
1052
|
+
} : { icu: finish(converted.nodes, false) });
|
|
1053
|
+
}
|
|
1054
|
+
let origin = textOf(sourceLocale, ns, key);
|
|
1055
|
+
for (const [k, text] of namespaces(sourceLocale).get(ns) ?? []) {
|
|
1056
|
+
if (origin !== void 0) break;
|
|
1057
|
+
if (k.startsWith(key + "_")) origin = text;
|
|
1058
|
+
}
|
|
1059
|
+
if (!env.define({
|
|
1060
|
+
key: id,
|
|
1061
|
+
named: true,
|
|
1062
|
+
source: sourceText,
|
|
1063
|
+
context: null,
|
|
1064
|
+
hint: null,
|
|
1065
|
+
translations,
|
|
1066
|
+
file: origin?.file ?? "",
|
|
1067
|
+
line: origin?.line ?? 0
|
|
1068
|
+
})) return { problem: `${messageKey(ns, key)} could not be converted` };
|
|
1069
|
+
if (tags !== null) namings.set(id, naming);
|
|
1070
|
+
return { key: id };
|
|
1071
|
+
};
|
|
1072
|
+
const readCatalog = (file, env) => {
|
|
1073
|
+
let text;
|
|
1074
|
+
try {
|
|
1075
|
+
text = readFileSync(file.file, "utf8");
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
env.report("catalog-parse", `cannot read: ${error.message}`, { file: file.file }, "error");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const parsed = parseJson(text);
|
|
1081
|
+
if (parsed.problem !== null) {
|
|
1082
|
+
env.report("catalog-parse", parsed.problem.message, {
|
|
1083
|
+
file: file.file,
|
|
1084
|
+
line: parsed.problem.line
|
|
1085
|
+
}, "error");
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const flat = flattenTree(parsed.value, text);
|
|
1089
|
+
for (const problem of flat.problems) env.report("migrate-unconverted", problem.message, {
|
|
1090
|
+
file: file.file,
|
|
1091
|
+
line: problem.line
|
|
1092
|
+
});
|
|
1093
|
+
const ns = file.namespace ?? DEFAULT_NS;
|
|
1094
|
+
let byNs = catalogs.get(file.locale);
|
|
1095
|
+
if (byNs === void 0) catalogs.set(file.locale, byNs = /* @__PURE__ */ new Map());
|
|
1096
|
+
let texts = byNs.get(ns);
|
|
1097
|
+
if (texts === void 0) byNs.set(ns, texts = /* @__PURE__ */ new Map());
|
|
1098
|
+
for (const [key, value] of flat.texts) texts.set(key, {
|
|
1099
|
+
...value,
|
|
1100
|
+
file: file.file
|
|
1101
|
+
});
|
|
1102
|
+
};
|
|
1103
|
+
const planModule = (file, text, env) => {
|
|
1104
|
+
let program;
|
|
1105
|
+
try {
|
|
1106
|
+
program = parseModule(text, langFromId(file), file).program;
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (!isDiagnosticError(error)) throw error;
|
|
1109
|
+
env.diagnostics.push({
|
|
1110
|
+
...error.diagnostic,
|
|
1111
|
+
file: env.rel(file)
|
|
1112
|
+
});
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
const info = analyzeModule(program);
|
|
1116
|
+
const builder = new ModuleBuilder({
|
|
1117
|
+
file,
|
|
1118
|
+
text,
|
|
1119
|
+
info,
|
|
1120
|
+
isOldSource
|
|
1121
|
+
});
|
|
1122
|
+
const tsx = /\.[mc]?tsx$/.test(extname(file)) || extname(file) === ".tsx";
|
|
1123
|
+
const imported = (name) => {
|
|
1124
|
+
const locals = /* @__PURE__ */ new Set();
|
|
1125
|
+
for (const declaration of info.imports) {
|
|
1126
|
+
if (!isOldSource(declaration.source)) continue;
|
|
1127
|
+
for (const s of declaration.specifiers) if (s.imported === name) locals.add(s.local);
|
|
1128
|
+
}
|
|
1129
|
+
return locals;
|
|
1130
|
+
};
|
|
1131
|
+
const useTranslation = imported("useTranslation");
|
|
1132
|
+
const transNames = imported("Trans");
|
|
1133
|
+
const providers = imported("I18nextProvider");
|
|
1134
|
+
const instances = /* @__PURE__ */ new Set([
|
|
1135
|
+
...imported("default"),
|
|
1136
|
+
...imported("i18n"),
|
|
1137
|
+
...imported("i18next")
|
|
1138
|
+
]);
|
|
1139
|
+
const importedT = imported("t");
|
|
1140
|
+
const withTranslation = imported("withTranslation");
|
|
1141
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
1142
|
+
const hookInstances = /* @__PURE__ */ new Set();
|
|
1143
|
+
const fixedLocale = /* @__PURE__ */ new Set();
|
|
1144
|
+
for (const binding of info.bindings) {
|
|
1145
|
+
const init = binding.declarator?.init;
|
|
1146
|
+
if (init === null || init === void 0) continue;
|
|
1147
|
+
const call = unwrap(init);
|
|
1148
|
+
if (call.type !== "CallExpression") continue;
|
|
1149
|
+
const callee = identifierName(call.callee);
|
|
1150
|
+
const isHook = callee !== null && useTranslation.has(callee);
|
|
1151
|
+
const fixed = memberParts(call.callee)?.property === "getFixedT";
|
|
1152
|
+
if (!isHook && !fixed) continue;
|
|
1153
|
+
const args = call.arguments;
|
|
1154
|
+
if (isHook) builder.hooks.push(binding);
|
|
1155
|
+
const nsNode = isHook ? args[0] : args[1];
|
|
1156
|
+
let ns = null;
|
|
1157
|
+
if (nsNode !== void 0) {
|
|
1158
|
+
const n = unwrap(nsNode);
|
|
1159
|
+
ns = stringValue(n) ?? (n.type === "ArrayExpression" ? stringValue(n.elements[0]) : null);
|
|
1160
|
+
}
|
|
1161
|
+
const options = isHook ? args[1] : args[2];
|
|
1162
|
+
const prefix = options === void 0 ? null : stringValue(objectProperties(options)?.properties.get("keyPrefix")?.value);
|
|
1163
|
+
const slot = binding.slot;
|
|
1164
|
+
const pattern = binding.declarator.id;
|
|
1165
|
+
const role = slot === null ? "t" : slot.type === "Property" ? keyName(slot) : pattern.type === "ArrayPattern" ? pattern.elements.indexOf(slot) === 0 ? "t" : "i18n" : null;
|
|
1166
|
+
if (role === "i18n") hookInstances.add(binding);
|
|
1167
|
+
else if (role === "t") scopes.set(binding, {
|
|
1168
|
+
ns,
|
|
1169
|
+
prefix
|
|
1170
|
+
});
|
|
1171
|
+
const lng = args[0] === void 0 ? null : unwrap(args[0]);
|
|
1172
|
+
if (fixed && lng !== null && !(lng.type === "Literal" && lng.value === null) && identifierName(lng) !== "undefined") {
|
|
1173
|
+
fixedLocale.add(binding);
|
|
1174
|
+
builder.note(call.start, "getFixedT(lng) pins a locale; its calls were not rewritten: use withLocale(lng, () => t(…)) or createTranslator(lng) from @menapo/vi18n/server");
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/** The innermost binding of `name` visible at `offset`. */
|
|
1178
|
+
const resolve = (name, offset) => {
|
|
1179
|
+
let best = null;
|
|
1180
|
+
for (const b of info.bindings) {
|
|
1181
|
+
if (b.name !== name || offset < b.scope.start || offset >= b.scope.end) continue;
|
|
1182
|
+
if (best === null || b.scope.end - b.scope.start < best.scope.end - best.scope.start) best = b;
|
|
1183
|
+
}
|
|
1184
|
+
return best;
|
|
1185
|
+
};
|
|
1186
|
+
/** Keys a computed key can produce, defined so they reach the `msg()` table. */
|
|
1187
|
+
const computedKeys = (node, ns, prefix) => {
|
|
1188
|
+
const all = [];
|
|
1189
|
+
for (const [space, texts] of namespaces(sourceLocale)) {
|
|
1190
|
+
if (ns !== null && space !== ns) continue;
|
|
1191
|
+
for (const key of new Set([...texts.keys()].map(baseKey))) all.push({
|
|
1192
|
+
ns: space,
|
|
1193
|
+
key
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
const found = /* @__PURE__ */ new Set();
|
|
1197
|
+
const add = (entryNs, key) => {
|
|
1198
|
+
const shape = {
|
|
1199
|
+
count: isPlural(entryNs, key),
|
|
1200
|
+
ordinal: false,
|
|
1201
|
+
context: false,
|
|
1202
|
+
tags: false
|
|
1203
|
+
};
|
|
1204
|
+
const defined = define(env, entryNs, key, shape, null, null);
|
|
1205
|
+
reference(entryNs, key);
|
|
1206
|
+
if ("key" in defined) found.add(defined.key);
|
|
1207
|
+
};
|
|
1208
|
+
const escape = (q) => q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1209
|
+
const visitKey = (n) => {
|
|
1210
|
+
if (n.type === "Literal" && typeof n.value === "string") {
|
|
1211
|
+
const key = prefix === null ? n.value : `${prefix}.${n.value}`;
|
|
1212
|
+
for (const e of all) if (e.key === key) add(e.ns, e.key);
|
|
1213
|
+
} else if (n.type === "TemplateLiteral") {
|
|
1214
|
+
const quasis = n.quasis.map((q) => q.value.cooked ?? "");
|
|
1215
|
+
if (prefix !== null) quasis[0] = `${prefix}.${quasis[0]}`;
|
|
1216
|
+
if (/[A-Za-z]/.test(quasis.join(""))) {
|
|
1217
|
+
const pattern = new RegExp("^" + quasis.map(escape).join("[^]+?") + "$");
|
|
1218
|
+
for (const e of all) if (pattern.test(e.key)) add(e.ns, e.key);
|
|
1219
|
+
}
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
for (const [, child] of childEntries(n)) visitKey(child);
|
|
1223
|
+
};
|
|
1224
|
+
visitKey(node);
|
|
1225
|
+
return [...found];
|
|
1226
|
+
};
|
|
1227
|
+
/** `t(key, options)` or `<x>.t(key, options)`. */
|
|
1228
|
+
const tCall = (node) => {
|
|
1229
|
+
const callee = unwrap(node.callee);
|
|
1230
|
+
const args = node.arguments;
|
|
1231
|
+
let known = false;
|
|
1232
|
+
let consume = [];
|
|
1233
|
+
let scope = {
|
|
1234
|
+
ns: null,
|
|
1235
|
+
prefix: null
|
|
1236
|
+
};
|
|
1237
|
+
let pinned = false;
|
|
1238
|
+
if (callee.type === "Identifier") {
|
|
1239
|
+
const name = callee.name;
|
|
1240
|
+
const binding = resolve(name, callee.start);
|
|
1241
|
+
if (binding !== null && scopes.has(binding)) {
|
|
1242
|
+
known = true;
|
|
1243
|
+
pinned = fixedLocale.has(binding);
|
|
1244
|
+
scope = scopes.get(binding);
|
|
1245
|
+
} else if (importedT.has(name)) known = true;
|
|
1246
|
+
else if (name !== "t" || binding?.kind === "import") return;
|
|
1247
|
+
consume = [callee];
|
|
1248
|
+
} else {
|
|
1249
|
+
const member = memberParts(callee);
|
|
1250
|
+
if (member === null || member.property !== "t") return;
|
|
1251
|
+
const object = unwrap(member.object);
|
|
1252
|
+
if (object.type === "Identifier") {
|
|
1253
|
+
consume = [object];
|
|
1254
|
+
const binding = resolve(object.name, object.start);
|
|
1255
|
+
if (binding !== null && scopes.has(binding)) {
|
|
1256
|
+
known = true;
|
|
1257
|
+
pinned = fixedLocale.has(binding);
|
|
1258
|
+
scope = scopes.get(binding);
|
|
1259
|
+
} else if (instances.has(object.name) || binding !== null && hookInstances.has(binding)) known = true;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
const first = args[0];
|
|
1263
|
+
if (first === void 0 || first.type === "SpreadElement") return;
|
|
1264
|
+
const options = args[1];
|
|
1265
|
+
const optionProps = options === void 0 ? null : objectProperties(options);
|
|
1266
|
+
let rawKey = stringValue(first);
|
|
1267
|
+
if (rawKey === null) {
|
|
1268
|
+
const keys = computedKeys(first, scope.ns, scope.prefix);
|
|
1269
|
+
if (keys.length > 0 || known) builder.note(first.start, `t(${text.slice(first.start, first.end)}) uses a computed key and was not rewritten`, keys);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const label = JSON.stringify(rawKey);
|
|
1273
|
+
let ns = stringValue(optionProps?.properties.get("ns")?.value) ?? scope.ns;
|
|
1274
|
+
const colon = rawKey.indexOf(":");
|
|
1275
|
+
if (colon !== -1 && namespaces(sourceLocale).has(rawKey.slice(0, colon))) {
|
|
1276
|
+
ns = rawKey.slice(0, colon);
|
|
1277
|
+
rawKey = rawKey.slice(colon + 1);
|
|
1278
|
+
}
|
|
1279
|
+
const key = scope.prefix === null ? rawKey : `${scope.prefix}.${rawKey}`;
|
|
1280
|
+
const fallback = options !== void 0 && optionProps === null ? stringValue(options) : stringValue(optionProps?.properties.get("defaultValue")?.value);
|
|
1281
|
+
const resolvedNs = findNamespace(key, ns);
|
|
1282
|
+
if (resolvedNs !== null) reference(resolvedNs, key);
|
|
1283
|
+
if (resolvedNs === null && fallback === null) {
|
|
1284
|
+
if (known) builder.note(node.start, `t(${label}): the key is in no ${sourceLocale} catalog; not rewritten`);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const unsupported = [...optionProps?.properties.keys() ?? []].filter((k) => UNSUPPORTED_OPTIONS.has(k));
|
|
1288
|
+
if (unsupported.length > 0 || args.length > 2) {
|
|
1289
|
+
builder.note(node.start, unsupported.includes("lng") || unsupported.includes("lngs") ? `t(${label}) fixes a locale (${unsupported.join(", ")}): use withLocale(lng, () => t(…)) from @menapo/vi18n/server; not rewritten` : `t(${label}, …) passes ${unsupported.join(", ") || "a third argument"}, which vi18n has no counterpart for; not rewritten`);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const finalNs = resolvedNs ?? ns ?? defaultNs ?? DEFAULT_NS;
|
|
1293
|
+
if (pinned) {
|
|
1294
|
+
const shape = {
|
|
1295
|
+
count: isPlural(finalNs, key),
|
|
1296
|
+
ordinal: false,
|
|
1297
|
+
context: false,
|
|
1298
|
+
tags: false
|
|
1299
|
+
};
|
|
1300
|
+
const defined = define(env, finalNs, key, shape, fallback, null);
|
|
1301
|
+
if ("key" in defined) builder.note(node.start, `t(${label}) uses a locale getFixedT() pinned; not rewritten`, [defined.key]);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const passesValues = options !== void 0 && optionProps === null && fallback === null;
|
|
1305
|
+
const shape = {
|
|
1306
|
+
count: optionProps?.properties.has("count") === true || passesValues && isPlural(finalNs, key),
|
|
1307
|
+
ordinal: optionProps?.properties.has("ordinal") === true,
|
|
1308
|
+
context: optionProps?.properties.has("context") === true,
|
|
1309
|
+
tags: false
|
|
1310
|
+
};
|
|
1311
|
+
if (!shape.count && isPlural(finalNs, key)) {
|
|
1312
|
+
builder.note(node.start, `t(${label}) has plural forms but passes no count; not rewritten`);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const defined = define(env, finalNs, key, shape, fallback, null);
|
|
1316
|
+
if ("problem" in defined) {
|
|
1317
|
+
builder.note(node.start, `${defined.problem}; t(${label}) was not rewritten`);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const kept = (optionProps?.order ?? []).filter((p) => p.type === "SpreadElement" || !LOOKUP_OPTIONS.has(keyName(p) ?? ""));
|
|
1321
|
+
builder.site(node, (r) => {
|
|
1322
|
+
let values = "";
|
|
1323
|
+
if (kept.length > 0) values = `, { ${kept.map((p) => r.src(p.start, p.end)).join(", ")} }`;
|
|
1324
|
+
else if (passesValues) values = `, ${r.src(options.start, options.end)}`;
|
|
1325
|
+
return `${r.fn.t}(${r.message(defined.key)}${values})`;
|
|
1326
|
+
}, {
|
|
1327
|
+
keys: [defined.key],
|
|
1328
|
+
uses: ["t"],
|
|
1329
|
+
consume
|
|
1330
|
+
});
|
|
1331
|
+
};
|
|
1332
|
+
/** `<Trans>` as `rich()`, or `t()` when its message has no tags. */
|
|
1333
|
+
const trans = (element) => {
|
|
1334
|
+
const attributes = jsxAttributes(element);
|
|
1335
|
+
const attr = (name) => attributes.named.get(name)?.value ?? null;
|
|
1336
|
+
const has = (name) => attributes.named.has(name);
|
|
1337
|
+
if (attributes.spreads.length > 0) {
|
|
1338
|
+
builder.note(element.start, "<Trans {...props}> spreads props and was not rewritten");
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
for (const name of [
|
|
1342
|
+
"tOptions",
|
|
1343
|
+
"parent",
|
|
1344
|
+
"i18nIsDynamicList",
|
|
1345
|
+
"shouldUnescape"
|
|
1346
|
+
]) if (has(name)) {
|
|
1347
|
+
builder.note(element.start, `<Trans ${name}> has no vi18n counterpart; not rewritten`);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const children = jsxChildren(element);
|
|
1351
|
+
const childString = nodesToString(children);
|
|
1352
|
+
let rawKey = has("i18nKey") ? attributeString(attr("i18nKey")) : null;
|
|
1353
|
+
if (has("i18nKey") && rawKey === null) {
|
|
1354
|
+
builder.note(element.start, "<Trans i18nKey={…}> with a computed key was not rewritten");
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
const fallback = has("defaults") ? attributeString(attr("defaults")) : childString.ok ? childString.text : null;
|
|
1358
|
+
rawKey ??= fallback;
|
|
1359
|
+
if (rawKey === null || rawKey === "") {
|
|
1360
|
+
builder.note(element.start, `<Trans> without i18nKey: ${childString.reason || "no text"}; not rewritten`);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
let ns = has("ns") ? attributeString(attr("ns")) : null;
|
|
1364
|
+
const colon = rawKey.indexOf(":");
|
|
1365
|
+
if (colon !== -1 && namespaces(sourceLocale).has(rawKey.slice(0, colon))) {
|
|
1366
|
+
ns = rawKey.slice(0, colon);
|
|
1367
|
+
rawKey = rawKey.slice(colon + 1);
|
|
1368
|
+
}
|
|
1369
|
+
const resolvedNs = findNamespace(rawKey, ns);
|
|
1370
|
+
if (resolvedNs !== null) reference(resolvedNs, rawKey);
|
|
1371
|
+
const finalNs = resolvedNs ?? ns ?? defaultNs ?? DEFAULT_NS;
|
|
1372
|
+
const componentsExpr = has("components") ? attributeExpression(attr("components")) : null;
|
|
1373
|
+
let indexed = children;
|
|
1374
|
+
let named = null;
|
|
1375
|
+
if (componentsExpr !== null) {
|
|
1376
|
+
const c = unwrap(componentsExpr);
|
|
1377
|
+
if (c.type === "ArrayExpression") indexed = c.elements.map((e) => e === null ? c : unwrap(e));
|
|
1378
|
+
else {
|
|
1379
|
+
const props = objectProperties(c);
|
|
1380
|
+
if (props === null || props.spreads.length > 0) {
|
|
1381
|
+
builder.note(element.start, "<Trans components={…}> is not a literal object or array; not rewritten");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
named = new Map([...props.properties].map(([k, p]) => [k, unwrap(p.value)]));
|
|
1385
|
+
indexed = null;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const pathOf = (name, parent) => {
|
|
1389
|
+
if (/^\d+$/.test(name) && indexed !== null) return parent === null ? name : `${parent}.${name}`;
|
|
1390
|
+
if (named?.has(name) === true) return name;
|
|
1391
|
+
if (BASIC_HTML.has(name)) return `html:${name}`;
|
|
1392
|
+
return null;
|
|
1393
|
+
};
|
|
1394
|
+
const elementAt = (path) => {
|
|
1395
|
+
if (path.startsWith("html:")) return "html";
|
|
1396
|
+
if (named !== null && !/^\d/.test(path)) return named.get(path) ?? null;
|
|
1397
|
+
let scope = indexed;
|
|
1398
|
+
let found = null;
|
|
1399
|
+
for (const part of path.split(".")) {
|
|
1400
|
+
found = scope?.[Number(part)] ?? null;
|
|
1401
|
+
if (found === null || found.type !== "JSXElement") return null;
|
|
1402
|
+
scope = jsxChildren(found);
|
|
1403
|
+
}
|
|
1404
|
+
return found;
|
|
1405
|
+
};
|
|
1406
|
+
const shape = {
|
|
1407
|
+
count: has("count"),
|
|
1408
|
+
ordinal: false,
|
|
1409
|
+
context: has("context"),
|
|
1410
|
+
tags: true
|
|
1411
|
+
};
|
|
1412
|
+
const defined = define(env, finalNs, rawKey, shape, resolvedNs === null ? fallback : null, {
|
|
1413
|
+
pathOf,
|
|
1414
|
+
elementAt
|
|
1415
|
+
});
|
|
1416
|
+
if ("problem" in defined) {
|
|
1417
|
+
builder.note(element.start, `${defined.problem}; <Trans> was not rewritten`);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const message = env.message(defined.key);
|
|
1421
|
+
const naming = namings.get(defined.key);
|
|
1422
|
+
if (message === void 0 || naming === void 0) return;
|
|
1423
|
+
const { handlers, missing } = siteHandlers(naming, elementAt);
|
|
1424
|
+
if (missing.length > 0) {
|
|
1425
|
+
builder.note(element.start, `<Trans>: ${missing.map((m) => `<${m}>`).join(", ")} ${missing.length === 1 ? "maps" : "map"} to no element here; not rewritten`, [defined.key]);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const selfClosing = selfClosingTags(env.codec.parse(message.canonical));
|
|
1429
|
+
const values = [];
|
|
1430
|
+
const valuesExpr = has("values") ? attributeExpression(attr("values")) : null;
|
|
1431
|
+
if (valuesExpr !== null) {
|
|
1432
|
+
const props = objectProperties(valuesExpr);
|
|
1433
|
+
if (props === null) {
|
|
1434
|
+
builder.note(element.start, "<Trans values={…}> is not an object literal; not rewritten");
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
values.push(...props.order);
|
|
1438
|
+
}
|
|
1439
|
+
const collect = (list) => {
|
|
1440
|
+
for (const child of list) {
|
|
1441
|
+
if (child.type === "JSXElement") collect(jsxChildren(child));
|
|
1442
|
+
if (child.type !== "JSXExpressionContainer") continue;
|
|
1443
|
+
const props = objectProperties(child.expression);
|
|
1444
|
+
if (props !== null) values.push(...props.order.filter((p) => p.type === "SpreadElement" || keyName(p) !== "format"));
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
collect(children);
|
|
1448
|
+
const extra = [];
|
|
1449
|
+
for (const name of ["count", "context"]) {
|
|
1450
|
+
const expression = has(name) ? attributeExpression(attr(name)) : null;
|
|
1451
|
+
if (expression !== null) extra.push([name, expression]);
|
|
1452
|
+
}
|
|
1453
|
+
const rich = handlers.size > 0;
|
|
1454
|
+
const types = rich && tsx ? reactTypes(builder) : null;
|
|
1455
|
+
builder.site(element, (r) => {
|
|
1456
|
+
const entries = [];
|
|
1457
|
+
for (const [name, handler] of handlers) entries.push(entry(name, elementHandler(handler, selfClosing.has(name), r, types)));
|
|
1458
|
+
for (const p of values) entries.push(r.src(p.start, p.end));
|
|
1459
|
+
for (const [name, expression] of extra) entries.push(entry(name, r.src(expression.start, expression.end)));
|
|
1460
|
+
const call = rich ? richCall(r, defined.key, entries, types) : `${r.fn.t}(${r.message(defined.key)}${entries.length > 0 ? `, { ${entries.join(", ")} }` : ""})`;
|
|
1461
|
+
return inJsxChildren(info, element) ? `{${call}}` : call;
|
|
1462
|
+
}, {
|
|
1463
|
+
keys: [defined.key],
|
|
1464
|
+
uses: [rich ? "rich" : "t"],
|
|
1465
|
+
consume: jsxNameNodes(element)
|
|
1466
|
+
});
|
|
1467
|
+
};
|
|
1468
|
+
const unwrapProvider = (element) => {
|
|
1469
|
+
const children = jsxChildren(element);
|
|
1470
|
+
const opening = element.openingElement;
|
|
1471
|
+
const closing = element.closingElement;
|
|
1472
|
+
builder.consumeRange(opening);
|
|
1473
|
+
builder.consume(jsxNameNodes(element));
|
|
1474
|
+
const nested = inJsxChildren(info, element);
|
|
1475
|
+
builder.replace(element, (r) => {
|
|
1476
|
+
if (closing === null) return nested ? "" : "null";
|
|
1477
|
+
if (nested) return r.src(opening.end, closing.start).trim();
|
|
1478
|
+
if (children.length === 1 && children[0].type === "JSXElement") return r.src(children[0].start, children[0].end);
|
|
1479
|
+
return `<>${r.src(opening.end, closing.start)}</>`;
|
|
1480
|
+
});
|
|
1481
|
+
};
|
|
1482
|
+
/** Setup and APIs without a mechanical counterpart, reported. */
|
|
1483
|
+
const otherCall = (node) => {
|
|
1484
|
+
const name = identifierName(node.callee);
|
|
1485
|
+
if (name !== null && withTranslation.has(name)) {
|
|
1486
|
+
builder.note(node.start, "withTranslation() injects t as a prop: drop it and import t from @menapo/vi18n where the component calls it");
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const member = memberParts(node.callee);
|
|
1490
|
+
if (member === null) return;
|
|
1491
|
+
let root = unwrap(member.object);
|
|
1492
|
+
for (;;) if (root.type === "CallExpression") root = unwrap(root.callee);
|
|
1493
|
+
else if (root.type === "MemberExpression") root = unwrap(root.object);
|
|
1494
|
+
else break;
|
|
1495
|
+
const object = identifierName(root);
|
|
1496
|
+
const isInstance = object !== null && (instances.has(object) || /^i18n(?:ext)?$/.test(object));
|
|
1497
|
+
if (member.property === "init" && isInstance) builder.note(node.start, "i18next.init(): delete this setup once nothing uses i18next; vi18n needs only its Vite plugin and vi18n.config.ts");
|
|
1498
|
+
else if (member.property === "changeLanguage" && isInstance) builder.note(node.start, "changeLanguage(lng): use setLocale(lng) from @menapo/vi18n");
|
|
1499
|
+
else if (member.property === "exists" && isInstance) builder.note(node.start, "exists(key) has no vi18n counterpart: every message the code names exists");
|
|
1500
|
+
};
|
|
1501
|
+
const visit = (node) => {
|
|
1502
|
+
if (node.type === "CallExpression") {
|
|
1503
|
+
tCall(node);
|
|
1504
|
+
otherCall(node);
|
|
1505
|
+
} else if (node.type === "JSXElement") {
|
|
1506
|
+
const name = jsxName(node);
|
|
1507
|
+
if (transNames.has(name)) trans(node);
|
|
1508
|
+
else if (providers.has(name)) unwrapProvider(node);
|
|
1509
|
+
else if (imported("Translation").has(name)) builder.note(node.start, "<Translation> render props are not rewritten: call t() from @menapo/vi18n inside");
|
|
1510
|
+
}
|
|
1511
|
+
for (const [, child] of childEntries(node)) visit(child);
|
|
1512
|
+
};
|
|
1513
|
+
visit(program);
|
|
1514
|
+
return builder.finish();
|
|
1515
|
+
};
|
|
1516
|
+
return {
|
|
1517
|
+
catalogExtensions: [".json"],
|
|
1518
|
+
catalogsRequired: true,
|
|
1519
|
+
extensions: MODULE_EXTENSIONS,
|
|
1520
|
+
mentions: (text) => /i18next|\bt\s*\(|<Trans\b/.test(text),
|
|
1521
|
+
load(files, env) {
|
|
1522
|
+
sourceLocale = env.config.sourceLocale;
|
|
1523
|
+
for (const file of files) readCatalog(file, env);
|
|
1524
|
+
const source = namespaces(sourceLocale);
|
|
1525
|
+
defaultNs = source.has(DEFAULT_NS) ? DEFAULT_NS : source.size === 1 ? [...source.keys()][0] : null;
|
|
1526
|
+
},
|
|
1527
|
+
planFile: planModule,
|
|
1528
|
+
unusedKeys() {
|
|
1529
|
+
const out = /* @__PURE__ */ new Set();
|
|
1530
|
+
for (const [ns, texts] of namespaces(sourceLocale)) for (const key of texts.keys()) if (!referenced.has(`${ns}:${key}`)) out.add(messageKey(ns, key));
|
|
1531
|
+
return [...out];
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
/** react-i18next's `nodesToString`: the key and default text `<Trans>` derives from its children. */
|
|
1536
|
+
function nodesToString(children) {
|
|
1537
|
+
let text = "";
|
|
1538
|
+
for (const [index, child] of children.entries()) {
|
|
1539
|
+
if (child.type === "JSXText") {
|
|
1540
|
+
text += cleanJsxText(child.value);
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
if (child.type === "JSXExpressionContainer") {
|
|
1544
|
+
const expression = unwrap(child.expression);
|
|
1545
|
+
const literal = stringValue(expression);
|
|
1546
|
+
if (literal !== null) {
|
|
1547
|
+
text += literal;
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
const props = objectProperties(expression);
|
|
1551
|
+
if (props !== null && props.spreads.length === 0) {
|
|
1552
|
+
const format = stringValue(props.properties.get("format")?.value);
|
|
1553
|
+
const names = [...props.properties.keys()].filter((k) => k !== "format");
|
|
1554
|
+
if (names.length === 1) {
|
|
1555
|
+
text += `{{${format === null ? names[0] : `${names[0]}, ${format}`}}}`;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
ok: false,
|
|
1561
|
+
text,
|
|
1562
|
+
reason: `{${text.length > 0 ? "…" : ""}} holds an expression, not text or {{ value }}`
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
if (child.type === "JSXElement") {
|
|
1566
|
+
const name = jsxName(child);
|
|
1567
|
+
const attributes = child.openingElement.attributes;
|
|
1568
|
+
const inner = jsxChildren(child);
|
|
1569
|
+
const basic = BASIC_HTML.has(name);
|
|
1570
|
+
if (inner.length === 0) {
|
|
1571
|
+
text += basic && attributes.length === 0 ? `<${name}/>` : `<${index}></${index}>`;
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
if (basic && attributes.length === 0 && inner.length === 1 && inner[0].type === "JSXText") {
|
|
1575
|
+
text += `<${name}>${cleanJsxText(inner[0].value)}</${name}>`;
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
const nested = nodesToString(inner);
|
|
1579
|
+
if (!nested.ok) return nested;
|
|
1580
|
+
text += `<${index}>${nested.text}</${index}>`;
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
return {
|
|
1584
|
+
ok: false,
|
|
1585
|
+
text,
|
|
1586
|
+
reason: `a ${child.type} child`
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
return {
|
|
1590
|
+
ok: true,
|
|
1591
|
+
text,
|
|
1592
|
+
reason: ""
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
//#endregion
|
|
1596
|
+
export { linguiId as _, annotateHandlers as a, pushLiteral as b, newNaming as c, richCall as d, selfClosingTags as f, formatjsId as g, formatjsAstToIcu as h, isOldSource as i, reactTypes as l, flattenTree as m, createI18nextAdapter as n, elementHandler as o, siteHandlers as p, interpolationNode as r, entry as s, MODULE_EXTENSIONS as t, renameTags as u, parseJson as v, yamlKeyLines as x, parseYaml as y };
|