@verbaly/compiler 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/claude-Bi_Ex2hm.js +53 -0
- package/dist/claude-Bi_Ex2hm.js.map +1 -0
- package/dist/cli.js +1150 -975
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +150 -105
- package/dist/index.js +1080 -968
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/claude-XSRCRBAQ.js +0 -63
- package/dist/claude-XSRCRBAQ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,378 +1,409 @@
|
|
|
1
|
-
// src/analyze.ts
|
|
2
1
|
import { parse } from "@babel/parser";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { RICH_TAGS, createVerbaly, parse as parse$1, parseTags, safeHref } from "verbaly";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { glob } from "tinyglobby";
|
|
8
|
+
import MagicString from "magic-string";
|
|
9
|
+
//#region src/key.ts
|
|
6
10
|
function stableKey(message) {
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
return createHash("sha256").update(message).digest("base64url").slice(0, 8);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/analyze.ts
|
|
15
|
+
const SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
16
|
+
"loc",
|
|
17
|
+
"leadingComments",
|
|
18
|
+
"trailingComments",
|
|
19
|
+
"innerComments",
|
|
20
|
+
"extra"
|
|
21
|
+
]);
|
|
12
22
|
function analyze(code, file) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
23
|
+
const ast = parse(code, {
|
|
24
|
+
sourceType: "module",
|
|
25
|
+
errorRecovery: true,
|
|
26
|
+
plugins: /\.[jt]sx$/.test(file) ? ["typescript", "jsx"] : ["typescript"]
|
|
27
|
+
});
|
|
28
|
+
const tagged = [];
|
|
29
|
+
const usedKeys = [];
|
|
30
|
+
walk(ast.program, (node) => {
|
|
31
|
+
if (node.type === "TaggedTemplateExpression") {
|
|
32
|
+
const tag = node.tag;
|
|
33
|
+
const explicit = explicitId(tag);
|
|
34
|
+
if (!explicit && !isTReference(tag)) return;
|
|
35
|
+
const quasi = node.quasi;
|
|
36
|
+
const message = buildMessage(code, quasi);
|
|
37
|
+
if (!message) return;
|
|
38
|
+
tagged.push({
|
|
39
|
+
key: explicit ? explicit.key : stableKey(message.text),
|
|
40
|
+
message: message.text,
|
|
41
|
+
params: message.params,
|
|
42
|
+
start: node.start,
|
|
43
|
+
end: node.end,
|
|
44
|
+
tagStart: explicit ? explicit.refStart : tag.start,
|
|
45
|
+
tagEnd: explicit ? explicit.refEnd : tag.end,
|
|
46
|
+
file
|
|
47
|
+
});
|
|
48
|
+
} else if (node.type === "CallExpression") {
|
|
49
|
+
const callee = node.callee;
|
|
50
|
+
if (!isTReference(callee)) return;
|
|
51
|
+
const first = node.arguments[0];
|
|
52
|
+
if (first?.type === "StringLiteral") usedKeys.push({
|
|
53
|
+
key: first.value,
|
|
54
|
+
file
|
|
55
|
+
});
|
|
56
|
+
} else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys);
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
tagged,
|
|
60
|
+
usedKeys
|
|
61
|
+
};
|
|
52
62
|
}
|
|
53
63
|
function handleTrans(code, node, file, tagged, usedKeys) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
64
|
+
const opening = node.openingElement;
|
|
65
|
+
const nameNode = opening.name;
|
|
66
|
+
if (nameNode.type !== "JSXIdentifier" || nameNode.name !== "Trans") return;
|
|
67
|
+
const attrs = opening.attributes;
|
|
68
|
+
const idAttr = attrs.find((a) => a.type === "JSXAttribute" && a.name.name === "id");
|
|
69
|
+
const children = node.children;
|
|
70
|
+
if (idAttr) {
|
|
71
|
+
const value = idAttr.value;
|
|
72
|
+
if (value?.type !== "StringLiteral") return;
|
|
73
|
+
const id = value.value;
|
|
74
|
+
if (attrs.length === 1 && children?.length) {
|
|
75
|
+
const built = buildTransMessage(code, children);
|
|
76
|
+
if (built?.text.trim()) {
|
|
77
|
+
tagged.push({
|
|
78
|
+
key: id,
|
|
79
|
+
message: built.text,
|
|
80
|
+
params: built.params,
|
|
81
|
+
start: node.start,
|
|
82
|
+
end: node.end,
|
|
83
|
+
tagStart: nameNode.start,
|
|
84
|
+
tagEnd: nameNode.end,
|
|
85
|
+
file,
|
|
86
|
+
jsx: {
|
|
87
|
+
name: nameNode.name,
|
|
88
|
+
components: built.components
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
usedKeys.push({
|
|
95
|
+
key: id,
|
|
96
|
+
file
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (attrs.length > 0) return;
|
|
101
|
+
if (!children?.length) return;
|
|
102
|
+
const built = buildTransMessage(code, children);
|
|
103
|
+
if (!built || !built.text.trim()) return;
|
|
104
|
+
tagged.push({
|
|
105
|
+
key: stableKey(built.text),
|
|
106
|
+
message: built.text,
|
|
107
|
+
params: built.params,
|
|
108
|
+
start: node.start,
|
|
109
|
+
end: node.end,
|
|
110
|
+
tagStart: nameNode.start,
|
|
111
|
+
tagEnd: nameNode.end,
|
|
112
|
+
file,
|
|
113
|
+
jsx: {
|
|
114
|
+
name: nameNode.name,
|
|
115
|
+
components: built.components
|
|
116
|
+
}
|
|
117
|
+
});
|
|
99
118
|
}
|
|
100
119
|
function explicitId(tag) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
if (tag.type !== "CallExpression") return void 0;
|
|
121
|
+
const callee = tag.callee;
|
|
122
|
+
if (callee.type !== "MemberExpression" || callee.computed) return void 0;
|
|
123
|
+
const prop = callee.property;
|
|
124
|
+
if (prop.type !== "Identifier" || prop.name !== "id") return void 0;
|
|
125
|
+
const obj = callee.object;
|
|
126
|
+
if (!isTReference(obj)) return void 0;
|
|
127
|
+
const args = tag.arguments;
|
|
128
|
+
const first = args[0];
|
|
129
|
+
if (args.length !== 1 || first?.type !== "StringLiteral") return void 0;
|
|
130
|
+
return {
|
|
131
|
+
key: first.value,
|
|
132
|
+
refStart: obj.start,
|
|
133
|
+
refEnd: obj.end
|
|
134
|
+
};
|
|
112
135
|
}
|
|
113
136
|
function buildTransMessage(code, children) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
137
|
+
const params = [];
|
|
138
|
+
const components = [];
|
|
139
|
+
const takenParams = /* @__PURE__ */ new Map();
|
|
140
|
+
const takenTags = /* @__PURE__ */ new Map();
|
|
141
|
+
function walkChildren(nodes) {
|
|
142
|
+
let text = "";
|
|
143
|
+
for (const child of nodes) if (child.type === "JSXText") text += escapeText(cleanJsxText(child.value));
|
|
144
|
+
else if (child.type === "JSXExpressionContainer") {
|
|
145
|
+
const expr = child.expression;
|
|
146
|
+
if (expr.type === "JSXEmptyExpression") continue;
|
|
147
|
+
if (expr.type === "StringLiteral") {
|
|
148
|
+
text += escapeText(expr.value);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (expr.type === "TaggedTemplateExpression") return void 0;
|
|
152
|
+
const source = code.slice(expr.start, expr.end);
|
|
153
|
+
const name = uniqueName(deriveName(expr, params.length), source, takenParams);
|
|
154
|
+
params.push({
|
|
155
|
+
name,
|
|
156
|
+
start: expr.start,
|
|
157
|
+
end: expr.end
|
|
158
|
+
});
|
|
159
|
+
text += `{${name}}`;
|
|
160
|
+
} else if (child.type === "JSXElement") {
|
|
161
|
+
const opening = child.openingElement;
|
|
162
|
+
const nameNode = opening.name;
|
|
163
|
+
if (nameNode.type !== "JSXIdentifier" || nameNode.name === "Trans") return void 0;
|
|
164
|
+
const source = selfClosedSource(code, opening);
|
|
165
|
+
const tag = uniqueName(nameNode.name.toLowerCase(), source, takenTags);
|
|
166
|
+
if (!components.some((c) => c.name === tag)) components.push({
|
|
167
|
+
name: tag,
|
|
168
|
+
source
|
|
169
|
+
});
|
|
170
|
+
if (!child.closingElement) text += `<${tag}/>`;
|
|
171
|
+
else {
|
|
172
|
+
const inner = walkChildren(child.children);
|
|
173
|
+
if (inner === void 0) return void 0;
|
|
174
|
+
text += `<${tag}>${inner}</${tag}>`;
|
|
175
|
+
}
|
|
176
|
+
} else return;
|
|
177
|
+
return text;
|
|
178
|
+
}
|
|
179
|
+
const text = walkChildren(children);
|
|
180
|
+
if (text === void 0) return void 0;
|
|
181
|
+
return {
|
|
182
|
+
text,
|
|
183
|
+
params,
|
|
184
|
+
components
|
|
185
|
+
};
|
|
158
186
|
}
|
|
159
187
|
function cleanJsxText(raw) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
188
|
+
const lines = raw.split(/\r\n|[\r\n]/);
|
|
189
|
+
let out = "";
|
|
190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
191
|
+
let line = lines[i].replace(/\t/g, " ");
|
|
192
|
+
if (i !== 0) line = line.replace(/^ +/, "");
|
|
193
|
+
if (i !== lines.length - 1) line = line.replace(/ +$/, "");
|
|
194
|
+
if (!line) continue;
|
|
195
|
+
if (out && i !== 0) out += " ";
|
|
196
|
+
out += line;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
171
199
|
}
|
|
172
200
|
function selfClosedSource(code, opening) {
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
const src = code.slice(opening.start, opening.end);
|
|
202
|
+
return src.endsWith("/>") ? src : `${src.slice(0, -1).trimEnd()} />`;
|
|
175
203
|
}
|
|
176
204
|
function isTReference(node) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
205
|
+
if (node.type === "Identifier") return node.name === "t";
|
|
206
|
+
if (node.type === "MemberExpression" && !node.computed) {
|
|
207
|
+
const prop = node.property;
|
|
208
|
+
return prop.type === "Identifier" && prop.name === "t";
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
183
211
|
}
|
|
184
212
|
function buildMessage(code, quasi) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
213
|
+
const quasis = quasi.quasis;
|
|
214
|
+
const expressions = quasi.expressions;
|
|
215
|
+
const params = [];
|
|
216
|
+
const taken = /* @__PURE__ */ new Map();
|
|
217
|
+
let text = escapeText(cookedValue(quasis[0]));
|
|
218
|
+
for (let i = 0; i < expressions.length; i++) {
|
|
219
|
+
const expr = expressions[i];
|
|
220
|
+
if (!expr) return void 0;
|
|
221
|
+
const source = code.slice(expr.start, expr.end);
|
|
222
|
+
const name = uniqueName(deriveName(expr, i), source, taken);
|
|
223
|
+
params.push({
|
|
224
|
+
name,
|
|
225
|
+
start: expr.start,
|
|
226
|
+
end: expr.end
|
|
227
|
+
});
|
|
228
|
+
text += `{${name}}` + escapeText(cookedValue(quasis[i + 1]));
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
text,
|
|
232
|
+
params
|
|
233
|
+
};
|
|
199
234
|
}
|
|
200
235
|
function cookedValue(element) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
236
|
+
if (!element) return "";
|
|
237
|
+
const value = element.value;
|
|
238
|
+
return value.cooked ?? value.raw;
|
|
204
239
|
}
|
|
205
240
|
function escapeText(text) {
|
|
206
|
-
|
|
241
|
+
return text.replace(/[{}]/g, (m) => m + m);
|
|
207
242
|
}
|
|
208
243
|
function deriveName(expr, index) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
244
|
+
if (expr.type === "Identifier") return expr.name;
|
|
245
|
+
if (expr.type === "MemberExpression" && !expr.computed) {
|
|
246
|
+
const prop = expr.property;
|
|
247
|
+
if (prop.type === "Identifier") return prop.name;
|
|
248
|
+
}
|
|
249
|
+
return `_${index}`;
|
|
215
250
|
}
|
|
216
251
|
function uniqueName(base, source, taken) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
function walk(node,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
} else if (isNode(value)) {
|
|
235
|
-
walk(value, visit2);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
252
|
+
let name = base;
|
|
253
|
+
let n = 2;
|
|
254
|
+
while (taken.has(name) && taken.get(name) !== source) {
|
|
255
|
+
name = `${base}${n}`;
|
|
256
|
+
n += 1;
|
|
257
|
+
}
|
|
258
|
+
taken.set(name, source);
|
|
259
|
+
return name;
|
|
260
|
+
}
|
|
261
|
+
function walk(node, visit) {
|
|
262
|
+
visit(node);
|
|
263
|
+
for (const [key, value] of Object.entries(node)) {
|
|
264
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
265
|
+
if (Array.isArray(value)) {
|
|
266
|
+
for (const item of value) if (isNode(item)) walk(item, visit);
|
|
267
|
+
} else if (isNode(value)) walk(value, visit);
|
|
268
|
+
}
|
|
238
269
|
}
|
|
239
270
|
function isNode(value) {
|
|
240
|
-
|
|
271
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
241
272
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
245
|
-
import { join } from "path";
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/catalog.ts
|
|
246
275
|
function catalogPath(cfg, locale) {
|
|
247
|
-
|
|
276
|
+
return join(cfg.dir, `${locale}.json`);
|
|
248
277
|
}
|
|
249
278
|
function readCatalog(cfg, locale) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
279
|
+
try {
|
|
280
|
+
return JSON.parse(readFileSync(catalogPath(cfg, locale), "utf8"));
|
|
281
|
+
} catch {
|
|
282
|
+
return {};
|
|
283
|
+
}
|
|
255
284
|
}
|
|
256
285
|
function loadCatalogs(cfg) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
const catalogs = {};
|
|
287
|
+
for (const locale of cfg.locales) catalogs[locale] = readCatalog(cfg, locale);
|
|
288
|
+
return catalogs;
|
|
260
289
|
}
|
|
261
290
|
function serializeCatalog(catalog) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
291
|
+
const sorted = {};
|
|
292
|
+
for (const key of Object.keys(catalog).sort()) {
|
|
293
|
+
const value = catalog[key];
|
|
294
|
+
if (value !== void 0) sorted[key] = value;
|
|
295
|
+
}
|
|
296
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
268
297
|
}
|
|
269
298
|
function writeCatalog(cfg, locale, catalog) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
299
|
+
const serialized = serializeCatalog(catalog);
|
|
300
|
+
mkdirSync(cfg.dir, { recursive: true });
|
|
301
|
+
writeFileSync(catalogPath(cfg, locale), serialized);
|
|
302
|
+
return serialized;
|
|
274
303
|
}
|
|
275
|
-
|
|
276
|
-
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/check.ts
|
|
277
306
|
function check(cfg, catalogs, registry) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
307
|
+
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
308
|
+
const extracted = registry.messages();
|
|
309
|
+
const unknown = [];
|
|
310
|
+
for (const [key, files] of registry.usedKeys()) if (!(extracted.has(key) || cfg.locales.some((locale) => catalogs[locale]?.[key] !== void 0))) unknown.push({
|
|
311
|
+
key,
|
|
312
|
+
files
|
|
313
|
+
});
|
|
314
|
+
const needed = /* @__PURE__ */ new Set([...extracted.keys(), ...Object.keys(source)]);
|
|
315
|
+
const missing = [];
|
|
316
|
+
for (const key of extracted.keys()) if (!source[key]) missing.push({
|
|
317
|
+
locale: cfg.sourceLocale,
|
|
318
|
+
key,
|
|
319
|
+
source: extracted.get(key)?.message
|
|
320
|
+
});
|
|
321
|
+
for (const locale of cfg.locales) {
|
|
322
|
+
if (locale === cfg.sourceLocale) continue;
|
|
323
|
+
for (const key of needed) if (!catalogs[locale]?.[key]) missing.push({
|
|
324
|
+
locale,
|
|
325
|
+
key,
|
|
326
|
+
source: source[key] ?? extracted.get(key)?.message
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
ok: missing.length === 0 && unknown.length === 0,
|
|
331
|
+
missing,
|
|
332
|
+
unknown
|
|
333
|
+
};
|
|
301
334
|
}
|
|
302
335
|
function formatCheckResult(result) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
return lines.join("\n");
|
|
336
|
+
const lines = [];
|
|
337
|
+
if (result.missing.length > 0) {
|
|
338
|
+
lines.push("missing translations:");
|
|
339
|
+
for (const entry of result.missing) {
|
|
340
|
+
const hint = entry.source ? ` — "${truncate(entry.source, 40)}"` : "";
|
|
341
|
+
lines.push(` [${entry.locale}] ${entry.key}${hint}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (result.unknown.length > 0) {
|
|
345
|
+
lines.push("unknown keys (not in any catalog):");
|
|
346
|
+
for (const entry of result.unknown) lines.push(` ${entry.key} — used in ${entry.files.join(", ")}`);
|
|
347
|
+
}
|
|
348
|
+
return lines.join("\n");
|
|
318
349
|
}
|
|
319
350
|
function truncate(text, max) {
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
351
|
+
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/params.ts
|
|
355
|
+
const PLURAL_KEYS = /* @__PURE__ */ new Set([
|
|
356
|
+
"zero",
|
|
357
|
+
"one",
|
|
358
|
+
"two",
|
|
359
|
+
"few",
|
|
360
|
+
"many"
|
|
361
|
+
]);
|
|
362
|
+
const NUMBER_FORMATS = /* @__PURE__ */ new Set([
|
|
363
|
+
"number",
|
|
364
|
+
"integer",
|
|
365
|
+
"percent",
|
|
366
|
+
"currency"
|
|
367
|
+
]);
|
|
368
|
+
const DATE_FORMATS = /* @__PURE__ */ new Set(["date", "time"]);
|
|
332
369
|
function collectParams(message) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
370
|
+
const out = /* @__PURE__ */ new Map();
|
|
371
|
+
visit(parse$1(message), out);
|
|
372
|
+
return out;
|
|
336
373
|
}
|
|
337
374
|
function visit(nodes, out) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
} else {
|
|
354
|
-
types.add("unknown");
|
|
355
|
-
}
|
|
356
|
-
}
|
|
375
|
+
for (const node of nodes) {
|
|
376
|
+
if (node.kind !== "param") continue;
|
|
377
|
+
const types = out.get(node.name) ?? /* @__PURE__ */ new Set();
|
|
378
|
+
out.set(node.name, types);
|
|
379
|
+
if (node.variants) {
|
|
380
|
+
for (const [key, body] of node.variants) {
|
|
381
|
+
if (PLURAL_KEYS.has(key) || /^=\d+$/.test(key)) types.add("number");
|
|
382
|
+
else if (key !== "other") types.add("string");
|
|
383
|
+
visit(body, out);
|
|
384
|
+
}
|
|
385
|
+
if (types.size === 0) types.add("string");
|
|
386
|
+
} else if (node.format && NUMBER_FORMATS.has(node.format)) types.add("number");
|
|
387
|
+
else if (node.format && DATE_FORMATS.has(node.format)) types.add("date");
|
|
388
|
+
else types.add("unknown");
|
|
389
|
+
}
|
|
357
390
|
}
|
|
358
391
|
function renderParamType(types) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
392
|
+
if (types.has("unknown") || types.size === 0) return "unknown";
|
|
393
|
+
const parts = [];
|
|
394
|
+
if (types.has("number")) parts.push("number");
|
|
395
|
+
if (types.has("string")) parts.push("string");
|
|
396
|
+
if (types.has("date")) parts.push("Date | number | string");
|
|
397
|
+
return parts.join(" | ");
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/codegen.ts
|
|
401
|
+
const VIRTUAL_ID = "virtual:verbaly";
|
|
369
402
|
function generateRuntimeModule(cfg) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
).join("\n");
|
|
375
|
-
return `import { createVerbaly } from 'verbaly';
|
|
403
|
+
const others = cfg.locales.filter((locale) => locale !== cfg.sourceLocale);
|
|
404
|
+
const src = JSON.stringify(cfg.sourceLocale);
|
|
405
|
+
const loaders = others.map((locale) => ` ${JSON.stringify(locale)}: () => import('${VIRTUAL_ID}/locale/${locale}'),`).join("\n");
|
|
406
|
+
return `import { createVerbaly } from 'verbaly';
|
|
376
407
|
import source from '${VIRTUAL_ID}/locale/${cfg.sourceLocale}';
|
|
377
408
|
|
|
378
409
|
const v = createVerbaly({
|
|
@@ -402,21 +433,19 @@ export async function setLocale(locale) {
|
|
|
402
433
|
`;
|
|
403
434
|
}
|
|
404
435
|
function generateLocaleModule(catalog) {
|
|
405
|
-
|
|
406
|
-
`;
|
|
436
|
+
return `export default ${JSON.stringify(catalog)};\n`;
|
|
407
437
|
}
|
|
408
438
|
function generateDts(entries) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return `// generated by verbaly \u2014 do not edit
|
|
439
|
+
const lines = [];
|
|
440
|
+
for (const [key, message] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
441
|
+
const params = collectParams(message);
|
|
442
|
+
if (params.size === 0) lines.push(` ${JSON.stringify(key)}: never;`);
|
|
443
|
+
else {
|
|
444
|
+
const fields = [...params.entries()].map(([name, types]) => `${JSON.stringify(name)}: ${renderParamType(types)}`).join("; ");
|
|
445
|
+
lines.push(` ${JSON.stringify(key)}: { ${fields} };`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return `// generated by verbaly — do not edit
|
|
420
449
|
declare module 'virtual:verbaly' {
|
|
421
450
|
export interface VerbalyMessages {
|
|
422
451
|
${lines.join("\n")}
|
|
@@ -448,700 +477,783 @@ declare module 'virtual:verbaly/locale/*' {
|
|
|
448
477
|
`;
|
|
449
478
|
}
|
|
450
479
|
function writeDts(cfg, entries) {
|
|
451
|
-
|
|
480
|
+
writeFileSync(join(cfg.root, "verbaly.d.ts"), generateDts(entries));
|
|
452
481
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
456
|
-
import { join as join3, resolve } from "path";
|
|
457
|
-
import { pathToFileURL } from "url";
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/config.ts
|
|
458
484
|
function resolveConfig(config = {}) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
485
|
+
const root = resolve(config.root ?? process.cwd());
|
|
486
|
+
const dir = resolve(root, config.dir ?? "locales");
|
|
487
|
+
const sourceLocale = config.sourceLocale ?? "en";
|
|
488
|
+
const locales = /* @__PURE__ */ new Set([sourceLocale, ...config.locales ?? []]);
|
|
489
|
+
if (existsSync(dir)) {
|
|
490
|
+
for (const file of readdirSync(dir)) if (file.endsWith(".json")) locales.add(file.slice(0, -5));
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
root,
|
|
494
|
+
dir,
|
|
495
|
+
sourceLocale,
|
|
496
|
+
locales: [...locales],
|
|
497
|
+
include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
|
|
498
|
+
exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
|
|
499
|
+
translate: config.translate ?? {},
|
|
500
|
+
render: config.render ?? {}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const CONFIG_FILES = [
|
|
504
|
+
"verbaly.config.js",
|
|
505
|
+
"verbaly.config.mjs",
|
|
506
|
+
"verbaly.config.ts",
|
|
507
|
+
"verbaly.config.mts",
|
|
508
|
+
"verbaly.config.json"
|
|
509
|
+
];
|
|
510
|
+
function findConfigFile(root) {
|
|
511
|
+
return CONFIG_FILES.find((name) => existsSync(join(root, name)));
|
|
478
512
|
}
|
|
479
513
|
async function loadConfigFile(root) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const jsonPath = join3(root, "verbaly.config.json");
|
|
492
|
-
if (existsSync(jsonPath)) {
|
|
493
|
-
return JSON.parse(readFileSync2(jsonPath, "utf8"));
|
|
494
|
-
}
|
|
495
|
-
return {};
|
|
514
|
+
for (const name of ["verbaly.config.js", "verbaly.config.mjs"]) {
|
|
515
|
+
const path = join(root, name);
|
|
516
|
+
if (existsSync(path)) return (await import(pathToFileURL(path).href)).default ?? {};
|
|
517
|
+
}
|
|
518
|
+
for (const name of ["verbaly.config.ts", "verbaly.config.mts"]) {
|
|
519
|
+
const path = join(root, name);
|
|
520
|
+
if (existsSync(path)) return loadTsConfig(path);
|
|
521
|
+
}
|
|
522
|
+
const jsonPath = join(root, "verbaly.config.json");
|
|
523
|
+
if (existsSync(jsonPath)) return JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
524
|
+
return {};
|
|
496
525
|
}
|
|
497
526
|
async function loadTsConfig(path) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
function isModuleNotFound(error, name) {
|
|
512
|
-
return error instanceof Error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes(name);
|
|
527
|
+
try {
|
|
528
|
+
const { bundleRequire } = await import("bundle-require");
|
|
529
|
+
const { mod } = await bundleRequire({ filepath: path });
|
|
530
|
+
return mod.default ?? {};
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (isModuleNotFound$1(error, "esbuild")) throw new Error(`[verbaly] ${path} needs esbuild — install it: pnpm add -D esbuild`, { cause: error });
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function isModuleNotFound$1(error, name) {
|
|
537
|
+
return error instanceof Error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes(name);
|
|
513
538
|
}
|
|
514
539
|
async function loadConfig(root, overrides = {}) {
|
|
515
|
-
|
|
516
|
-
|
|
540
|
+
return resolveConfig({
|
|
541
|
+
...await loadConfigFile(root),
|
|
542
|
+
...definedOnly(overrides),
|
|
543
|
+
root
|
|
544
|
+
});
|
|
517
545
|
}
|
|
518
546
|
function definedOnly(config) {
|
|
519
|
-
|
|
520
|
-
Object.entries(config).filter(([, value]) => value !== void 0)
|
|
521
|
-
);
|
|
547
|
+
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
|
|
522
548
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
526
|
-
import { glob } from "tinyglobby";
|
|
527
|
-
|
|
528
|
-
// src/registry.ts
|
|
549
|
+
//#endregion
|
|
550
|
+
//#region src/registry.ts
|
|
529
551
|
var MessageRegistry = class {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
for (const used of analysis.usedKeys) {
|
|
559
|
-
const files = out.get(used.key) ?? [];
|
|
560
|
-
if (!files.includes(used.file)) files.push(used.file);
|
|
561
|
-
out.set(used.key, files);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return out;
|
|
565
|
-
}
|
|
552
|
+
files = /* @__PURE__ */ new Map();
|
|
553
|
+
update(file, analysis) {
|
|
554
|
+
this.files.set(file, analysis);
|
|
555
|
+
}
|
|
556
|
+
remove(file) {
|
|
557
|
+
this.files.delete(file);
|
|
558
|
+
}
|
|
559
|
+
messages() {
|
|
560
|
+
const out = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
|
|
562
|
+
const existing = out.get(msg.key);
|
|
563
|
+
if (existing) {
|
|
564
|
+
if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} — second dropped.`);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
out.set(msg.key, msg);
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
usedKeys() {
|
|
572
|
+
const out = /* @__PURE__ */ new Map();
|
|
573
|
+
for (const analysis of this.files.values()) for (const used of analysis.usedKeys) {
|
|
574
|
+
const files = out.get(used.key) ?? [];
|
|
575
|
+
if (!files.includes(used.file)) files.push(used.file);
|
|
576
|
+
out.set(used.key, files);
|
|
577
|
+
}
|
|
578
|
+
return out;
|
|
579
|
+
}
|
|
566
580
|
};
|
|
567
|
-
|
|
568
|
-
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region src/extract.ts
|
|
569
583
|
async function extractProject(cfg) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
return registry;
|
|
584
|
+
const files = await glob(cfg.include, {
|
|
585
|
+
cwd: cfg.root,
|
|
586
|
+
ignore: cfg.exclude,
|
|
587
|
+
absolute: true
|
|
588
|
+
});
|
|
589
|
+
const registry = new MessageRegistry();
|
|
590
|
+
for (const file of files) registry.update(file, analyze(readFileSync(file, "utf8"), file));
|
|
591
|
+
return registry;
|
|
580
592
|
}
|
|
581
593
|
function syncCatalogs(cfg, catalogs, registry) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return { added };
|
|
594
|
+
const added = {};
|
|
595
|
+
const source = catalogs[cfg.sourceLocale] ??= {};
|
|
596
|
+
for (const [key, msg] of registry.messages()) if (source[key] !== msg.message) {
|
|
597
|
+
source[key] = msg.message;
|
|
598
|
+
(added[cfg.sourceLocale] ??= []).push(key);
|
|
599
|
+
}
|
|
600
|
+
const needed = Object.keys(source);
|
|
601
|
+
for (const locale of cfg.locales) {
|
|
602
|
+
if (locale === cfg.sourceLocale) continue;
|
|
603
|
+
const catalog = catalogs[locale] ??= {};
|
|
604
|
+
for (const key of needed) if (catalog[key] === void 0) {
|
|
605
|
+
catalog[key] = "";
|
|
606
|
+
(added[locale] ??= []).push(key);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return { added };
|
|
602
610
|
}
|
|
603
611
|
function pruneCatalogs(cfg, catalogs, registry) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
612
|
+
const keep = /* @__PURE__ */ new Set([...registry.messages().keys(), ...registry.usedKeys().keys()]);
|
|
613
|
+
const removed = {};
|
|
614
|
+
for (const locale of cfg.locales) {
|
|
615
|
+
const catalog = catalogs[locale];
|
|
616
|
+
if (!catalog) continue;
|
|
617
|
+
for (const key of Object.keys(catalog)) if (!keep.has(key)) {
|
|
618
|
+
delete catalog[key];
|
|
619
|
+
(removed[locale] ??= []).push(key);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return removed;
|
|
623
|
+
}
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/init.ts
|
|
626
|
+
const BUNDLERS = [
|
|
627
|
+
"vite",
|
|
628
|
+
"webpack",
|
|
629
|
+
"rollup",
|
|
630
|
+
"rspack",
|
|
631
|
+
"esbuild"
|
|
632
|
+
];
|
|
633
|
+
function detectBundler(root) {
|
|
634
|
+
const path = join(root, "package.json");
|
|
635
|
+
if (!existsSync(path)) return void 0;
|
|
636
|
+
try {
|
|
637
|
+
const pkg = JSON.parse(readFileSync(path, "utf8"));
|
|
638
|
+
const deps = {
|
|
639
|
+
...pkg.dependencies,
|
|
640
|
+
...pkg.devDependencies
|
|
641
|
+
};
|
|
642
|
+
return BUNDLERS.find((name) => deps[name]);
|
|
643
|
+
} catch {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function configSource(options, typescript) {
|
|
648
|
+
const fields = [` sourceLocale: '${options.sourceLocale ?? "en"}',`];
|
|
649
|
+
if (options.locales?.length) fields.push(` locales: [${options.locales.map((l) => `'${l}'`).join(", ")}],`);
|
|
650
|
+
if (options.dir) fields.push(` dir: '${options.dir}',`);
|
|
651
|
+
const body = `export default {\n${fields.join("\n")}\n}`;
|
|
652
|
+
if (typescript) return `import type { VerbalyConfig } from '@verbaly/compiler';\n\n${body} satisfies VerbalyConfig;\n`;
|
|
653
|
+
return `/** @type {import('@verbaly/compiler').VerbalyConfig} */\n${body};\n`;
|
|
654
|
+
}
|
|
655
|
+
function init(options = {}) {
|
|
656
|
+
const root = options.root ?? process.cwd();
|
|
657
|
+
const created = [];
|
|
658
|
+
const skipped = [];
|
|
659
|
+
const existing = findConfigFile(root);
|
|
660
|
+
const typescript = existsSync(join(root, "tsconfig.json"));
|
|
661
|
+
const configFile = existing ?? (typescript ? "verbaly.config.ts" : "verbaly.config.mjs");
|
|
662
|
+
if (existing) skipped.push(existing);
|
|
663
|
+
else {
|
|
664
|
+
writeFileSync(join(root, configFile), configSource(options, typescript));
|
|
665
|
+
created.push(configFile);
|
|
666
|
+
}
|
|
667
|
+
const dir = join(root, options.dir ?? "locales");
|
|
668
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
669
|
+
for (const locale of /* @__PURE__ */ new Set([options.sourceLocale ?? "en", ...options.locales ?? []])) {
|
|
670
|
+
const file = join(dir, `${locale}.json`);
|
|
671
|
+
const label = relative(root, file).replaceAll("\\", "/");
|
|
672
|
+
if (existsSync(file)) skipped.push(label);
|
|
673
|
+
else {
|
|
674
|
+
writeFileSync(file, "{}\n");
|
|
675
|
+
created.push(label);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const bundler = detectBundler(root);
|
|
679
|
+
const next = [];
|
|
680
|
+
if (bundler === "vite") next.push("install the plugin: pnpm add -D @verbaly/vite", "add verbaly() to the plugins in vite.config");
|
|
681
|
+
else if (bundler) next.push("install the plugin: pnpm add -D @verbaly/unplugin", `add the verbaly ${bundler} plugin to your build config`);
|
|
682
|
+
else next.push("run \"verbaly extract\" after writing your first t`…` message");
|
|
683
|
+
return {
|
|
684
|
+
created,
|
|
685
|
+
skipped,
|
|
686
|
+
bundler,
|
|
687
|
+
configFile,
|
|
688
|
+
next
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/doctor.ts
|
|
693
|
+
const PREVIEW = 5;
|
|
694
|
+
async function doctor(cfg) {
|
|
695
|
+
const entries = [];
|
|
696
|
+
const ok = (check, message) => entries.push({
|
|
697
|
+
level: "ok",
|
|
698
|
+
check,
|
|
699
|
+
message
|
|
700
|
+
});
|
|
701
|
+
const warn = (check, message, fix) => entries.push({
|
|
702
|
+
level: "warn",
|
|
703
|
+
check,
|
|
704
|
+
message,
|
|
705
|
+
fix
|
|
706
|
+
});
|
|
707
|
+
const error = (check, message, fix) => entries.push({
|
|
708
|
+
level: "error",
|
|
709
|
+
check,
|
|
710
|
+
message,
|
|
711
|
+
fix
|
|
712
|
+
});
|
|
713
|
+
const rel = (path) => relative(cfg.root, path).replaceAll("\\", "/");
|
|
714
|
+
const configFile = findConfigFile(cfg.root);
|
|
715
|
+
if (configFile) ok("config", `${configFile} found`);
|
|
716
|
+
else warn("config", "no config file — running on defaults", "run `npx verbaly init`");
|
|
717
|
+
const catalogs = {};
|
|
718
|
+
let catalogsHealthy = true;
|
|
719
|
+
if (!existsSync(cfg.dir)) {
|
|
720
|
+
catalogsHealthy = false;
|
|
721
|
+
error("catalogs", `catalogs directory ${rel(cfg.dir)}/ does not exist`, "run `npx verbaly init` to scaffold it");
|
|
722
|
+
} else {
|
|
723
|
+
for (const locale of cfg.locales) {
|
|
724
|
+
const file = join(cfg.dir, `${locale}.json`);
|
|
725
|
+
if (!existsSync(file)) {
|
|
726
|
+
catalogsHealthy = false;
|
|
727
|
+
error(`locale ${locale}`, `${rel(file)} is missing`, "run `npx verbaly extract` to create it");
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
732
|
+
const bad = Object.entries(parsed).find(([, value]) => typeof value !== "string");
|
|
733
|
+
if (bad) {
|
|
734
|
+
catalogsHealthy = false;
|
|
735
|
+
error(`locale ${locale}`, `${rel(file)} has a non-string value at "${bad[0]}"`, "catalogs are flat key → string JSON; fix the value");
|
|
736
|
+
} else catalogs[locale] = parsed;
|
|
737
|
+
} catch {
|
|
738
|
+
catalogsHealthy = false;
|
|
739
|
+
error(`locale ${locale}`, `${rel(file)} is not valid JSON`, "repair the file (or delete it and run `npx verbaly extract`)");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (catalogsHealthy) ok("catalogs", `${cfg.locales.length} locales (${cfg.locales.join(", ")}) in ${rel(cfg.dir)}/`);
|
|
743
|
+
}
|
|
744
|
+
const source = catalogs[cfg.sourceLocale];
|
|
745
|
+
if (source && Object.keys(source).length === 0) warn("source", `source catalog ${cfg.sourceLocale}.json is empty`, "write your first t`…` message and run `npx verbaly extract`");
|
|
746
|
+
const deps = readDeps(cfg.root);
|
|
747
|
+
const bundler = detectBundler(cfg.root);
|
|
748
|
+
const wired = deps["@verbaly/vite"] || deps["@verbaly/unplugin"];
|
|
749
|
+
if (!bundler) ok("plugin", "no bundler detected — CLI flow (extract/check) applies");
|
|
750
|
+
else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
|
|
751
|
+
else if (bundler === "vite") warn("plugin", "vite detected but @verbaly/vite is not installed", "pnpm add -D @verbaly/vite and add verbaly() to the plugins in vite.config");
|
|
752
|
+
else warn("plugin", `${bundler} detected but @verbaly/unplugin is not installed`, `pnpm add -D @verbaly/unplugin and add the verbaly ${bundler} plugin to your build config`);
|
|
753
|
+
if (source) {
|
|
754
|
+
const dtsPath = join(cfg.root, "verbaly.d.ts");
|
|
755
|
+
if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
|
|
756
|
+
else if (readFileSync(dtsPath, "utf8") !== generateDts(new Map(Object.entries(source)))) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
|
|
757
|
+
else ok("types", "verbaly.d.ts is up to date");
|
|
758
|
+
}
|
|
759
|
+
const registry = await extractProject(cfg);
|
|
760
|
+
if (source) {
|
|
761
|
+
const extracted = registry.messages();
|
|
762
|
+
const used = registry.usedKeys();
|
|
763
|
+
const orphans = Object.keys(source).filter((key) => !extracted.has(key) && !used.has(key));
|
|
764
|
+
if (orphans.length > 0) warn("orphans", `${orphans.length} catalog ${orphans.length === 1 ? "key is" : "keys are"} no longer referenced (${preview(orphans)})`, "run `npx verbaly extract --prune` to drop them");
|
|
765
|
+
else ok("orphans", "no orphan keys");
|
|
766
|
+
}
|
|
767
|
+
if (catalogsHealthy) {
|
|
768
|
+
const result = check(cfg, catalogs, registry);
|
|
769
|
+
if (result.unknown.length > 0) error("keys", `${result.unknown.length} unknown ${result.unknown.length === 1 ? "key" : "keys"} used in code (${preview(result.unknown.map((u) => u.key))})`, "fix the key or add it to the catalogs — `npx verbaly check` for details");
|
|
770
|
+
if (result.missing.length > 0) {
|
|
771
|
+
const locales = [...new Set(result.missing.map((m) => m.locale))];
|
|
772
|
+
warn("translations", `${result.missing.length} missing ${result.missing.length === 1 ? "translation" : "translations"} (${locales.join(", ")})`, "run `npx verbaly translate` or fill the catalogs — `npx verbaly check` for details");
|
|
773
|
+
}
|
|
774
|
+
if (result.ok) ok("translations", "all translations complete");
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
ok: entries.every((entry) => entry.level !== "error"),
|
|
778
|
+
entries
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function preview(keys) {
|
|
782
|
+
const head = keys.slice(0, PREVIEW).join(", ");
|
|
783
|
+
return keys.length > PREVIEW ? `${head}, …` : head;
|
|
784
|
+
}
|
|
785
|
+
function readDeps(root) {
|
|
786
|
+
try {
|
|
787
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
788
|
+
return {
|
|
789
|
+
...pkg.dependencies,
|
|
790
|
+
...pkg.devDependencies
|
|
791
|
+
};
|
|
792
|
+
} catch {
|
|
793
|
+
return {};
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region src/providers/claude.ts
|
|
798
|
+
const DEFAULT_MODEL = "claude-sonnet-5";
|
|
799
|
+
const SYSTEM = `You translate UI strings for the Verbaly i18n library.
|
|
622
800
|
Rules:
|
|
623
801
|
- Translate only the human-readable text, naturally for the target locale.
|
|
624
802
|
- Preserve verbatim: placeholders like {name}, format specs like {price:currency/EUR}, variant blocks like {count | one: ... | other: # ...} (translate only the text inside each variant, keep keys and # as-is), ICU syntax, named tags like <em>...</em> and escapes {{ }} || ##.
|
|
625
803
|
- Keys are opaque identifiers: return exactly the same keys, never translate or rename them.`;
|
|
626
804
|
function claudeProvider(options = {}) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
805
|
+
return async (request) => {
|
|
806
|
+
const text = (await new (await (loadSdk()))(options.apiKey ? { apiKey: options.apiKey } : {}).messages.create({
|
|
807
|
+
model: options.model ?? DEFAULT_MODEL,
|
|
808
|
+
max_tokens: options.maxTokens ?? 16e3,
|
|
809
|
+
thinking: { type: "disabled" },
|
|
810
|
+
system: SYSTEM,
|
|
811
|
+
messages: [{
|
|
812
|
+
role: "user",
|
|
813
|
+
content: buildPrompt(request)
|
|
814
|
+
}],
|
|
815
|
+
output_config: { format: batchFormat(request) }
|
|
816
|
+
})).content.find((block) => block.type === "text")?.text ?? "{}";
|
|
817
|
+
return JSON.parse(text);
|
|
818
|
+
};
|
|
641
819
|
}
|
|
642
820
|
function buildPrompt(request) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
` + JSON.stringify(request.messages, null, 2);
|
|
821
|
+
return `Translate each value from "${request.sourceLocale}" to "${request.targetLocale}". Return a JSON object with the same keys and translated values.\n\n` + JSON.stringify(request.messages, null, 2);
|
|
646
822
|
}
|
|
647
823
|
function batchFormat(request) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
async function loadSdk() {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
"[verbaly] the claude translate provider needs @anthropic-ai/sdk \u2014 install it as a dev dependency (e.g. `pnpm add -D @anthropic-ai/sdk` / `npm i -D @anthropic-ai/sdk`) and set ANTHROPIC_API_KEY",
|
|
667
|
-
{ cause: error }
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
throw error;
|
|
671
|
-
}
|
|
824
|
+
const keys = Object.keys(request.messages);
|
|
825
|
+
return {
|
|
826
|
+
type: "json_schema",
|
|
827
|
+
schema: {
|
|
828
|
+
type: "object",
|
|
829
|
+
properties: Object.fromEntries(keys.map((key) => [key, { type: "string" }])),
|
|
830
|
+
required: keys,
|
|
831
|
+
additionalProperties: false
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
async function loadSdk(load = () => import("@anthropic-ai/sdk")) {
|
|
836
|
+
try {
|
|
837
|
+
return (await load()).default;
|
|
838
|
+
} catch (error) {
|
|
839
|
+
if (isModuleNotFound(error, "@anthropic-ai/sdk")) throw new Error("[verbaly] the claude translate provider needs @anthropic-ai/sdk — install it as a dev dependency (e.g. `pnpm add -D @anthropic-ai/sdk` / `npm i -D @anthropic-ai/sdk`) and set ANTHROPIC_API_KEY", { cause: error });
|
|
840
|
+
throw error;
|
|
841
|
+
}
|
|
672
842
|
}
|
|
673
|
-
function
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
843
|
+
function isModuleNotFound(error, name) {
|
|
844
|
+
return error instanceof Error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes(name);
|
|
845
|
+
}
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/pseudo.ts
|
|
848
|
+
const PSEUDO_LOCALE = "en-XA";
|
|
849
|
+
const ACCENTS = {
|
|
850
|
+
a: "á",
|
|
851
|
+
b: "ƀ",
|
|
852
|
+
c: "ç",
|
|
853
|
+
d: "đ",
|
|
854
|
+
e: "é",
|
|
855
|
+
f: "ƒ",
|
|
856
|
+
g: "ğ",
|
|
857
|
+
h: "ĥ",
|
|
858
|
+
i: "í",
|
|
859
|
+
j: "ĵ",
|
|
860
|
+
k: "ķ",
|
|
861
|
+
l: "ĺ",
|
|
862
|
+
m: "ɱ",
|
|
863
|
+
n: "ñ",
|
|
864
|
+
o: "ó",
|
|
865
|
+
p: "þ",
|
|
866
|
+
q: "ǫ",
|
|
867
|
+
r: "ŕ",
|
|
868
|
+
s: "š",
|
|
869
|
+
t: "ţ",
|
|
870
|
+
u: "ú",
|
|
871
|
+
v: "ṽ",
|
|
872
|
+
w: "ŵ",
|
|
873
|
+
x: "ẋ",
|
|
874
|
+
y: "ý",
|
|
875
|
+
z: "ž",
|
|
876
|
+
A: "Á",
|
|
877
|
+
B: "Ɓ",
|
|
878
|
+
C: "Ç",
|
|
879
|
+
D: "Đ",
|
|
880
|
+
E: "É",
|
|
881
|
+
F: "Ƒ",
|
|
882
|
+
G: "Ğ",
|
|
883
|
+
H: "Ĥ",
|
|
884
|
+
I: "Í",
|
|
885
|
+
J: "Ĵ",
|
|
886
|
+
K: "Ķ",
|
|
887
|
+
L: "Ĺ",
|
|
888
|
+
M: "Ḿ",
|
|
889
|
+
N: "Ñ",
|
|
890
|
+
O: "Ó",
|
|
891
|
+
P: "Þ",
|
|
892
|
+
Q: "Ǫ",
|
|
893
|
+
R: "Ŕ",
|
|
894
|
+
S: "Š",
|
|
895
|
+
T: "Ţ",
|
|
896
|
+
U: "Ú",
|
|
897
|
+
V: "Ṽ",
|
|
898
|
+
W: "Ŵ",
|
|
899
|
+
X: "Ẍ",
|
|
900
|
+
Y: "Ý",
|
|
901
|
+
Z: "Ž"
|
|
732
902
|
};
|
|
733
|
-
|
|
903
|
+
const TAG_AT = /<\/?[a-zA-Z][\w-]*\/?>/y;
|
|
734
904
|
function pseudoLocalize(message) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const pad = "~".repeat(Math.ceil(letters / 3));
|
|
771
|
-
return `\u27E6${out}${pad ? " " + pad : ""}\u27E7`;
|
|
905
|
+
let out = "";
|
|
906
|
+
let letters = 0;
|
|
907
|
+
let i = 0;
|
|
908
|
+
while (i < message.length) {
|
|
909
|
+
const two = message.slice(i, i + 2);
|
|
910
|
+
if (two === "{{" || two === "}}" || two === "||" || two === "##") {
|
|
911
|
+
out += two;
|
|
912
|
+
i += 2;
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const ch = message[i];
|
|
916
|
+
if (ch === "{") {
|
|
917
|
+
const end = matchBrace(message, i);
|
|
918
|
+
out += message.slice(i, end);
|
|
919
|
+
i = end;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (ch === "<") {
|
|
923
|
+
TAG_AT.lastIndex = i;
|
|
924
|
+
const m = TAG_AT.exec(message);
|
|
925
|
+
if (m) {
|
|
926
|
+
out += m[0];
|
|
927
|
+
i += m[0].length;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const mapped = ACCENTS[ch];
|
|
932
|
+
if (mapped) {
|
|
933
|
+
out += mapped;
|
|
934
|
+
letters += 1;
|
|
935
|
+
} else out += ch;
|
|
936
|
+
i += 1;
|
|
937
|
+
}
|
|
938
|
+
const pad = "~".repeat(Math.ceil(letters / 3));
|
|
939
|
+
return `⟦${out}${pad ? " " + pad : ""}⟧`;
|
|
772
940
|
}
|
|
773
941
|
function matchBrace(message, start) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
942
|
+
let depth = 0;
|
|
943
|
+
for (let i = start; i < message.length; i++) {
|
|
944
|
+
const two = message.slice(i, i + 2);
|
|
945
|
+
if (two === "{{" || two === "}}") {
|
|
946
|
+
i += 1;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const ch = message[i];
|
|
950
|
+
if (ch === "{") depth += 1;
|
|
951
|
+
else if (ch === "}") {
|
|
952
|
+
depth -= 1;
|
|
953
|
+
if (depth === 0) return i + 1;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return message.length;
|
|
789
957
|
}
|
|
790
958
|
function pseudoCatalogs(cfg, catalogs, locale = PSEUDO_LOCALE) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
"br",
|
|
815
|
-
"col",
|
|
816
|
-
"embed",
|
|
817
|
-
"hr",
|
|
818
|
-
"img",
|
|
819
|
-
"input",
|
|
820
|
-
"link",
|
|
821
|
-
"meta",
|
|
822
|
-
"param",
|
|
823
|
-
"source",
|
|
824
|
-
"track",
|
|
825
|
-
"wbr"
|
|
959
|
+
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
960
|
+
const target = {};
|
|
961
|
+
for (const [key, msg] of Object.entries(source)) target[key] = msg ? pseudoLocalize(msg) : "";
|
|
962
|
+
catalogs[locale] = target;
|
|
963
|
+
return Object.keys(target).filter((key) => target[key]);
|
|
964
|
+
}
|
|
965
|
+
//#endregion
|
|
966
|
+
//#region src/render.ts
|
|
967
|
+
const VOID_TAGS = /* @__PURE__ */ new Set([
|
|
968
|
+
"area",
|
|
969
|
+
"base",
|
|
970
|
+
"br",
|
|
971
|
+
"col",
|
|
972
|
+
"embed",
|
|
973
|
+
"hr",
|
|
974
|
+
"img",
|
|
975
|
+
"input",
|
|
976
|
+
"link",
|
|
977
|
+
"meta",
|
|
978
|
+
"param",
|
|
979
|
+
"source",
|
|
980
|
+
"track",
|
|
981
|
+
"wbr"
|
|
826
982
|
]);
|
|
827
|
-
|
|
828
|
-
|
|
983
|
+
const START_TAG = /<([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|'[^']*'|[^"'>])*)>/g;
|
|
984
|
+
const ATTR = /([^\s=/"'<>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
829
985
|
function renderHtml(html, options) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
986
|
+
const attr = options.attribute ?? "data-verbaly";
|
|
987
|
+
const argsAttr = `${attr}-args`;
|
|
988
|
+
const attrsAttr = `${attr}-attr`;
|
|
989
|
+
const richAttr = `${attr}-rich`;
|
|
990
|
+
const linksAttr = `${attr}-links`;
|
|
991
|
+
const richTags = new Set(options.richTags ?? RICH_TAGS);
|
|
992
|
+
const globalLinks = options.richLinks;
|
|
993
|
+
const sourceLocale = options.sourceLocale ?? "en";
|
|
994
|
+
const messages = {};
|
|
995
|
+
for (const [locale, catalog] of Object.entries(options.catalogs)) {
|
|
996
|
+
const clean = {};
|
|
997
|
+
for (const [key, msg] of Object.entries(catalog)) if (msg) clean[key] = msg;
|
|
998
|
+
messages[locale] = clean;
|
|
999
|
+
}
|
|
1000
|
+
const v = createVerbaly({
|
|
1001
|
+
locale: options.locale,
|
|
1002
|
+
fallback: sourceLocale,
|
|
1003
|
+
messages
|
|
1004
|
+
});
|
|
1005
|
+
const t = v.t;
|
|
1006
|
+
const ms = new MagicString(html);
|
|
1007
|
+
const missing = /* @__PURE__ */ new Set();
|
|
1008
|
+
const skip = protectedRanges(html);
|
|
1009
|
+
const inSkip = (index) => skip.some(([from, to]) => index >= from && index < to);
|
|
1010
|
+
START_TAG.lastIndex = 0;
|
|
1011
|
+
let m;
|
|
1012
|
+
while ((m = START_TAG.exec(html)) !== null) {
|
|
1013
|
+
if (inSkip(m.index)) continue;
|
|
1014
|
+
const [full, rawName, attrChunk] = m;
|
|
1015
|
+
const tagName = rawName.toLowerCase();
|
|
1016
|
+
const openEnd = m.index + full.length;
|
|
1017
|
+
const chunkStart = m.index + 1 + rawName.length;
|
|
1018
|
+
if (tagName === "html" && options.setLang !== false) {
|
|
1019
|
+
setAttribute(ms, html, chunkStart, openEnd, attrChunk, "lang", options.locale);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const attrs = parseAttrs(attrChunk);
|
|
1023
|
+
const key = attrs.get(attr);
|
|
1024
|
+
const attrMapRaw = attrs.get(attrsAttr);
|
|
1025
|
+
if (key === void 0 && attrMapRaw === void 0) continue;
|
|
1026
|
+
const args = parseArgs(attrs.get(argsAttr));
|
|
1027
|
+
if (key) {
|
|
1028
|
+
if (!v.has(key)) missing.add(key);
|
|
1029
|
+
else if (!VOID_TAGS.has(tagName) && !attrChunk.trimEnd().endsWith("/")) {
|
|
1030
|
+
const close = findClose(html, tagName, openEnd, inSkip);
|
|
1031
|
+
if (close) {
|
|
1032
|
+
const text = t(key, args);
|
|
1033
|
+
const own = parseArgs(attrs.get(linksAttr));
|
|
1034
|
+
const links = own ? globalLinks ? {
|
|
1035
|
+
...globalLinks,
|
|
1036
|
+
...own
|
|
1037
|
+
} : own : globalLinks;
|
|
1038
|
+
const content = attrs.has(richAttr) ? richToHtml(parseTags(text), richTags, links) : escapeHtml(text);
|
|
1039
|
+
if (html.slice(openEnd, close.contentEnd) !== content) if (openEnd === close.contentEnd) ms.appendLeft(openEnd, content);
|
|
1040
|
+
else ms.overwrite(openEnd, close.contentEnd, content);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (attrMapRaw !== void 0) {
|
|
1045
|
+
const map = parseArgs(attrMapRaw);
|
|
1046
|
+
if (map) for (const [name, attrKey] of Object.entries(map)) {
|
|
1047
|
+
if (typeof attrKey !== "string" || name.toLowerCase().startsWith("on")) continue;
|
|
1048
|
+
if (!v.has(attrKey)) {
|
|
1049
|
+
missing.add(attrKey);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
setAttribute(ms, html, chunkStart, openEnd, attrChunk, name, t(attrKey, args));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
html: ms.toString(),
|
|
1058
|
+
missing: [...missing]
|
|
1059
|
+
};
|
|
899
1060
|
}
|
|
900
1061
|
async function renderSite(cfg, options = {}) {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1062
|
+
const site = join(cfg.root, options.site ?? "dist");
|
|
1063
|
+
const locales = options.locales ?? cfg.locales;
|
|
1064
|
+
const catalogs = loadCatalogs(cfg);
|
|
1065
|
+
const files = await glob("**/*.html", {
|
|
1066
|
+
cwd: site,
|
|
1067
|
+
absolute: true,
|
|
1068
|
+
ignore: locales.map((locale) => `${locale}/**`)
|
|
1069
|
+
});
|
|
1070
|
+
const missing = {};
|
|
1071
|
+
for (const file of files) {
|
|
1072
|
+
const html = readFileSync(file, "utf8");
|
|
1073
|
+
const rel = relative(site, file);
|
|
1074
|
+
for (const locale of locales) {
|
|
1075
|
+
const result = renderHtml(html, {
|
|
1076
|
+
locale,
|
|
1077
|
+
catalogs,
|
|
1078
|
+
sourceLocale: cfg.sourceLocale,
|
|
1079
|
+
attribute: options.attribute,
|
|
1080
|
+
richTags: options.richTags,
|
|
1081
|
+
richLinks: options.richLinks ?? cfg.render.links
|
|
1082
|
+
});
|
|
1083
|
+
for (const key of result.missing) {
|
|
1084
|
+
const list = missing[locale] ??= [];
|
|
1085
|
+
if (!list.includes(key)) list.push(key);
|
|
1086
|
+
}
|
|
1087
|
+
const out = locale === cfg.sourceLocale ? file : join(site, locale, rel);
|
|
1088
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
1089
|
+
writeFileSync(out, result.html);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
files: files.length,
|
|
1094
|
+
locales: [...locales],
|
|
1095
|
+
missing
|
|
1096
|
+
};
|
|
932
1097
|
}
|
|
933
1098
|
function parseAttrs(chunk) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}
|
|
940
|
-
return attrs;
|
|
1099
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
1100
|
+
ATTR.lastIndex = 0;
|
|
1101
|
+
let m;
|
|
1102
|
+
while ((m = ATTR.exec(chunk)) !== null) attrs.set(m[1].toLowerCase(), m[2] ?? m[3] ?? m[4] ?? "");
|
|
1103
|
+
return attrs;
|
|
941
1104
|
}
|
|
942
1105
|
function parseArgs(raw) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1106
|
+
if (!raw) return void 0;
|
|
1107
|
+
try {
|
|
1108
|
+
return JSON.parse(decodeEntities(raw));
|
|
1109
|
+
} catch {
|
|
1110
|
+
console.warn(`[verbaly] invalid args JSON: ${raw}`);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
950
1113
|
}
|
|
951
1114
|
function protectedRanges(html) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1115
|
+
const ranges = [];
|
|
1116
|
+
const re = /<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script\s*>|<style\b[\s\S]*?<\/style\s*>/gi;
|
|
1117
|
+
let m;
|
|
1118
|
+
while ((m = re.exec(html)) !== null) {
|
|
1119
|
+
const openEnd = m[0].startsWith("<!--") ? m.index : html.indexOf(">", m.index) + 1;
|
|
1120
|
+
ranges.push([openEnd, m.index + m[0].length]);
|
|
1121
|
+
}
|
|
1122
|
+
return ranges;
|
|
960
1123
|
}
|
|
961
1124
|
function findClose(html, tagName, from, inSkip) {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
} else if (!m[0].endsWith("/>")) {
|
|
975
|
-
depth += 1;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return null;
|
|
1125
|
+
const re = new RegExp(`<${tagName}(?=[\\s/>])(?:"[^"]*"|'[^']*'|[^"'>])*>|</${tagName}\\s*>`, "gi");
|
|
1126
|
+
re.lastIndex = from;
|
|
1127
|
+
let depth = 1;
|
|
1128
|
+
let m;
|
|
1129
|
+
while ((m = re.exec(html)) !== null) {
|
|
1130
|
+
if (inSkip(m.index)) continue;
|
|
1131
|
+
if (m[0][1] === "/") {
|
|
1132
|
+
depth -= 1;
|
|
1133
|
+
if (depth === 0) return { contentEnd: m.index };
|
|
1134
|
+
} else if (!m[0].endsWith("/>")) depth += 1;
|
|
1135
|
+
}
|
|
1136
|
+
return null;
|
|
979
1137
|
}
|
|
980
1138
|
function setAttribute(ms, html, chunkStart, openEnd, attrChunk, name, value) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
const selfClosing = html.slice(openEnd - 2, openEnd) === "/>";
|
|
994
|
-
const insertAt = openEnd - (selfClosing ? 2 : 1);
|
|
995
|
-
ms.appendLeft(insertAt, ` ${name}="${escaped}"`);
|
|
1139
|
+
const escaped = escapeAttr(value);
|
|
1140
|
+
const existing = new RegExp(`(\\s${name}\\s*=\\s*)("[^"]*"|'[^']*'|[^\\s>]+)`, "i").exec(attrChunk);
|
|
1141
|
+
if (existing) {
|
|
1142
|
+
const valueStart = chunkStart + existing.index + existing[1].length;
|
|
1143
|
+
const valueEnd = valueStart + existing[2].length;
|
|
1144
|
+
if (html.slice(valueStart, valueEnd) !== `"${escaped}"`) ms.overwrite(valueStart, valueEnd, `"${escaped}"`);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const insertAt = openEnd - (html.slice(openEnd - 2, openEnd) === "/>" ? 2 : 1);
|
|
1148
|
+
ms.appendLeft(insertAt, ` ${name}="${escaped}"`);
|
|
996
1149
|
}
|
|
997
1150
|
function richToHtml(nodes, allowed, links) {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
out += `<${node.name}>${richToHtml(node.children, allowed, links)}</${node.name}>`;
|
|
1012
|
-
} else {
|
|
1013
|
-
out += richToHtml(node.children, allowed, links);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
return out;
|
|
1151
|
+
let out = "";
|
|
1152
|
+
for (const node of nodes) if (typeof node === "string") out += escapeHtml(node);
|
|
1153
|
+
else if (links?.[node.name] !== void 0) {
|
|
1154
|
+
const link = links[node.name];
|
|
1155
|
+
const { href, target, rel } = typeof link === "string" ? { href: link } : link;
|
|
1156
|
+
const safe = safeHref(href);
|
|
1157
|
+
let attrs = safe !== void 0 ? ` href="${escapeAttr(safe)}"` : "";
|
|
1158
|
+
if (target) attrs += ` target="${escapeAttr(target)}"`;
|
|
1159
|
+
if (rel) attrs += ` rel="${escapeAttr(rel)}"`;
|
|
1160
|
+
out += `<a${attrs}>${richToHtml(node.children, allowed, links)}</a>`;
|
|
1161
|
+
} else if (allowed.has(node.name)) out += `<${node.name}>${richToHtml(node.children, allowed, links)}</${node.name}>`;
|
|
1162
|
+
else out += richToHtml(node.children, allowed, links);
|
|
1163
|
+
return out;
|
|
1017
1164
|
}
|
|
1018
1165
|
function escapeHtml(text) {
|
|
1019
|
-
|
|
1166
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1020
1167
|
}
|
|
1021
1168
|
function escapeAttr(text) {
|
|
1022
|
-
|
|
1169
|
+
return escapeHtml(text).replace(/"/g, """);
|
|
1023
1170
|
}
|
|
1024
1171
|
function decodeEntities(text) {
|
|
1025
|
-
|
|
1172
|
+
return text.replace(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
1026
1173
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
import MagicString2 from "magic-string";
|
|
1174
|
+
//#endregion
|
|
1175
|
+
//#region src/transform.ts
|
|
1030
1176
|
function transformCode(code, file, analysis) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
// src/translate.ts
|
|
1177
|
+
const { tagged } = analysis ?? analyze(code, file);
|
|
1178
|
+
if (tagged.length === 0) return null;
|
|
1179
|
+
const s = new MagicString(code);
|
|
1180
|
+
for (const msg of tagged) {
|
|
1181
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1182
|
+
const entries = msg.params.filter((p) => !seen.has(p.name) && seen.add(p.name));
|
|
1183
|
+
const pairs = entries.map((p) => `${JSON.stringify(p.name)}: ${code.slice(p.start, p.end)}`).join(", ");
|
|
1184
|
+
if (msg.jsx) {
|
|
1185
|
+
const values = entries.length ? ` values={{ ${pairs} }}` : "";
|
|
1186
|
+
const components = msg.jsx.components.length ? ` components={{ ${msg.jsx.components.map((c) => `${JSON.stringify(c.name)}: ${c.source}`).join(", ")} }}` : "";
|
|
1187
|
+
s.overwrite(msg.start, msg.end, `<${msg.jsx.name} id=${JSON.stringify(msg.key)}${values}${components} />`);
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
const tagSource = code.slice(msg.tagStart, msg.tagEnd);
|
|
1191
|
+
const args = entries.length ? `, { ${pairs} }` : "";
|
|
1192
|
+
s.overwrite(msg.start, msg.end, `${tagSource}(${JSON.stringify(msg.key)}${args})`);
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
code: s.toString(),
|
|
1196
|
+
map: s.generateMap({ hires: true })
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
//#endregion
|
|
1200
|
+
//#region src/translate.ts
|
|
1056
1201
|
async function translateCatalogs(cfg, catalogs, provider, options = {}) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1202
|
+
const batchSize = options.batchSize ?? 20;
|
|
1203
|
+
const targets = (options.locales ?? cfg.locales).filter((locale) => locale !== cfg.sourceLocale && cfg.locales.includes(locale));
|
|
1204
|
+
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
1205
|
+
const result = {
|
|
1206
|
+
translated: {},
|
|
1207
|
+
invalid: {},
|
|
1208
|
+
pending: {}
|
|
1209
|
+
};
|
|
1210
|
+
for (const locale of targets) {
|
|
1211
|
+
const catalog = catalogs[locale] ??= {};
|
|
1212
|
+
const missing = Object.keys(source).filter((key) => source[key] && !catalog[key]);
|
|
1213
|
+
if (missing.length === 0) continue;
|
|
1214
|
+
if (options.dryRun) {
|
|
1215
|
+
result.pending[locale] = missing;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
for (let i = 0; i < missing.length; i += batchSize) {
|
|
1219
|
+
const keys = missing.slice(i, i + batchSize);
|
|
1220
|
+
const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
|
|
1221
|
+
const out = await provider({
|
|
1222
|
+
sourceLocale: cfg.sourceLocale,
|
|
1223
|
+
targetLocale: locale,
|
|
1224
|
+
messages
|
|
1225
|
+
});
|
|
1226
|
+
for (const key of keys) {
|
|
1227
|
+
const text = out[key];
|
|
1228
|
+
if (typeof text === "string" && text.trim() && structureMatches(source[key], text)) {
|
|
1229
|
+
catalog[key] = text;
|
|
1230
|
+
(result.translated[locale] ??= []).push(key);
|
|
1231
|
+
} else (result.invalid[locale] ??= []).push(key);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return result;
|
|
1091
1236
|
}
|
|
1092
1237
|
function structureMatches(source, translated) {
|
|
1093
|
-
|
|
1238
|
+
return sameMembers(paramNames(source), paramNames(translated)) && sameMembers(tagTokens(source), tagTokens(translated));
|
|
1094
1239
|
}
|
|
1095
1240
|
function paramNames(message) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1241
|
+
try {
|
|
1242
|
+
return [...collectParams(message).keys()].sort();
|
|
1243
|
+
} catch {
|
|
1244
|
+
return ["\0invalid"];
|
|
1245
|
+
}
|
|
1101
1246
|
}
|
|
1102
|
-
|
|
1247
|
+
const TAG = /<(\/?)([a-zA-Z][\w-]*)(\/?)>/g;
|
|
1103
1248
|
function tagTokens(message) {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
return out.sort();
|
|
1249
|
+
const out = [];
|
|
1250
|
+
for (const match of message.matchAll(TAG)) out.push(`${match[1]}${match[2]}${match[3]}`);
|
|
1251
|
+
return out.sort();
|
|
1109
1252
|
}
|
|
1110
1253
|
function sameMembers(a, b) {
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
VIRTUAL_ID,
|
|
1117
|
-
analyze,
|
|
1118
|
-
catalogPath,
|
|
1119
|
-
check,
|
|
1120
|
-
claudeProvider,
|
|
1121
|
-
collectParams,
|
|
1122
|
-
extractProject,
|
|
1123
|
-
formatCheckResult,
|
|
1124
|
-
generateDts,
|
|
1125
|
-
generateLocaleModule,
|
|
1126
|
-
generateRuntimeModule,
|
|
1127
|
-
loadCatalogs,
|
|
1128
|
-
loadConfig,
|
|
1129
|
-
loadConfigFile,
|
|
1130
|
-
pruneCatalogs,
|
|
1131
|
-
pseudoCatalogs,
|
|
1132
|
-
pseudoLocalize,
|
|
1133
|
-
readCatalog,
|
|
1134
|
-
renderHtml,
|
|
1135
|
-
renderParamType,
|
|
1136
|
-
renderSite,
|
|
1137
|
-
resolveConfig,
|
|
1138
|
-
serializeCatalog,
|
|
1139
|
-
stableKey,
|
|
1140
|
-
structureMatches,
|
|
1141
|
-
syncCatalogs,
|
|
1142
|
-
transformCode,
|
|
1143
|
-
translateCatalogs,
|
|
1144
|
-
writeCatalog,
|
|
1145
|
-
writeDts
|
|
1146
|
-
};
|
|
1254
|
+
return a.length === b.length && a.every((value, i) => value === b[i]);
|
|
1255
|
+
}
|
|
1256
|
+
//#endregion
|
|
1257
|
+
export { MessageRegistry, PSEUDO_LOCALE, VIRTUAL_ID, analyze, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, extractProject, findConfigFile, formatCheckResult, generateDts, generateLocaleModule, generateRuntimeModule, init, loadCatalogs, loadConfig, loadConfigFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, serializeCatalog, stableKey, structureMatches, syncCatalogs, transformCode, translateCatalogs, writeCatalog, writeDts };
|
|
1258
|
+
|
|
1147
1259
|
//# sourceMappingURL=index.js.map
|