@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/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 ? ` "${truncate(entry.source, 40)}"` : "";
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} used in ${entry.files.join(", ")}`);
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 parts = [];
131
- if (types.has("number")) parts.push("number");
132
- if (types.has("string")) parts.push("string");
133
- if (types.has("date")) parts.push("Date | number | string");
134
- return parts.join(" | ");
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(entries) {
142
+ function generateDts(catalog) {
139
143
  const lines = [];
140
- for (const [key, message] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
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 do not edit
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, entries) {
190
- writeFileSync(join(cfg.root, "verbaly.d.ts"), generateDts(entries));
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 install it: pnpm add -D esbuild`, { cause: error });
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
- function analyze(code, file) {
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
- tagged.push({
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
- tagged.push({
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 === "t";
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 === "t";
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 [key, value] of Object.entries(node)) {
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/registry.ts
658
- var MessageRegistry = class {
659
- files = /* @__PURE__ */ new Map();
660
- update(file, analysis) {
661
- this.files.set(file, analysis);
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
- remove(file) {
664
- this.files.delete(file);
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
- messages() {
667
- const out = /* @__PURE__ */ new Map();
668
- for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
669
- const existing = out.get(msg.key);
670
- if (existing) {
671
- if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} — second dropped.`);
672
- continue;
673
- }
674
- out.set(msg.key, msg);
675
- }
676
- return out;
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
- usedKeys() {
679
- const out = /* @__PURE__ */ new Map();
680
- for (const analysis of this.files.values()) for (const used of analysis.usedKeys) {
681
- const files = out.get(used.key) ?? [];
682
- if (!files.includes(used.file)) files.push(used.file);
683
- out.set(used.key, files);
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
- return out;
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, analyze(readFileSync(file, "utf8"), 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 = (check, message) => entries.push({
908
+ const ok = (name, message) => entries.push({
804
909
  level: "ok",
805
- check,
910
+ check: name,
806
911
  message
807
912
  });
808
- const warn = (check, message, fix) => entries.push({
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 = (check, message, fix) => entries.push({
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 running on defaults", "run `npx verbaly init`");
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 CLI flow (extract/check) applies");
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(new Map(Object.entries(source)))) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
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 `npx verbaly check` for details");
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 `npx verbaly check` for details");
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
- let entries = Object.keys(source).filter((key) => source[key]).sort().map((key) => ({
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
- if (options.missing) entries = entries.filter((entry) => !entry.target);
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: entries.filter((entry) => !entry.target).length
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 pass --locale <id>.`);
1004
- 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.`);
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 pass --locale <id>.`);
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 expected .xlf, .xliff or .csv.`);
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 i18n compiler
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 enables hreflang alternates (render)
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 ${result.entries.length} checks`);
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} ${keys.join(", ")}` : ` ${locale}: -${keys.length} pruned`);
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, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));
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 ${keys.join(", ")}`);
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}" use xliff or csv`);
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 ${keys.join(", ")}`);
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" ? " did you mean --locales?" : ""}`);
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-mpjdqqnY.js");
1799
+ const { claudeProvider } = await import("./claude-C-ETlqsW.js");
1694
1800
  return claudeProvider({ model: model ?? cfg.translate.model });
1695
1801
  }
1696
1802
  //#endregion