@verbaly/compiler 0.19.0 → 0.21.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 +1 -1
- package/dist/{claude-mpjdqqnY.js → claude-C-ETlqsW.js} +2 -2
- package/dist/{claude-mpjdqqnY.js.map → claude-C-ETlqsW.js.map} +1 -1
- package/dist/cli.js +224 -118
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +28 -12
- package/dist/index.js +234 -115
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
|
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { RICH_TAGS, createVerbaly, parse as parse$1, parseTags, safeHref } from "verbaly";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { glob } from "tinyglobby";
|
|
8
7
|
import MagicString from "magic-string";
|
|
8
|
+
import { glob } from "tinyglobby";
|
|
9
9
|
//#region src/key.ts
|
|
10
10
|
function stableKey(message) {
|
|
11
11
|
return createHash("sha256").update(message).digest("base64url").slice(0, 8);
|
|
@@ -19,7 +19,9 @@ const SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
19
19
|
"innerComments",
|
|
20
20
|
"extra"
|
|
21
21
|
]);
|
|
22
|
-
|
|
22
|
+
const DEFAULT_T_NAMES = ["t"];
|
|
23
|
+
function analyze(code, file, options = {}) {
|
|
24
|
+
const names = new Set(options.tNames ?? DEFAULT_T_NAMES);
|
|
23
25
|
const ast = parse(code, {
|
|
24
26
|
sourceType: "module",
|
|
25
27
|
errorRecovery: true,
|
|
@@ -30,10 +32,10 @@ function analyze(code, file) {
|
|
|
30
32
|
walk(ast.program, (node) => {
|
|
31
33
|
if (node.type === "TaggedTemplateExpression") {
|
|
32
34
|
const tag = node.tag;
|
|
33
|
-
const explicit = explicitId(tag);
|
|
34
|
-
if (!explicit && !isTReference(tag)) return;
|
|
35
|
+
const explicit = explicitId(tag, names);
|
|
36
|
+
if (!explicit && !isTReference(tag, names)) return;
|
|
35
37
|
const quasi = node.quasi;
|
|
36
|
-
const message = buildMessage(code, quasi);
|
|
38
|
+
const message = buildMessage(code, quasi, names);
|
|
37
39
|
if (!message) return;
|
|
38
40
|
tagged.push({
|
|
39
41
|
key: explicit ? explicit.key : stableKey(message.text),
|
|
@@ -47,47 +49,48 @@ function analyze(code, file) {
|
|
|
47
49
|
});
|
|
48
50
|
} else if (node.type === "CallExpression") {
|
|
49
51
|
const callee = node.callee;
|
|
50
|
-
if (!isTReference(callee)) return;
|
|
52
|
+
if (!isTReference(callee, names)) return;
|
|
51
53
|
const first = node.arguments[0];
|
|
52
54
|
if (first?.type === "StringLiteral") usedKeys.push({
|
|
53
55
|
key: first.value,
|
|
54
56
|
file
|
|
55
57
|
});
|
|
56
|
-
} else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys);
|
|
58
|
+
} else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys, names);
|
|
57
59
|
});
|
|
58
60
|
return {
|
|
59
61
|
tagged,
|
|
60
62
|
usedKeys
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
|
-
function handleTrans(code, node, file, tagged, usedKeys) {
|
|
65
|
+
function handleTrans(code, node, file, tagged, usedKeys, names) {
|
|
64
66
|
const opening = node.openingElement;
|
|
65
67
|
const nameNode = opening.name;
|
|
66
68
|
if (nameNode.type !== "JSXIdentifier" || nameNode.name !== "Trans") return;
|
|
67
69
|
const attrs = opening.attributes;
|
|
68
70
|
const idAttr = attrs.find((a) => a.type === "JSXAttribute" && a.name.name === "id");
|
|
69
71
|
const children = node.children;
|
|
72
|
+
const push = (key, built) => tagged.push({
|
|
73
|
+
key,
|
|
74
|
+
message: built.text,
|
|
75
|
+
params: built.params,
|
|
76
|
+
start: node.start,
|
|
77
|
+
end: node.end,
|
|
78
|
+
tagStart: nameNode.start,
|
|
79
|
+
tagEnd: nameNode.end,
|
|
80
|
+
file,
|
|
81
|
+
jsx: {
|
|
82
|
+
name: nameNode.name,
|
|
83
|
+
components: built.components
|
|
84
|
+
}
|
|
85
|
+
});
|
|
70
86
|
if (idAttr) {
|
|
71
87
|
const value = idAttr.value;
|
|
72
88
|
if (value?.type !== "StringLiteral") return;
|
|
73
89
|
const id = value.value;
|
|
74
90
|
if (attrs.length === 1 && children?.length) {
|
|
75
|
-
const built = buildTransMessage(code, children);
|
|
91
|
+
const built = buildTransMessage(code, children, names);
|
|
76
92
|
if (built?.text.trim()) {
|
|
77
|
-
|
|
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
|
-
});
|
|
93
|
+
push(id, built);
|
|
91
94
|
return;
|
|
92
95
|
}
|
|
93
96
|
}
|
|
@@ -99,31 +102,18 @@ function handleTrans(code, node, file, tagged, usedKeys) {
|
|
|
99
102
|
}
|
|
100
103
|
if (attrs.length > 0) return;
|
|
101
104
|
if (!children?.length) return;
|
|
102
|
-
const built = buildTransMessage(code, children);
|
|
105
|
+
const built = buildTransMessage(code, children, names);
|
|
103
106
|
if (!built || !built.text.trim()) return;
|
|
104
|
-
|
|
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
|
-
});
|
|
107
|
+
push(stableKey(built.text), built);
|
|
118
108
|
}
|
|
119
|
-
function explicitId(tag) {
|
|
109
|
+
function explicitId(tag, names) {
|
|
120
110
|
if (tag.type !== "CallExpression") return void 0;
|
|
121
111
|
const callee = tag.callee;
|
|
122
112
|
if (callee.type !== "MemberExpression" || callee.computed) return void 0;
|
|
123
113
|
const prop = callee.property;
|
|
124
114
|
if (prop.type !== "Identifier" || prop.name !== "id") return void 0;
|
|
125
115
|
const obj = callee.object;
|
|
126
|
-
if (!isTReference(obj)) return void 0;
|
|
116
|
+
if (!isTReference(obj, names)) return void 0;
|
|
127
117
|
const args = tag.arguments;
|
|
128
118
|
const first = args[0];
|
|
129
119
|
if (args.length !== 1 || first?.type !== "StringLiteral") return void 0;
|
|
@@ -133,7 +123,7 @@ function explicitId(tag) {
|
|
|
133
123
|
refEnd: obj.end
|
|
134
124
|
};
|
|
135
125
|
}
|
|
136
|
-
function buildTransMessage(code, children) {
|
|
126
|
+
function buildTransMessage(code, children, names) {
|
|
137
127
|
const params = [];
|
|
138
128
|
const components = [];
|
|
139
129
|
const takenParams = /* @__PURE__ */ new Map();
|
|
@@ -148,7 +138,7 @@ function buildTransMessage(code, children) {
|
|
|
148
138
|
text += escapeText(expr.value);
|
|
149
139
|
continue;
|
|
150
140
|
}
|
|
151
|
-
if (containsTaggedT(expr)) return void 0;
|
|
141
|
+
if (containsTaggedT(expr, names)) return void 0;
|
|
152
142
|
const source = code.slice(expr.start, expr.end);
|
|
153
143
|
const name = uniqueName(deriveName(expr, params.length), source, takenParams);
|
|
154
144
|
params.push({
|
|
@@ -201,24 +191,24 @@ function selfClosedSource(code, opening) {
|
|
|
201
191
|
const src = code.slice(opening.start, opening.end);
|
|
202
192
|
return src.endsWith("/>") ? src : `${src.slice(0, -1).trimEnd()} />`;
|
|
203
193
|
}
|
|
204
|
-
function containsTaggedT(node) {
|
|
194
|
+
function containsTaggedT(node, names) {
|
|
205
195
|
let found = false;
|
|
206
196
|
walk(node, (n) => {
|
|
207
197
|
if (n.type !== "TaggedTemplateExpression") return;
|
|
208
198
|
const tag = n.tag;
|
|
209
|
-
if (isTReference(tag) || explicitId(tag)) found = true;
|
|
199
|
+
if (isTReference(tag, names) || explicitId(tag, names)) found = true;
|
|
210
200
|
});
|
|
211
201
|
return found;
|
|
212
202
|
}
|
|
213
|
-
function isTReference(node) {
|
|
214
|
-
if (node.type === "Identifier") return node.name
|
|
203
|
+
function isTReference(node, names) {
|
|
204
|
+
if (node.type === "Identifier") return names.has(node.name);
|
|
215
205
|
if (node.type === "MemberExpression" && !node.computed) {
|
|
216
206
|
const prop = node.property;
|
|
217
|
-
return prop.type === "Identifier" && prop.name
|
|
207
|
+
return prop.type === "Identifier" && names.has(prop.name);
|
|
218
208
|
}
|
|
219
209
|
return false;
|
|
220
210
|
}
|
|
221
|
-
function buildMessage(code, quasi) {
|
|
211
|
+
function buildMessage(code, quasi, names) {
|
|
222
212
|
const quasis = quasi.quasis;
|
|
223
213
|
const expressions = quasi.expressions;
|
|
224
214
|
const params = [];
|
|
@@ -227,7 +217,7 @@ function buildMessage(code, quasi) {
|
|
|
227
217
|
for (let i = 0; i < expressions.length; i++) {
|
|
228
218
|
const expr = expressions[i];
|
|
229
219
|
if (!expr) return void 0;
|
|
230
|
-
if (containsTaggedT(expr)) return void 0;
|
|
220
|
+
if (containsTaggedT(expr, names)) return void 0;
|
|
231
221
|
const source = code.slice(expr.start, expr.end);
|
|
232
222
|
const name = uniqueName(deriveName(expr, i), source, taken);
|
|
233
223
|
params.push({
|
|
@@ -270,8 +260,9 @@ function uniqueName(base, source, taken) {
|
|
|
270
260
|
}
|
|
271
261
|
function walk(node, visit) {
|
|
272
262
|
visit(node);
|
|
273
|
-
for (const
|
|
263
|
+
for (const key in node) {
|
|
274
264
|
if (SKIP_KEYS.has(key)) continue;
|
|
265
|
+
const value = node[key];
|
|
275
266
|
if (Array.isArray(value)) {
|
|
276
267
|
for (const item of value) if (isNode(item)) walk(item, visit);
|
|
277
268
|
} else if (isNode(value)) walk(value, visit);
|
|
@@ -281,6 +272,111 @@ function isNode(value) {
|
|
|
281
272
|
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
282
273
|
}
|
|
283
274
|
//#endregion
|
|
275
|
+
//#region src/sfc.ts
|
|
276
|
+
const SFC_FILE_RE = /\.(?:svelte|vue)$/;
|
|
277
|
+
function analyzeFile(code, file) {
|
|
278
|
+
return SFC_FILE_RE.test(file) ? analyzeSfc(code, file) : analyze(code, file);
|
|
279
|
+
}
|
|
280
|
+
const SCRIPT_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi;
|
|
281
|
+
const STYLE_RE = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
|
|
282
|
+
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
283
|
+
const CANDIDATE_SVELTE = /(?<![\w$])\$?t(?=[`(]|\.id\()/g;
|
|
284
|
+
const CANDIDATE_VUE = /(?<![\w$])t(?=[`(]|\.id\()/g;
|
|
285
|
+
function analyzeSfc(code, file) {
|
|
286
|
+
const svelte = file.endsWith(".svelte");
|
|
287
|
+
const options = svelte ? { tNames: ["t", "$t"] } : void 0;
|
|
288
|
+
const tagged = [];
|
|
289
|
+
const usedKeys = [];
|
|
290
|
+
for (const match of code.matchAll(SCRIPT_RE)) {
|
|
291
|
+
const content = match[2];
|
|
292
|
+
if (!content) continue;
|
|
293
|
+
merge(analyzeSegment(content, file, options), match.index + match[1].length, false);
|
|
294
|
+
}
|
|
295
|
+
const markup = blank(blank(blank(code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
|
|
296
|
+
const candidates = svelte ? CANDIDATE_SVELTE : CANDIDATE_VUE;
|
|
297
|
+
let consumedTo = 0;
|
|
298
|
+
for (const match of markup.matchAll(candidates)) {
|
|
299
|
+
const start = match.index;
|
|
300
|
+
if (start < consumedTo) continue;
|
|
301
|
+
const end = expressionExtent(markup, start);
|
|
302
|
+
if (end === void 0) continue;
|
|
303
|
+
merge(analyzeSegment(code.slice(start, end), file, options), start, true);
|
|
304
|
+
consumedTo = end;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
tagged,
|
|
308
|
+
usedKeys
|
|
309
|
+
};
|
|
310
|
+
function merge(analysis, offset, markupSegment) {
|
|
311
|
+
if (!analysis) return;
|
|
312
|
+
for (const msg of analysis.tagged) tagged.push({
|
|
313
|
+
...msg,
|
|
314
|
+
start: msg.start + offset,
|
|
315
|
+
end: msg.end + offset,
|
|
316
|
+
tagStart: msg.tagStart + offset,
|
|
317
|
+
tagEnd: msg.tagEnd + offset,
|
|
318
|
+
params: msg.params.map((p) => ({
|
|
319
|
+
...p,
|
|
320
|
+
start: p.start + offset,
|
|
321
|
+
end: p.end + offset
|
|
322
|
+
})),
|
|
323
|
+
...markupSegment && { singleQuote: true }
|
|
324
|
+
});
|
|
325
|
+
usedKeys.push(...analysis.usedKeys);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function analyzeSegment(code, file, options) {
|
|
329
|
+
try {
|
|
330
|
+
return analyze(code, file, options);
|
|
331
|
+
} catch {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function blank(source, re) {
|
|
336
|
+
return source.replace(re, (m) => " ".repeat(m.length));
|
|
337
|
+
}
|
|
338
|
+
function expressionExtent(code, start) {
|
|
339
|
+
let i = start;
|
|
340
|
+
if (code[i] === "$") i += 1;
|
|
341
|
+
i += 1;
|
|
342
|
+
if (code.startsWith(".id(", i)) {
|
|
343
|
+
const close = balancedEnd(code, i + 3);
|
|
344
|
+
if (close === void 0 || code[close] !== "`") return void 0;
|
|
345
|
+
return balancedEnd(code, close);
|
|
346
|
+
}
|
|
347
|
+
if (code[i] === "`" || code[i] === "(") return balancedEnd(code, i);
|
|
348
|
+
}
|
|
349
|
+
function balancedEnd(code, open) {
|
|
350
|
+
const stack = [];
|
|
351
|
+
let i = open;
|
|
352
|
+
while (i < code.length) {
|
|
353
|
+
const ch = code[i];
|
|
354
|
+
if (stack[stack.length - 1] === "`") {
|
|
355
|
+
if (ch === "\\") i += 1;
|
|
356
|
+
else if (ch === "`") stack.pop();
|
|
357
|
+
else if (ch === "$" && code[i + 1] === "{") {
|
|
358
|
+
stack.push("{");
|
|
359
|
+
i += 1;
|
|
360
|
+
}
|
|
361
|
+
} else if (ch === "'" || ch === "\"") i = stringEnd(code, i);
|
|
362
|
+
else if (ch === "`" || ch === "(" || ch === "{") stack.push(ch);
|
|
363
|
+
else if (ch === ")" || ch === "}") {
|
|
364
|
+
if (stack[stack.length - 1] !== (ch === ")" ? "(" : "{")) return void 0;
|
|
365
|
+
stack.pop();
|
|
366
|
+
}
|
|
367
|
+
i += 1;
|
|
368
|
+
if (stack.length === 0) return i;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function stringEnd(code, start) {
|
|
372
|
+
const quote = code[start];
|
|
373
|
+
let i = start + 1;
|
|
374
|
+
while (i < code.length) if (code[i] === "\\") i += 2;
|
|
375
|
+
else if (code[i] === quote) return i;
|
|
376
|
+
else i += 1;
|
|
377
|
+
return code.length;
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
284
380
|
//#region src/catalog.ts
|
|
285
381
|
function catalogPath(cfg, locale) {
|
|
286
382
|
return join(cfg.dir, `${locale}.json`);
|
|
@@ -347,13 +443,13 @@ function formatCheckResult(result) {
|
|
|
347
443
|
if (result.missing.length > 0) {
|
|
348
444
|
lines.push("missing translations:");
|
|
349
445
|
for (const entry of result.missing) {
|
|
350
|
-
const hint = entry.source ?
|
|
446
|
+
const hint = entry.source ? `: "${truncate(entry.source, 40)}"` : "";
|
|
351
447
|
lines.push(` [${entry.locale}] ${entry.key}${hint}`);
|
|
352
448
|
}
|
|
353
449
|
}
|
|
354
450
|
if (result.unknown.length > 0) {
|
|
355
451
|
lines.push("unknown keys (not in any catalog):");
|
|
356
|
-
for (const entry of result.unknown) lines.push(` ${entry.key}
|
|
452
|
+
for (const entry of result.unknown) lines.push(` ${entry.key} (used in ${entry.files.join(", ")})`);
|
|
357
453
|
}
|
|
358
454
|
return lines.join("\n");
|
|
359
455
|
}
|
|
@@ -400,11 +496,15 @@ function visit(nodes, out) {
|
|
|
400
496
|
}
|
|
401
497
|
function renderParamType(types) {
|
|
402
498
|
if (types.has("unknown") || types.size === 0) return "unknown";
|
|
403
|
-
const
|
|
404
|
-
if (types.has("number"))
|
|
405
|
-
if (types.has("string"))
|
|
406
|
-
if (types.has("date"))
|
|
407
|
-
|
|
499
|
+
const members = /* @__PURE__ */ new Set();
|
|
500
|
+
if (types.has("number")) members.add("number");
|
|
501
|
+
if (types.has("string")) members.add("string");
|
|
502
|
+
if (types.has("date")) for (const m of [
|
|
503
|
+
"Date",
|
|
504
|
+
"number",
|
|
505
|
+
"string"
|
|
506
|
+
]) members.add(m);
|
|
507
|
+
return [...members].join(" | ");
|
|
408
508
|
}
|
|
409
509
|
//#endregion
|
|
410
510
|
//#region src/codegen.ts
|
|
@@ -424,14 +524,14 @@ const localeLoaders = {
|
|
|
424
524
|
${loaders}
|
|
425
525
|
};
|
|
426
526
|
|
|
427
|
-
// raw catalog access
|
|
527
|
+
// raw catalog access: SSR integrations serialize it across the client boundary
|
|
428
528
|
export async function loadMessages(locale) {
|
|
429
529
|
if (locale === ${src}) return source;
|
|
430
530
|
const loader = localeLoaders[locale];
|
|
431
531
|
return loader ? (await loader()).default : {};
|
|
432
532
|
}
|
|
433
533
|
|
|
434
|
-
// per-request/per-instance factory (SSR)
|
|
534
|
+
// per-request/per-instance factory (SSR): the singleton below is browser/SPA-only
|
|
435
535
|
export function createInstance(options) {
|
|
436
536
|
return createVerbaly({
|
|
437
537
|
locale: ${src},
|
|
@@ -471,9 +571,9 @@ ${options.extraExports ?? ""}`;
|
|
|
471
571
|
function generateLocaleModule(catalog) {
|
|
472
572
|
return `export default ${JSON.stringify(catalog)};\n`;
|
|
473
573
|
}
|
|
474
|
-
function generateDts(
|
|
574
|
+
function generateDts(catalog) {
|
|
475
575
|
const lines = [];
|
|
476
|
-
for (const [key, message] of
|
|
576
|
+
for (const [key, message] of Object.entries(catalog).sort(([a], [b]) => a.localeCompare(b))) {
|
|
477
577
|
const params = collectParams(message);
|
|
478
578
|
if (params.size === 0) lines.push(` ${JSON.stringify(key)}: never;`);
|
|
479
579
|
else {
|
|
@@ -481,7 +581,7 @@ function generateDts(entries) {
|
|
|
481
581
|
lines.push(` ${JSON.stringify(key)}: { ${fields} };`);
|
|
482
582
|
}
|
|
483
583
|
}
|
|
484
|
-
return `// generated by verbaly
|
|
584
|
+
return `// generated by verbaly: do not edit
|
|
485
585
|
declare module 'virtual:verbaly' {
|
|
486
586
|
export interface VerbalyMessages {
|
|
487
587
|
${lines.join("\n")}
|
|
@@ -522,8 +622,13 @@ declare module 'virtual:verbaly/locale/*' {
|
|
|
522
622
|
}
|
|
523
623
|
`;
|
|
524
624
|
}
|
|
525
|
-
function writeDts(cfg,
|
|
526
|
-
|
|
625
|
+
function writeDts(cfg, catalog) {
|
|
626
|
+
const file = join(cfg.root, "verbaly.d.ts");
|
|
627
|
+
const content = generateDts(catalog);
|
|
628
|
+
try {
|
|
629
|
+
if (readFileSync(file, "utf8") === content) return;
|
|
630
|
+
} catch {}
|
|
631
|
+
writeFileSync(file, content);
|
|
527
632
|
}
|
|
528
633
|
//#endregion
|
|
529
634
|
//#region src/pseudo.ts
|
|
@@ -659,7 +764,7 @@ function resolveConfig(config = {}) {
|
|
|
659
764
|
dir,
|
|
660
765
|
sourceLocale,
|
|
661
766
|
locales: [...locales],
|
|
662
|
-
include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
|
|
767
|
+
include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue}"],
|
|
663
768
|
exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
|
|
664
769
|
translate: config.translate ?? {},
|
|
665
770
|
render: config.render ?? {}
|
|
@@ -694,7 +799,7 @@ async function loadTsConfig(path) {
|
|
|
694
799
|
const { mod } = await bundleRequire({ filepath: path });
|
|
695
800
|
return mod.default ?? {};
|
|
696
801
|
} catch (error) {
|
|
697
|
-
if (isModuleNotFound(error, "esbuild")) throw new Error(`[verbaly] ${path} needs esbuild
|
|
802
|
+
if (isModuleNotFound(error, "esbuild")) throw new Error(`[verbaly] ${path} needs esbuild: install it with pnpm add -D esbuild`, { cause: error });
|
|
698
803
|
throw error;
|
|
699
804
|
}
|
|
700
805
|
}
|
|
@@ -715,10 +820,39 @@ function definedOnly(config) {
|
|
|
715
820
|
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
|
|
716
821
|
}
|
|
717
822
|
//#endregion
|
|
823
|
+
//#region src/transform.ts
|
|
824
|
+
function quote(text, single) {
|
|
825
|
+
if (!single) return JSON.stringify(text);
|
|
826
|
+
return `'${text.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
827
|
+
}
|
|
828
|
+
function transformCode(code, file, analysis) {
|
|
829
|
+
const { tagged } = analysis ?? analyzeFile(code, file);
|
|
830
|
+
if (tagged.length === 0) return null;
|
|
831
|
+
const s = new MagicString(code);
|
|
832
|
+
for (const msg of tagged) {
|
|
833
|
+
const seen = /* @__PURE__ */ new Set();
|
|
834
|
+
const entries = msg.params.filter((p) => !seen.has(p.name) && seen.add(p.name));
|
|
835
|
+
const pairs = entries.map((p) => `${quote(p.name, msg.singleQuote)}: ${code.slice(p.start, p.end)}`).join(", ");
|
|
836
|
+
if (msg.jsx) {
|
|
837
|
+
const values = entries.length ? ` values={{ ${pairs} }}` : "";
|
|
838
|
+
const components = msg.jsx.components.length ? ` components={{ ${msg.jsx.components.map((c) => `${JSON.stringify(c.name)}: ${c.source}`).join(", ")} }}` : "";
|
|
839
|
+
s.overwrite(msg.start, msg.end, `<${msg.jsx.name} id=${JSON.stringify(msg.key)}${values}${components} />`);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const tagSource = code.slice(msg.tagStart, msg.tagEnd);
|
|
843
|
+
const args = entries.length ? `, { ${pairs} }` : "";
|
|
844
|
+
s.overwrite(msg.start, msg.end, `${tagSource}(${quote(msg.key, msg.singleQuote)}${args})`);
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
code: s.toString(),
|
|
848
|
+
map: s.generateMap({ hires: true })
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
//#endregion
|
|
718
852
|
//#region src/plugin.ts
|
|
719
853
|
const RESOLVED_VIRTUAL_ID = "\0virtual:verbaly";
|
|
720
854
|
const LOCALE_MODULE_PREFIX = `${RESOLVED_VIRTUAL_ID}/locale/`;
|
|
721
|
-
const SOURCE_FILE_RE = /\.[cm]?[jt]sx
|
|
855
|
+
const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|svelte|vue)$/;
|
|
722
856
|
function resolveVirtualId(id) {
|
|
723
857
|
if (id === "virtual:verbaly" || id.startsWith(`virtual:verbaly/`)) return "\0" + id;
|
|
724
858
|
}
|
|
@@ -729,7 +863,16 @@ function loadVirtualModule(id, cfg, catalogs) {
|
|
|
729
863
|
function isTransformTarget(id) {
|
|
730
864
|
return SOURCE_FILE_RE.test(id) && !id.includes("node_modules") && !id.startsWith("\0");
|
|
731
865
|
}
|
|
732
|
-
function
|
|
866
|
+
function transformSource(code, id, registry) {
|
|
867
|
+
const analysis = analyzeFile(code, id);
|
|
868
|
+
registry.update(id, analysis);
|
|
869
|
+
return {
|
|
870
|
+
analysis,
|
|
871
|
+
result: transformCode(code, id, analysis) ?? null
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function runBuildGate(cfg, registry, failOnMissing) {
|
|
875
|
+
if (failOnMissing === false) return;
|
|
733
876
|
const result = check(cfg, loadCatalogs(cfg), registry);
|
|
734
877
|
if (!result.ok) throw new Error(`[verbaly] build blocked\n${formatCheckResult(result)}\nRun \`npx verbaly extract\` and fill the missing translations.`);
|
|
735
878
|
}
|
|
@@ -748,7 +891,7 @@ var MessageRegistry = class {
|
|
|
748
891
|
for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
|
|
749
892
|
const existing = out.get(msg.key);
|
|
750
893
|
if (existing) {
|
|
751
|
-
if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)}
|
|
894
|
+
if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)}, second dropped.`);
|
|
752
895
|
continue;
|
|
753
896
|
}
|
|
754
897
|
out.set(msg.key, msg);
|
|
@@ -774,7 +917,7 @@ async function extractProject(cfg) {
|
|
|
774
917
|
absolute: true
|
|
775
918
|
});
|
|
776
919
|
const registry = new MessageRegistry();
|
|
777
|
-
for (const file of files) registry.update(file,
|
|
920
|
+
for (const file of files) registry.update(file, analyzeFile(readFileSync(file, "utf8"), file));
|
|
778
921
|
return registry;
|
|
779
922
|
}
|
|
780
923
|
function syncCatalogs(cfg, catalogs, registry) {
|
|
@@ -880,27 +1023,27 @@ function init(options = {}) {
|
|
|
880
1023
|
const PREVIEW = 5;
|
|
881
1024
|
async function doctor(cfg) {
|
|
882
1025
|
const entries = [];
|
|
883
|
-
const ok = (
|
|
1026
|
+
const ok = (name, message) => entries.push({
|
|
884
1027
|
level: "ok",
|
|
885
|
-
check,
|
|
1028
|
+
check: name,
|
|
886
1029
|
message
|
|
887
1030
|
});
|
|
888
|
-
const warn = (
|
|
1031
|
+
const warn = (name, message, fix) => entries.push({
|
|
889
1032
|
level: "warn",
|
|
890
|
-
check,
|
|
1033
|
+
check: name,
|
|
891
1034
|
message,
|
|
892
1035
|
fix
|
|
893
1036
|
});
|
|
894
|
-
const error = (
|
|
1037
|
+
const error = (name, message, fix) => entries.push({
|
|
895
1038
|
level: "error",
|
|
896
|
-
check,
|
|
1039
|
+
check: name,
|
|
897
1040
|
message,
|
|
898
1041
|
fix
|
|
899
1042
|
});
|
|
900
1043
|
const rel = (path) => relative(cfg.root, path).replaceAll("\\", "/");
|
|
901
1044
|
const configFile = findConfigFile(cfg.root);
|
|
902
1045
|
if (configFile) ok("config", `${configFile} found`);
|
|
903
|
-
else warn("config", "no config file
|
|
1046
|
+
else warn("config", "no config file, running on defaults", "run `npx verbaly init`");
|
|
904
1047
|
const catalogs = {};
|
|
905
1048
|
let catalogsHealthy = true;
|
|
906
1049
|
if (!existsSync(cfg.dir)) {
|
|
@@ -933,14 +1076,14 @@ async function doctor(cfg) {
|
|
|
933
1076
|
const deps = readDeps(cfg.root);
|
|
934
1077
|
const bundler = detectBundler(cfg.root);
|
|
935
1078
|
const wired = deps["@verbaly/vite"] || deps["@verbaly/unplugin"];
|
|
936
|
-
if (!bundler) ok("plugin", "no bundler detected
|
|
1079
|
+
if (!bundler) ok("plugin", "no bundler detected, the CLI flow (extract/check) applies");
|
|
937
1080
|
else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
|
|
938
1081
|
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");
|
|
939
1082
|
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`);
|
|
940
1083
|
if (source) {
|
|
941
1084
|
const dtsPath = join(cfg.root, "verbaly.d.ts");
|
|
942
1085
|
if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
|
|
943
|
-
else if (readFileSync(dtsPath, "utf8") !== generateDts(
|
|
1086
|
+
else if (readFileSync(dtsPath, "utf8") !== generateDts(source)) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
|
|
944
1087
|
else ok("types", "verbaly.d.ts is up to date");
|
|
945
1088
|
}
|
|
946
1089
|
const registry = await extractProject(cfg);
|
|
@@ -953,10 +1096,10 @@ async function doctor(cfg) {
|
|
|
953
1096
|
}
|
|
954
1097
|
if (catalogsHealthy) {
|
|
955
1098
|
const result = check(cfg, catalogs, registry);
|
|
956
|
-
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
|
|
1099
|
+
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)");
|
|
957
1100
|
if (result.missing.length > 0) {
|
|
958
1101
|
const locales = [...new Set(result.missing.map((m) => m.locale))];
|
|
959
|
-
warn("translations", `${result.missing.length} missing ${result.missing.length === 1 ? "translation" : "translations"} (${locales.join(", ")})`, "run `npx verbaly translate` or fill the catalogs
|
|
1102
|
+
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)");
|
|
960
1103
|
}
|
|
961
1104
|
if (result.ok) ok("translations", "all translations complete");
|
|
962
1105
|
}
|
|
@@ -1048,19 +1191,20 @@ function exportCatalogs(cfg, catalogs, options = {}) {
|
|
|
1048
1191
|
mkdirSync(dir, { recursive: true });
|
|
1049
1192
|
for (const locale of targets) {
|
|
1050
1193
|
const catalog = catalogs[locale] ?? {};
|
|
1051
|
-
|
|
1194
|
+
const all = Object.keys(source).filter((key) => source[key]).sort().map((key) => ({
|
|
1052
1195
|
key,
|
|
1053
1196
|
source: source[key],
|
|
1054
1197
|
target: catalog[key] ?? ""
|
|
1055
1198
|
}));
|
|
1056
|
-
|
|
1199
|
+
const untranslated = all.filter((entry) => !entry.target);
|
|
1200
|
+
const entries = options.missing ? untranslated : all;
|
|
1057
1201
|
const path = join(dir, `${locale}.${format === "csv" ? "csv" : "xlf"}`);
|
|
1058
1202
|
writeFileSync(path, format === "csv" ? toCsv(entries) : toXliff(cfg.sourceLocale, locale, entries));
|
|
1059
1203
|
files.push({
|
|
1060
1204
|
locale,
|
|
1061
1205
|
path,
|
|
1062
1206
|
total: entries.length,
|
|
1063
|
-
untranslated:
|
|
1207
|
+
untranslated: untranslated.length
|
|
1064
1208
|
});
|
|
1065
1209
|
}
|
|
1066
1210
|
return {
|
|
@@ -1080,8 +1224,8 @@ function importCatalogs(cfg, catalogs, files, options = {}) {
|
|
|
1080
1224
|
for (const file of files) {
|
|
1081
1225
|
const parsed = parseExchangeFile(file, options.locale);
|
|
1082
1226
|
const locale = parsed.locale;
|
|
1083
|
-
if (!/^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]+)*$/.test(locale)) throw new Error(`[verbaly] ${file}: "${locale}" doesn't look like a locale
|
|
1084
|
-
if (locale === cfg.sourceLocale) throw new Error(`[verbaly] ${file} targets the source locale "${locale}"
|
|
1227
|
+
if (!/^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]+)*$/.test(locale)) throw new Error(`[verbaly] ${file}: "${locale}" doesn't look like a locale, pass --locale <id>.`);
|
|
1228
|
+
if (locale === cfg.sourceLocale) throw new Error(`[verbaly] ${file} targets the source locale "${locale}": import fills translations, not the source. Pass --locale if the detection is wrong.`);
|
|
1085
1229
|
const catalog = catalogs[locale] ??= {};
|
|
1086
1230
|
for (const [key, text] of Object.entries(parsed.entries)) {
|
|
1087
1231
|
if (!text.trim()) continue;
|
|
@@ -1108,7 +1252,7 @@ function parseExchangeFile(file, localeOverride) {
|
|
|
1108
1252
|
if (/\.(xlf|xliff)$/i.test(file)) {
|
|
1109
1253
|
const parsed = parseXliff(content);
|
|
1110
1254
|
const locale = localeOverride ?? parsed.locale;
|
|
1111
|
-
if (!locale) throw new Error(`[verbaly] ${file} has no trgLang/target-language
|
|
1255
|
+
if (!locale) throw new Error(`[verbaly] ${file} has no trgLang/target-language, pass --locale <id>.`);
|
|
1112
1256
|
return {
|
|
1113
1257
|
locale,
|
|
1114
1258
|
entries: parsed.entries
|
|
@@ -1118,7 +1262,7 @@ function parseExchangeFile(file, localeOverride) {
|
|
|
1118
1262
|
locale: localeOverride ?? basename(file).replace(/\.csv$/i, ""),
|
|
1119
1263
|
entries: parseCsv(content)
|
|
1120
1264
|
};
|
|
1121
|
-
throw new Error(`[verbaly] ${file}: unsupported format
|
|
1265
|
+
throw new Error(`[verbaly] ${file}: unsupported format, expected .xlf, .xliff or .csv.`);
|
|
1122
1266
|
}
|
|
1123
1267
|
function toXliff(sourceLocale, locale, entries) {
|
|
1124
1268
|
const units = entries.map(({ key, source, target }) => [
|
|
@@ -1264,7 +1408,7 @@ async function loadSdk(load = () => import("@anthropic-ai/sdk")) {
|
|
|
1264
1408
|
try {
|
|
1265
1409
|
return (await load()).default;
|
|
1266
1410
|
} catch (error) {
|
|
1267
|
-
if (isModuleNotFound(error, "@anthropic-ai/sdk")) throw new Error("[verbaly] the claude translate provider needs @anthropic-ai/sdk
|
|
1411
|
+
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 });
|
|
1268
1412
|
throw error;
|
|
1269
1413
|
}
|
|
1270
1414
|
}
|
|
@@ -1539,31 +1683,6 @@ function decodeEntities(text) {
|
|
|
1539
1683
|
return text.replace(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
1540
1684
|
}
|
|
1541
1685
|
//#endregion
|
|
1542
|
-
|
|
1543
|
-
function transformCode(code, file, analysis) {
|
|
1544
|
-
const { tagged } = analysis ?? analyze(code, file);
|
|
1545
|
-
if (tagged.length === 0) return null;
|
|
1546
|
-
const s = new MagicString(code);
|
|
1547
|
-
for (const msg of tagged) {
|
|
1548
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1549
|
-
const entries = msg.params.filter((p) => !seen.has(p.name) && seen.add(p.name));
|
|
1550
|
-
const pairs = entries.map((p) => `${JSON.stringify(p.name)}: ${code.slice(p.start, p.end)}`).join(", ");
|
|
1551
|
-
if (msg.jsx) {
|
|
1552
|
-
const values = entries.length ? ` values={{ ${pairs} }}` : "";
|
|
1553
|
-
const components = msg.jsx.components.length ? ` components={{ ${msg.jsx.components.map((c) => `${JSON.stringify(c.name)}: ${c.source}`).join(", ")} }}` : "";
|
|
1554
|
-
s.overwrite(msg.start, msg.end, `<${msg.jsx.name} id=${JSON.stringify(msg.key)}${values}${components} />`);
|
|
1555
|
-
continue;
|
|
1556
|
-
}
|
|
1557
|
-
const tagSource = code.slice(msg.tagStart, msg.tagEnd);
|
|
1558
|
-
const args = entries.length ? `, { ${pairs} }` : "";
|
|
1559
|
-
s.overwrite(msg.start, msg.end, `${tagSource}(${JSON.stringify(msg.key)}${args})`);
|
|
1560
|
-
}
|
|
1561
|
-
return {
|
|
1562
|
-
code: s.toString(),
|
|
1563
|
-
map: s.generateMap({ hires: true })
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
//#endregion
|
|
1567
|
-
export { LOCALE_MODULE_PREFIX, MessageRegistry, PSEUDO_LOCALE, RESOLVED_VIRTUAL_ID, SOURCE_FILE_RE, VIRTUAL_ID, analyze, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, generateDts, generateLocaleModule, generateRuntimeModule, importCatalogs, init, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, structureMatches, syncCatalogs, targetLocales, transformCode, translateCatalogs, writeCatalog, writeDts };
|
|
1686
|
+
export { LOCALE_MODULE_PREFIX, MessageRegistry, PSEUDO_LOCALE, RESOLVED_VIRTUAL_ID, SFC_FILE_RE, SOURCE_FILE_RE, VIRTUAL_ID, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, generateDts, generateLocaleModule, generateRuntimeModule, importCatalogs, init, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, writeCatalog, writeDts };
|
|
1568
1687
|
|
|
1569
1688
|
//# sourceMappingURL=index.js.map
|