@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/cli.js
CHANGED
|
@@ -74,13 +74,13 @@ function formatCheckResult(result) {
|
|
|
74
74
|
if (result.missing.length > 0) {
|
|
75
75
|
lines.push("missing translations:");
|
|
76
76
|
for (const entry of result.missing) {
|
|
77
|
-
const hint = entry.source ?
|
|
77
|
+
const hint = entry.source ? `: "${truncate(entry.source, 40)}"` : "";
|
|
78
78
|
lines.push(` [${entry.locale}] ${entry.key}${hint}`);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
if (result.unknown.length > 0) {
|
|
82
82
|
lines.push("unknown keys (not in any catalog):");
|
|
83
|
-
for (const entry of result.unknown) lines.push(` ${entry.key}
|
|
83
|
+
for (const entry of result.unknown) lines.push(` ${entry.key} (used in ${entry.files.join(", ")})`);
|
|
84
84
|
}
|
|
85
85
|
return lines.join("\n");
|
|
86
86
|
}
|
|
@@ -127,17 +127,21 @@ function visit(nodes, out) {
|
|
|
127
127
|
}
|
|
128
128
|
function renderParamType(types) {
|
|
129
129
|
if (types.has("unknown") || types.size === 0) return "unknown";
|
|
130
|
-
const
|
|
131
|
-
if (types.has("number"))
|
|
132
|
-
if (types.has("string"))
|
|
133
|
-
if (types.has("date"))
|
|
134
|
-
|
|
130
|
+
const members = /* @__PURE__ */ new Set();
|
|
131
|
+
if (types.has("number")) members.add("number");
|
|
132
|
+
if (types.has("string")) members.add("string");
|
|
133
|
+
if (types.has("date")) for (const m of [
|
|
134
|
+
"Date",
|
|
135
|
+
"number",
|
|
136
|
+
"string"
|
|
137
|
+
]) members.add(m);
|
|
138
|
+
return [...members].join(" | ");
|
|
135
139
|
}
|
|
136
140
|
//#endregion
|
|
137
141
|
//#region src/codegen.ts
|
|
138
|
-
function generateDts(
|
|
142
|
+
function generateDts(catalog) {
|
|
139
143
|
const lines = [];
|
|
140
|
-
for (const [key, message] of
|
|
144
|
+
for (const [key, message] of Object.entries(catalog).sort(([a], [b]) => a.localeCompare(b))) {
|
|
141
145
|
const params = collectParams(message);
|
|
142
146
|
if (params.size === 0) lines.push(` ${JSON.stringify(key)}: never;`);
|
|
143
147
|
else {
|
|
@@ -145,7 +149,7 @@ function generateDts(entries) {
|
|
|
145
149
|
lines.push(` ${JSON.stringify(key)}: { ${fields} };`);
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
|
-
return `// generated by verbaly
|
|
152
|
+
return `// generated by verbaly: do not edit
|
|
149
153
|
declare module 'virtual:verbaly' {
|
|
150
154
|
export interface VerbalyMessages {
|
|
151
155
|
${lines.join("\n")}
|
|
@@ -186,8 +190,13 @@ declare module 'virtual:verbaly/locale/*' {
|
|
|
186
190
|
}
|
|
187
191
|
`;
|
|
188
192
|
}
|
|
189
|
-
function writeDts(cfg,
|
|
190
|
-
|
|
193
|
+
function writeDts(cfg, catalog) {
|
|
194
|
+
const file = join(cfg.root, "verbaly.d.ts");
|
|
195
|
+
const content = generateDts(catalog);
|
|
196
|
+
try {
|
|
197
|
+
if (readFileSync(file, "utf8") === content) return;
|
|
198
|
+
} catch {}
|
|
199
|
+
writeFileSync(file, content);
|
|
191
200
|
}
|
|
192
201
|
//#endregion
|
|
193
202
|
//#region src/pseudo.ts
|
|
@@ -323,7 +332,7 @@ function resolveConfig(config = {}) {
|
|
|
323
332
|
dir,
|
|
324
333
|
sourceLocale,
|
|
325
334
|
locales: [...locales],
|
|
326
|
-
include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
|
|
335
|
+
include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue}"],
|
|
327
336
|
exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
|
|
328
337
|
translate: config.translate ?? {},
|
|
329
338
|
render: config.render ?? {}
|
|
@@ -358,7 +367,7 @@ async function loadTsConfig(path) {
|
|
|
358
367
|
const { mod } = await bundleRequire({ filepath: path });
|
|
359
368
|
return mod.default ?? {};
|
|
360
369
|
} catch (error) {
|
|
361
|
-
if (isModuleNotFound(error, "esbuild")) throw new Error(`[verbaly] ${path} needs esbuild
|
|
370
|
+
if (isModuleNotFound(error, "esbuild")) throw new Error(`[verbaly] ${path} needs esbuild: install it with pnpm add -D esbuild`, { cause: error });
|
|
362
371
|
throw error;
|
|
363
372
|
}
|
|
364
373
|
}
|
|
@@ -379,6 +388,38 @@ function definedOnly(config) {
|
|
|
379
388
|
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
|
|
380
389
|
}
|
|
381
390
|
//#endregion
|
|
391
|
+
//#region src/registry.ts
|
|
392
|
+
var MessageRegistry = class {
|
|
393
|
+
files = /* @__PURE__ */ new Map();
|
|
394
|
+
update(file, analysis) {
|
|
395
|
+
this.files.set(file, analysis);
|
|
396
|
+
}
|
|
397
|
+
remove(file) {
|
|
398
|
+
this.files.delete(file);
|
|
399
|
+
}
|
|
400
|
+
messages() {
|
|
401
|
+
const out = /* @__PURE__ */ new Map();
|
|
402
|
+
for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
|
|
403
|
+
const existing = out.get(msg.key);
|
|
404
|
+
if (existing) {
|
|
405
|
+
if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)}, second dropped.`);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
out.set(msg.key, msg);
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
usedKeys() {
|
|
413
|
+
const out = /* @__PURE__ */ new Map();
|
|
414
|
+
for (const analysis of this.files.values()) for (const used of analysis.usedKeys) {
|
|
415
|
+
const files = out.get(used.key) ?? [];
|
|
416
|
+
if (!files.includes(used.file)) files.push(used.file);
|
|
417
|
+
out.set(used.key, files);
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
//#endregion
|
|
382
423
|
//#region src/key.ts
|
|
383
424
|
function stableKey(message) {
|
|
384
425
|
return createHash("sha256").update(message).digest("base64url").slice(0, 8);
|
|
@@ -392,7 +433,9 @@ const SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
392
433
|
"innerComments",
|
|
393
434
|
"extra"
|
|
394
435
|
]);
|
|
395
|
-
|
|
436
|
+
const DEFAULT_T_NAMES = ["t"];
|
|
437
|
+
function analyze(code, file, options = {}) {
|
|
438
|
+
const names = new Set(options.tNames ?? DEFAULT_T_NAMES);
|
|
396
439
|
const ast = parse$1(code, {
|
|
397
440
|
sourceType: "module",
|
|
398
441
|
errorRecovery: true,
|
|
@@ -403,10 +446,10 @@ function analyze(code, file) {
|
|
|
403
446
|
walk(ast.program, (node) => {
|
|
404
447
|
if (node.type === "TaggedTemplateExpression") {
|
|
405
448
|
const tag = node.tag;
|
|
406
|
-
const explicit = explicitId(tag);
|
|
407
|
-
if (!explicit && !isTReference(tag)) return;
|
|
449
|
+
const explicit = explicitId(tag, names);
|
|
450
|
+
if (!explicit && !isTReference(tag, names)) return;
|
|
408
451
|
const quasi = node.quasi;
|
|
409
|
-
const message = buildMessage(code, quasi);
|
|
452
|
+
const message = buildMessage(code, quasi, names);
|
|
410
453
|
if (!message) return;
|
|
411
454
|
tagged.push({
|
|
412
455
|
key: explicit ? explicit.key : stableKey(message.text),
|
|
@@ -420,47 +463,48 @@ function analyze(code, file) {
|
|
|
420
463
|
});
|
|
421
464
|
} else if (node.type === "CallExpression") {
|
|
422
465
|
const callee = node.callee;
|
|
423
|
-
if (!isTReference(callee)) return;
|
|
466
|
+
if (!isTReference(callee, names)) return;
|
|
424
467
|
const first = node.arguments[0];
|
|
425
468
|
if (first?.type === "StringLiteral") usedKeys.push({
|
|
426
469
|
key: first.value,
|
|
427
470
|
file
|
|
428
471
|
});
|
|
429
|
-
} else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys);
|
|
472
|
+
} else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys, names);
|
|
430
473
|
});
|
|
431
474
|
return {
|
|
432
475
|
tagged,
|
|
433
476
|
usedKeys
|
|
434
477
|
};
|
|
435
478
|
}
|
|
436
|
-
function handleTrans(code, node, file, tagged, usedKeys) {
|
|
479
|
+
function handleTrans(code, node, file, tagged, usedKeys, names) {
|
|
437
480
|
const opening = node.openingElement;
|
|
438
481
|
const nameNode = opening.name;
|
|
439
482
|
if (nameNode.type !== "JSXIdentifier" || nameNode.name !== "Trans") return;
|
|
440
483
|
const attrs = opening.attributes;
|
|
441
484
|
const idAttr = attrs.find((a) => a.type === "JSXAttribute" && a.name.name === "id");
|
|
442
485
|
const children = node.children;
|
|
486
|
+
const push = (key, built) => tagged.push({
|
|
487
|
+
key,
|
|
488
|
+
message: built.text,
|
|
489
|
+
params: built.params,
|
|
490
|
+
start: node.start,
|
|
491
|
+
end: node.end,
|
|
492
|
+
tagStart: nameNode.start,
|
|
493
|
+
tagEnd: nameNode.end,
|
|
494
|
+
file,
|
|
495
|
+
jsx: {
|
|
496
|
+
name: nameNode.name,
|
|
497
|
+
components: built.components
|
|
498
|
+
}
|
|
499
|
+
});
|
|
443
500
|
if (idAttr) {
|
|
444
501
|
const value = idAttr.value;
|
|
445
502
|
if (value?.type !== "StringLiteral") return;
|
|
446
503
|
const id = value.value;
|
|
447
504
|
if (attrs.length === 1 && children?.length) {
|
|
448
|
-
const built = buildTransMessage(code, children);
|
|
505
|
+
const built = buildTransMessage(code, children, names);
|
|
449
506
|
if (built?.text.trim()) {
|
|
450
|
-
|
|
451
|
-
key: id,
|
|
452
|
-
message: built.text,
|
|
453
|
-
params: built.params,
|
|
454
|
-
start: node.start,
|
|
455
|
-
end: node.end,
|
|
456
|
-
tagStart: nameNode.start,
|
|
457
|
-
tagEnd: nameNode.end,
|
|
458
|
-
file,
|
|
459
|
-
jsx: {
|
|
460
|
-
name: nameNode.name,
|
|
461
|
-
components: built.components
|
|
462
|
-
}
|
|
463
|
-
});
|
|
507
|
+
push(id, built);
|
|
464
508
|
return;
|
|
465
509
|
}
|
|
466
510
|
}
|
|
@@ -472,31 +516,18 @@ function handleTrans(code, node, file, tagged, usedKeys) {
|
|
|
472
516
|
}
|
|
473
517
|
if (attrs.length > 0) return;
|
|
474
518
|
if (!children?.length) return;
|
|
475
|
-
const built = buildTransMessage(code, children);
|
|
519
|
+
const built = buildTransMessage(code, children, names);
|
|
476
520
|
if (!built || !built.text.trim()) return;
|
|
477
|
-
|
|
478
|
-
key: stableKey(built.text),
|
|
479
|
-
message: built.text,
|
|
480
|
-
params: built.params,
|
|
481
|
-
start: node.start,
|
|
482
|
-
end: node.end,
|
|
483
|
-
tagStart: nameNode.start,
|
|
484
|
-
tagEnd: nameNode.end,
|
|
485
|
-
file,
|
|
486
|
-
jsx: {
|
|
487
|
-
name: nameNode.name,
|
|
488
|
-
components: built.components
|
|
489
|
-
}
|
|
490
|
-
});
|
|
521
|
+
push(stableKey(built.text), built);
|
|
491
522
|
}
|
|
492
|
-
function explicitId(tag) {
|
|
523
|
+
function explicitId(tag, names) {
|
|
493
524
|
if (tag.type !== "CallExpression") return void 0;
|
|
494
525
|
const callee = tag.callee;
|
|
495
526
|
if (callee.type !== "MemberExpression" || callee.computed) return void 0;
|
|
496
527
|
const prop = callee.property;
|
|
497
528
|
if (prop.type !== "Identifier" || prop.name !== "id") return void 0;
|
|
498
529
|
const obj = callee.object;
|
|
499
|
-
if (!isTReference(obj)) return void 0;
|
|
530
|
+
if (!isTReference(obj, names)) return void 0;
|
|
500
531
|
const args = tag.arguments;
|
|
501
532
|
const first = args[0];
|
|
502
533
|
if (args.length !== 1 || first?.type !== "StringLiteral") return void 0;
|
|
@@ -506,7 +537,7 @@ function explicitId(tag) {
|
|
|
506
537
|
refEnd: obj.end
|
|
507
538
|
};
|
|
508
539
|
}
|
|
509
|
-
function buildTransMessage(code, children) {
|
|
540
|
+
function buildTransMessage(code, children, names) {
|
|
510
541
|
const params = [];
|
|
511
542
|
const components = [];
|
|
512
543
|
const takenParams = /* @__PURE__ */ new Map();
|
|
@@ -521,7 +552,7 @@ function buildTransMessage(code, children) {
|
|
|
521
552
|
text += escapeText(expr.value);
|
|
522
553
|
continue;
|
|
523
554
|
}
|
|
524
|
-
if (containsTaggedT(expr)) return void 0;
|
|
555
|
+
if (containsTaggedT(expr, names)) return void 0;
|
|
525
556
|
const source = code.slice(expr.start, expr.end);
|
|
526
557
|
const name = uniqueName(deriveName(expr, params.length), source, takenParams);
|
|
527
558
|
params.push({
|
|
@@ -574,24 +605,24 @@ function selfClosedSource(code, opening) {
|
|
|
574
605
|
const src = code.slice(opening.start, opening.end);
|
|
575
606
|
return src.endsWith("/>") ? src : `${src.slice(0, -1).trimEnd()} />`;
|
|
576
607
|
}
|
|
577
|
-
function containsTaggedT(node) {
|
|
608
|
+
function containsTaggedT(node, names) {
|
|
578
609
|
let found = false;
|
|
579
610
|
walk(node, (n) => {
|
|
580
611
|
if (n.type !== "TaggedTemplateExpression") return;
|
|
581
612
|
const tag = n.tag;
|
|
582
|
-
if (isTReference(tag) || explicitId(tag)) found = true;
|
|
613
|
+
if (isTReference(tag, names) || explicitId(tag, names)) found = true;
|
|
583
614
|
});
|
|
584
615
|
return found;
|
|
585
616
|
}
|
|
586
|
-
function isTReference(node) {
|
|
587
|
-
if (node.type === "Identifier") return node.name
|
|
617
|
+
function isTReference(node, names) {
|
|
618
|
+
if (node.type === "Identifier") return names.has(node.name);
|
|
588
619
|
if (node.type === "MemberExpression" && !node.computed) {
|
|
589
620
|
const prop = node.property;
|
|
590
|
-
return prop.type === "Identifier" && prop.name
|
|
621
|
+
return prop.type === "Identifier" && names.has(prop.name);
|
|
591
622
|
}
|
|
592
623
|
return false;
|
|
593
624
|
}
|
|
594
|
-
function buildMessage(code, quasi) {
|
|
625
|
+
function buildMessage(code, quasi, names) {
|
|
595
626
|
const quasis = quasi.quasis;
|
|
596
627
|
const expressions = quasi.expressions;
|
|
597
628
|
const params = [];
|
|
@@ -600,7 +631,7 @@ function buildMessage(code, quasi) {
|
|
|
600
631
|
for (let i = 0; i < expressions.length; i++) {
|
|
601
632
|
const expr = expressions[i];
|
|
602
633
|
if (!expr) return void 0;
|
|
603
|
-
if (containsTaggedT(expr)) return void 0;
|
|
634
|
+
if (containsTaggedT(expr, names)) return void 0;
|
|
604
635
|
const source = code.slice(expr.start, expr.end);
|
|
605
636
|
const name = uniqueName(deriveName(expr, i), source, taken);
|
|
606
637
|
params.push({
|
|
@@ -643,8 +674,9 @@ function uniqueName(base, source, taken) {
|
|
|
643
674
|
}
|
|
644
675
|
function walk(node, visit) {
|
|
645
676
|
visit(node);
|
|
646
|
-
for (const
|
|
677
|
+
for (const key in node) {
|
|
647
678
|
if (SKIP_KEYS.has(key)) continue;
|
|
679
|
+
const value = node[key];
|
|
648
680
|
if (Array.isArray(value)) {
|
|
649
681
|
for (const item of value) if (isNode(item)) walk(item, visit);
|
|
650
682
|
} else if (isNode(value)) walk(value, visit);
|
|
@@ -654,37 +686,110 @@ function isNode(value) {
|
|
|
654
686
|
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
655
687
|
}
|
|
656
688
|
//#endregion
|
|
657
|
-
//#region src/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
689
|
+
//#region src/sfc.ts
|
|
690
|
+
const SFC_FILE_RE = /\.(?:svelte|vue)$/;
|
|
691
|
+
function analyzeFile(code, file) {
|
|
692
|
+
return SFC_FILE_RE.test(file) ? analyzeSfc(code, file) : analyze(code, file);
|
|
693
|
+
}
|
|
694
|
+
const SCRIPT_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi;
|
|
695
|
+
const STYLE_RE = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
|
|
696
|
+
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
697
|
+
const CANDIDATE_SVELTE = /(?<![\w$])\$?t(?=[`(]|\.id\()/g;
|
|
698
|
+
const CANDIDATE_VUE = /(?<![\w$])t(?=[`(]|\.id\()/g;
|
|
699
|
+
function analyzeSfc(code, file) {
|
|
700
|
+
const svelte = file.endsWith(".svelte");
|
|
701
|
+
const options = svelte ? { tNames: ["t", "$t"] } : void 0;
|
|
702
|
+
const tagged = [];
|
|
703
|
+
const usedKeys = [];
|
|
704
|
+
for (const match of code.matchAll(SCRIPT_RE)) {
|
|
705
|
+
const content = match[2];
|
|
706
|
+
if (!content) continue;
|
|
707
|
+
merge(analyzeSegment(content, file, options), match.index + match[1].length, false);
|
|
662
708
|
}
|
|
663
|
-
|
|
664
|
-
|
|
709
|
+
const markup = blank(blank(blank(code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
|
|
710
|
+
const candidates = svelte ? CANDIDATE_SVELTE : CANDIDATE_VUE;
|
|
711
|
+
let consumedTo = 0;
|
|
712
|
+
for (const match of markup.matchAll(candidates)) {
|
|
713
|
+
const start = match.index;
|
|
714
|
+
if (start < consumedTo) continue;
|
|
715
|
+
const end = expressionExtent(markup, start);
|
|
716
|
+
if (end === void 0) continue;
|
|
717
|
+
merge(analyzeSegment(code.slice(start, end), file, options), start, true);
|
|
718
|
+
consumedTo = end;
|
|
665
719
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
720
|
+
return {
|
|
721
|
+
tagged,
|
|
722
|
+
usedKeys
|
|
723
|
+
};
|
|
724
|
+
function merge(analysis, offset, markupSegment) {
|
|
725
|
+
if (!analysis) return;
|
|
726
|
+
for (const msg of analysis.tagged) tagged.push({
|
|
727
|
+
...msg,
|
|
728
|
+
start: msg.start + offset,
|
|
729
|
+
end: msg.end + offset,
|
|
730
|
+
tagStart: msg.tagStart + offset,
|
|
731
|
+
tagEnd: msg.tagEnd + offset,
|
|
732
|
+
params: msg.params.map((p) => ({
|
|
733
|
+
...p,
|
|
734
|
+
start: p.start + offset,
|
|
735
|
+
end: p.end + offset
|
|
736
|
+
})),
|
|
737
|
+
...markupSegment && { singleQuote: true }
|
|
738
|
+
});
|
|
739
|
+
usedKeys.push(...analysis.usedKeys);
|
|
677
740
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
741
|
+
}
|
|
742
|
+
function analyzeSegment(code, file, options) {
|
|
743
|
+
try {
|
|
744
|
+
return analyze(code, file, options);
|
|
745
|
+
} catch {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function blank(source, re) {
|
|
750
|
+
return source.replace(re, (m) => " ".repeat(m.length));
|
|
751
|
+
}
|
|
752
|
+
function expressionExtent(code, start) {
|
|
753
|
+
let i = start;
|
|
754
|
+
if (code[i] === "$") i += 1;
|
|
755
|
+
i += 1;
|
|
756
|
+
if (code.startsWith(".id(", i)) {
|
|
757
|
+
const close = balancedEnd(code, i + 3);
|
|
758
|
+
if (close === void 0 || code[close] !== "`") return void 0;
|
|
759
|
+
return balancedEnd(code, close);
|
|
760
|
+
}
|
|
761
|
+
if (code[i] === "`" || code[i] === "(") return balancedEnd(code, i);
|
|
762
|
+
}
|
|
763
|
+
function balancedEnd(code, open) {
|
|
764
|
+
const stack = [];
|
|
765
|
+
let i = open;
|
|
766
|
+
while (i < code.length) {
|
|
767
|
+
const ch = code[i];
|
|
768
|
+
if (stack[stack.length - 1] === "`") {
|
|
769
|
+
if (ch === "\\") i += 1;
|
|
770
|
+
else if (ch === "`") stack.pop();
|
|
771
|
+
else if (ch === "$" && code[i + 1] === "{") {
|
|
772
|
+
stack.push("{");
|
|
773
|
+
i += 1;
|
|
774
|
+
}
|
|
775
|
+
} else if (ch === "'" || ch === "\"") i = stringEnd(code, i);
|
|
776
|
+
else if (ch === "`" || ch === "(" || ch === "{") stack.push(ch);
|
|
777
|
+
else if (ch === ")" || ch === "}") {
|
|
778
|
+
if (stack[stack.length - 1] !== (ch === ")" ? "(" : "{")) return void 0;
|
|
779
|
+
stack.pop();
|
|
684
780
|
}
|
|
685
|
-
|
|
781
|
+
i += 1;
|
|
782
|
+
if (stack.length === 0) return i;
|
|
686
783
|
}
|
|
687
|
-
}
|
|
784
|
+
}
|
|
785
|
+
function stringEnd(code, start) {
|
|
786
|
+
const quote = code[start];
|
|
787
|
+
let i = start + 1;
|
|
788
|
+
while (i < code.length) if (code[i] === "\\") i += 2;
|
|
789
|
+
else if (code[i] === quote) return i;
|
|
790
|
+
else i += 1;
|
|
791
|
+
return code.length;
|
|
792
|
+
}
|
|
688
793
|
//#endregion
|
|
689
794
|
//#region src/extract.ts
|
|
690
795
|
async function extractProject(cfg) {
|
|
@@ -694,7 +799,7 @@ async function extractProject(cfg) {
|
|
|
694
799
|
absolute: true
|
|
695
800
|
});
|
|
696
801
|
const registry = new MessageRegistry();
|
|
697
|
-
for (const file of files) registry.update(file,
|
|
802
|
+
for (const file of files) registry.update(file, analyzeFile(readFileSync(file, "utf8"), file));
|
|
698
803
|
return registry;
|
|
699
804
|
}
|
|
700
805
|
function syncCatalogs(cfg, catalogs, registry) {
|
|
@@ -800,27 +905,27 @@ function init(options = {}) {
|
|
|
800
905
|
const PREVIEW = 5;
|
|
801
906
|
async function doctor(cfg) {
|
|
802
907
|
const entries = [];
|
|
803
|
-
const ok = (
|
|
908
|
+
const ok = (name, message) => entries.push({
|
|
804
909
|
level: "ok",
|
|
805
|
-
check,
|
|
910
|
+
check: name,
|
|
806
911
|
message
|
|
807
912
|
});
|
|
808
|
-
const warn = (
|
|
913
|
+
const warn = (name, message, fix) => entries.push({
|
|
809
914
|
level: "warn",
|
|
810
|
-
check,
|
|
915
|
+
check: name,
|
|
811
916
|
message,
|
|
812
917
|
fix
|
|
813
918
|
});
|
|
814
|
-
const error = (
|
|
919
|
+
const error = (name, message, fix) => entries.push({
|
|
815
920
|
level: "error",
|
|
816
|
-
check,
|
|
921
|
+
check: name,
|
|
817
922
|
message,
|
|
818
923
|
fix
|
|
819
924
|
});
|
|
820
925
|
const rel = (path) => relative(cfg.root, path).replaceAll("\\", "/");
|
|
821
926
|
const configFile = findConfigFile(cfg.root);
|
|
822
927
|
if (configFile) ok("config", `${configFile} found`);
|
|
823
|
-
else warn("config", "no config file
|
|
928
|
+
else warn("config", "no config file, running on defaults", "run `npx verbaly init`");
|
|
824
929
|
const catalogs = {};
|
|
825
930
|
let catalogsHealthy = true;
|
|
826
931
|
if (!existsSync(cfg.dir)) {
|
|
@@ -853,14 +958,14 @@ async function doctor(cfg) {
|
|
|
853
958
|
const deps = readDeps(cfg.root);
|
|
854
959
|
const bundler = detectBundler(cfg.root);
|
|
855
960
|
const wired = deps["@verbaly/vite"] || deps["@verbaly/unplugin"];
|
|
856
|
-
if (!bundler) ok("plugin", "no bundler detected
|
|
961
|
+
if (!bundler) ok("plugin", "no bundler detected, the CLI flow (extract/check) applies");
|
|
857
962
|
else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
|
|
858
963
|
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");
|
|
859
964
|
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`);
|
|
860
965
|
if (source) {
|
|
861
966
|
const dtsPath = join(cfg.root, "verbaly.d.ts");
|
|
862
967
|
if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
|
|
863
|
-
else if (readFileSync(dtsPath, "utf8") !== generateDts(
|
|
968
|
+
else if (readFileSync(dtsPath, "utf8") !== generateDts(source)) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
|
|
864
969
|
else ok("types", "verbaly.d.ts is up to date");
|
|
865
970
|
}
|
|
866
971
|
const registry = await extractProject(cfg);
|
|
@@ -873,10 +978,10 @@ async function doctor(cfg) {
|
|
|
873
978
|
}
|
|
874
979
|
if (catalogsHealthy) {
|
|
875
980
|
const result = check(cfg, catalogs, registry);
|
|
876
|
-
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
|
|
981
|
+
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)");
|
|
877
982
|
if (result.missing.length > 0) {
|
|
878
983
|
const locales = [...new Set(result.missing.map((m) => m.locale))];
|
|
879
|
-
warn("translations", `${result.missing.length} missing ${result.missing.length === 1 ? "translation" : "translations"} (${locales.join(", ")})`, "run `npx verbaly translate` or fill the catalogs
|
|
984
|
+
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)");
|
|
880
985
|
}
|
|
881
986
|
if (result.ok) ok("translations", "all translations complete");
|
|
882
987
|
}
|
|
@@ -968,19 +1073,20 @@ function exportCatalogs(cfg, catalogs, options = {}) {
|
|
|
968
1073
|
mkdirSync(dir, { recursive: true });
|
|
969
1074
|
for (const locale of targets) {
|
|
970
1075
|
const catalog = catalogs[locale] ?? {};
|
|
971
|
-
|
|
1076
|
+
const all = Object.keys(source).filter((key) => source[key]).sort().map((key) => ({
|
|
972
1077
|
key,
|
|
973
1078
|
source: source[key],
|
|
974
1079
|
target: catalog[key] ?? ""
|
|
975
1080
|
}));
|
|
976
|
-
|
|
1081
|
+
const untranslated = all.filter((entry) => !entry.target);
|
|
1082
|
+
const entries = options.missing ? untranslated : all;
|
|
977
1083
|
const path = join(dir, `${locale}.${format === "csv" ? "csv" : "xlf"}`);
|
|
978
1084
|
writeFileSync(path, format === "csv" ? toCsv(entries) : toXliff(cfg.sourceLocale, locale, entries));
|
|
979
1085
|
files.push({
|
|
980
1086
|
locale,
|
|
981
1087
|
path,
|
|
982
1088
|
total: entries.length,
|
|
983
|
-
untranslated:
|
|
1089
|
+
untranslated: untranslated.length
|
|
984
1090
|
});
|
|
985
1091
|
}
|
|
986
1092
|
return {
|
|
@@ -1000,8 +1106,8 @@ function importCatalogs(cfg, catalogs, files, options = {}) {
|
|
|
1000
1106
|
for (const file of files) {
|
|
1001
1107
|
const parsed = parseExchangeFile(file, options.locale);
|
|
1002
1108
|
const locale = parsed.locale;
|
|
1003
|
-
if (!/^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]+)*$/.test(locale)) throw new Error(`[verbaly] ${file}: "${locale}" doesn't look like a locale
|
|
1004
|
-
if (locale === cfg.sourceLocale) throw new Error(`[verbaly] ${file} targets the source locale "${locale}"
|
|
1109
|
+
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>.`);
|
|
1110
|
+
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.`);
|
|
1005
1111
|
const catalog = catalogs[locale] ??= {};
|
|
1006
1112
|
for (const [key, text] of Object.entries(parsed.entries)) {
|
|
1007
1113
|
if (!text.trim()) continue;
|
|
@@ -1028,7 +1134,7 @@ function parseExchangeFile(file, localeOverride) {
|
|
|
1028
1134
|
if (/\.(xlf|xliff)$/i.test(file)) {
|
|
1029
1135
|
const parsed = parseXliff(content);
|
|
1030
1136
|
const locale = localeOverride ?? parsed.locale;
|
|
1031
|
-
if (!locale) throw new Error(`[verbaly] ${file} has no trgLang/target-language
|
|
1137
|
+
if (!locale) throw new Error(`[verbaly] ${file} has no trgLang/target-language, pass --locale <id>.`);
|
|
1032
1138
|
return {
|
|
1033
1139
|
locale,
|
|
1034
1140
|
entries: parsed.entries
|
|
@@ -1038,7 +1144,7 @@ function parseExchangeFile(file, localeOverride) {
|
|
|
1038
1144
|
locale: localeOverride ?? basename(file).replace(/\.csv$/i, ""),
|
|
1039
1145
|
entries: parseCsv(content)
|
|
1040
1146
|
};
|
|
1041
|
-
throw new Error(`[verbaly] ${file}: unsupported format
|
|
1147
|
+
throw new Error(`[verbaly] ${file}: unsupported format, expected .xlf, .xliff or .csv.`);
|
|
1042
1148
|
}
|
|
1043
1149
|
function toXliff(sourceLocale, locale, entries) {
|
|
1044
1150
|
const units = entries.map(({ key, source, target }) => [
|
|
@@ -1413,7 +1519,7 @@ function decodeEntities(text) {
|
|
|
1413
1519
|
}
|
|
1414
1520
|
//#endregion
|
|
1415
1521
|
//#region src/run.ts
|
|
1416
|
-
const HELP = `verbaly
|
|
1522
|
+
const HELP = `verbaly · i18n compiler
|
|
1417
1523
|
|
|
1418
1524
|
Usage:
|
|
1419
1525
|
verbaly init scaffold config + locale catalogs (detects your bundler)
|
|
@@ -1441,7 +1547,7 @@ Options:
|
|
|
1441
1547
|
--locale <id> pseudo-locale id (pseudo) / target-locale override (import)
|
|
1442
1548
|
--site <path> built site directory (render, default: dist)
|
|
1443
1549
|
--attribute <name> base data attribute (render, default: data-verbaly)
|
|
1444
|
-
--base-url <url> site origin
|
|
1550
|
+
--base-url <url> site origin, enables hreflang alternates (render)
|
|
1445
1551
|
--sitemap emit sitemap-i18n.xml with per-locale alternates (render)
|
|
1446
1552
|
--clean remove existing locale dirs before mirroring (render)
|
|
1447
1553
|
|
|
@@ -1508,7 +1614,7 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
1508
1614
|
warn: "⚠",
|
|
1509
1615
|
error: "✗"
|
|
1510
1616
|
};
|
|
1511
|
-
console.log(`[verbaly] doctor
|
|
1617
|
+
console.log(`[verbaly] doctor: ${result.entries.length} checks`);
|
|
1512
1618
|
for (const entry of result.entries) {
|
|
1513
1619
|
const line = ` ${icon[entry.level]} ${entry.check}: ${entry.message}`;
|
|
1514
1620
|
if (entry.level === "error") console.error(line);
|
|
@@ -1529,12 +1635,12 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
1529
1635
|
const catalogs = loadCatalogs(cfg);
|
|
1530
1636
|
if (values.prune) {
|
|
1531
1637
|
const removed = pruneCatalogs(cfg, catalogs, registry);
|
|
1532
|
-
for (const [locale, keys] of Object.entries(removed)) console.log(dryRun ? ` ${locale}: would prune ${keys.length}
|
|
1638
|
+
for (const [locale, keys] of Object.entries(removed)) console.log(dryRun ? ` ${locale}: would prune ${keys.length}: ${keys.join(", ")}` : ` ${locale}: -${keys.length} pruned`);
|
|
1533
1639
|
}
|
|
1534
1640
|
const { added } = syncCatalogs(cfg, catalogs, registry);
|
|
1535
1641
|
if (!dryRun) {
|
|
1536
1642
|
for (const locale of cfg.locales) writeCatalog(cfg, locale, catalogs[locale] ?? {});
|
|
1537
|
-
writeDts(cfg,
|
|
1643
|
+
writeDts(cfg, catalogs[cfg.sourceLocale] ?? {});
|
|
1538
1644
|
}
|
|
1539
1645
|
const total = registry.messages().size;
|
|
1540
1646
|
console.log(`[verbaly] ${total} messages · locales: ${cfg.locales.join(", ")}${dryRun ? " (dry run, nothing written)" : ""}`);
|
|
@@ -1565,7 +1671,7 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
1565
1671
|
console.log("[verbaly] nothing to translate ✓");
|
|
1566
1672
|
return;
|
|
1567
1673
|
}
|
|
1568
|
-
for (const [locale, keys] of entries) console.log(` ${locale}: ${keys.length} missing
|
|
1674
|
+
for (const [locale, keys] of entries) console.log(` ${locale}: ${keys.length} missing: ${keys.join(", ")}`);
|
|
1569
1675
|
return;
|
|
1570
1676
|
}
|
|
1571
1677
|
for (const locale of Object.keys(result.translated)) {
|
|
@@ -1579,7 +1685,7 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
1579
1685
|
if (command === "export") {
|
|
1580
1686
|
const format = values.format ?? "xliff";
|
|
1581
1687
|
if (format !== "xliff" && format !== "csv") {
|
|
1582
|
-
console.error(`[verbaly] unknown format "${values.format}"
|
|
1688
|
+
console.error(`[verbaly] unknown format "${values.format}", use xliff or csv`);
|
|
1583
1689
|
process.exitCode = 1;
|
|
1584
1690
|
return;
|
|
1585
1691
|
}
|
|
@@ -1631,7 +1737,7 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
1631
1737
|
clean: values.clean
|
|
1632
1738
|
});
|
|
1633
1739
|
console.log(`[verbaly] ${result.files} pages × ${result.locales.length} locales (${result.locales.join(", ")})`);
|
|
1634
|
-
for (const [locale, keys] of Object.entries(result.missing)) console.warn(` ${locale}: ${keys.length} keys not pre-filled
|
|
1740
|
+
for (const [locale, keys] of Object.entries(result.missing)) console.warn(` ${locale}: ${keys.length} keys not pre-filled: ${keys.join(", ")}`);
|
|
1635
1741
|
return;
|
|
1636
1742
|
}
|
|
1637
1743
|
if (command === "pseudo") {
|
|
@@ -1683,14 +1789,14 @@ function rejectStrayFlags(command, values) {
|
|
|
1683
1789
|
const allowed = /* @__PURE__ */ new Set([...COMMON_FLAGS, ...own]);
|
|
1684
1790
|
const stray = Object.keys(values).filter((k) => values[k] !== void 0 && !allowed.has(k));
|
|
1685
1791
|
if (stray.length === 0) return false;
|
|
1686
|
-
for (const flag of stray) console.error(`[verbaly] --${flag} is not a "${command}" flag${flag === "locale" ? "
|
|
1792
|
+
for (const flag of stray) console.error(`[verbaly] --${flag} is not a "${command}" flag${flag === "locale" ? " (did you mean --locales?)" : ""}`);
|
|
1687
1793
|
process.exitCode = 1;
|
|
1688
1794
|
return true;
|
|
1689
1795
|
}
|
|
1690
1796
|
async function resolveProvider(cfg, model) {
|
|
1691
1797
|
const configured = cfg.translate.provider;
|
|
1692
1798
|
if (typeof configured === "function") return configured;
|
|
1693
|
-
const { claudeProvider } = await import("./claude-
|
|
1799
|
+
const { claudeProvider } = await import("./claude-C-ETlqsW.js");
|
|
1694
1800
|
return claudeProvider({ model: model ?? cfg.translate.model });
|
|
1695
1801
|
}
|
|
1696
1802
|
//#endregion
|