@verbaly/compiler 0.18.0 → 0.20.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 CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ---
13
13
 
14
- The compiler behind [Verbaly](https://github.com/AronSoto/verbaly): AST extraction of `t\`...\``**and JSX`<Trans>` children** into stable hashed keys — or **readable keys** via `` t.id('inbox.title')`…` `` and `<Trans id="inbox.title">…</Trans>` — flat JSON catalog sync, and typed codegen. It also ships the **`verbaly` CLI**.
14
+ The compiler behind [Verbaly](https://github.com/AronSoto/verbaly): AST extraction of `t\`...\``**and JSX`<Trans>` children** into stable hashed keys — or **readable keys** via `` t.id('inbox.title')`…` `` and `<Trans id="inbox.title">…</Trans>` — flat JSON catalog sync, and typed codegen. It also ships the **`verbaly` CLI**. Extraction covers `.js/.ts/.jsx/.tsx` **and `.svelte`/`.vue` single-file components** (script blocks and markup, including Svelte's `$t` store form).
15
15
 
16
16
  > Most projects don't install this directly — [`@verbaly/vite`](https://www.npmjs.com/package/@verbaly/vite) wraps it with zero config. Reach for it when scripting extraction/checks yourself.
17
17
 
package/dist/cli.js CHANGED
@@ -145,7 +145,7 @@ function generateDts(entries) {
145
145
  lines.push(` ${JSON.stringify(key)}: { ${fields} };`);
146
146
  }
147
147
  }
148
- return `// generated by verbaly do not edit
148
+ return `// generated by verbaly: do not edit
149
149
  declare module 'virtual:verbaly' {
150
150
  export interface VerbalyMessages {
151
151
  ${lines.join("\n")}
@@ -156,6 +156,7 @@ ${lines.join("\n")}
156
156
 
157
157
  export const sourceLocale: string;
158
158
  export const locales: string[];
159
+ export function loadMessages(locale: string): Promise<Record<string, string>>;
159
160
  export function createInstance(
160
161
  options?: import('verbaly').VerbalyOptions<VerbalyKey>,
161
162
  ): import('verbaly').Verbaly<VerbalyKey>;
@@ -322,7 +323,7 @@ function resolveConfig(config = {}) {
322
323
  dir,
323
324
  sourceLocale,
324
325
  locales: [...locales],
325
- include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
326
+ include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue}"],
326
327
  exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
327
328
  translate: config.translate ?? {},
328
329
  render: config.render ?? {}
@@ -378,6 +379,38 @@ function definedOnly(config) {
378
379
  return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
379
380
  }
380
381
  //#endregion
382
+ //#region src/registry.ts
383
+ var MessageRegistry = class {
384
+ files = /* @__PURE__ */ new Map();
385
+ update(file, analysis) {
386
+ this.files.set(file, analysis);
387
+ }
388
+ remove(file) {
389
+ this.files.delete(file);
390
+ }
391
+ messages() {
392
+ const out = /* @__PURE__ */ new Map();
393
+ for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
394
+ const existing = out.get(msg.key);
395
+ if (existing) {
396
+ if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} — second dropped.`);
397
+ continue;
398
+ }
399
+ out.set(msg.key, msg);
400
+ }
401
+ return out;
402
+ }
403
+ usedKeys() {
404
+ const out = /* @__PURE__ */ new Map();
405
+ for (const analysis of this.files.values()) for (const used of analysis.usedKeys) {
406
+ const files = out.get(used.key) ?? [];
407
+ if (!files.includes(used.file)) files.push(used.file);
408
+ out.set(used.key, files);
409
+ }
410
+ return out;
411
+ }
412
+ };
413
+ //#endregion
381
414
  //#region src/key.ts
382
415
  function stableKey(message) {
383
416
  return createHash("sha256").update(message).digest("base64url").slice(0, 8);
@@ -391,7 +424,9 @@ const SKIP_KEYS = /* @__PURE__ */ new Set([
391
424
  "innerComments",
392
425
  "extra"
393
426
  ]);
394
- function analyze(code, file) {
427
+ const DEFAULT_T_NAMES = ["t"];
428
+ function analyze(code, file, options = {}) {
429
+ const names = new Set(options.tNames ?? DEFAULT_T_NAMES);
395
430
  const ast = parse$1(code, {
396
431
  sourceType: "module",
397
432
  errorRecovery: true,
@@ -402,10 +437,10 @@ function analyze(code, file) {
402
437
  walk(ast.program, (node) => {
403
438
  if (node.type === "TaggedTemplateExpression") {
404
439
  const tag = node.tag;
405
- const explicit = explicitId(tag);
406
- if (!explicit && !isTReference(tag)) return;
440
+ const explicit = explicitId(tag, names);
441
+ if (!explicit && !isTReference(tag, names)) return;
407
442
  const quasi = node.quasi;
408
- const message = buildMessage(code, quasi);
443
+ const message = buildMessage(code, quasi, names);
409
444
  if (!message) return;
410
445
  tagged.push({
411
446
  key: explicit ? explicit.key : stableKey(message.text),
@@ -419,20 +454,20 @@ function analyze(code, file) {
419
454
  });
420
455
  } else if (node.type === "CallExpression") {
421
456
  const callee = node.callee;
422
- if (!isTReference(callee)) return;
457
+ if (!isTReference(callee, names)) return;
423
458
  const first = node.arguments[0];
424
459
  if (first?.type === "StringLiteral") usedKeys.push({
425
460
  key: first.value,
426
461
  file
427
462
  });
428
- } else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys);
463
+ } else if (node.type === "JSXElement") handleTrans(code, node, file, tagged, usedKeys, names);
429
464
  });
430
465
  return {
431
466
  tagged,
432
467
  usedKeys
433
468
  };
434
469
  }
435
- function handleTrans(code, node, file, tagged, usedKeys) {
470
+ function handleTrans(code, node, file, tagged, usedKeys, names) {
436
471
  const opening = node.openingElement;
437
472
  const nameNode = opening.name;
438
473
  if (nameNode.type !== "JSXIdentifier" || nameNode.name !== "Trans") return;
@@ -444,7 +479,7 @@ function handleTrans(code, node, file, tagged, usedKeys) {
444
479
  if (value?.type !== "StringLiteral") return;
445
480
  const id = value.value;
446
481
  if (attrs.length === 1 && children?.length) {
447
- const built = buildTransMessage(code, children);
482
+ const built = buildTransMessage(code, children, names);
448
483
  if (built?.text.trim()) {
449
484
  tagged.push({
450
485
  key: id,
@@ -471,7 +506,7 @@ function handleTrans(code, node, file, tagged, usedKeys) {
471
506
  }
472
507
  if (attrs.length > 0) return;
473
508
  if (!children?.length) return;
474
- const built = buildTransMessage(code, children);
509
+ const built = buildTransMessage(code, children, names);
475
510
  if (!built || !built.text.trim()) return;
476
511
  tagged.push({
477
512
  key: stableKey(built.text),
@@ -488,14 +523,14 @@ function handleTrans(code, node, file, tagged, usedKeys) {
488
523
  }
489
524
  });
490
525
  }
491
- function explicitId(tag) {
526
+ function explicitId(tag, names) {
492
527
  if (tag.type !== "CallExpression") return void 0;
493
528
  const callee = tag.callee;
494
529
  if (callee.type !== "MemberExpression" || callee.computed) return void 0;
495
530
  const prop = callee.property;
496
531
  if (prop.type !== "Identifier" || prop.name !== "id") return void 0;
497
532
  const obj = callee.object;
498
- if (!isTReference(obj)) return void 0;
533
+ if (!isTReference(obj, names)) return void 0;
499
534
  const args = tag.arguments;
500
535
  const first = args[0];
501
536
  if (args.length !== 1 || first?.type !== "StringLiteral") return void 0;
@@ -505,7 +540,7 @@ function explicitId(tag) {
505
540
  refEnd: obj.end
506
541
  };
507
542
  }
508
- function buildTransMessage(code, children) {
543
+ function buildTransMessage(code, children, names) {
509
544
  const params = [];
510
545
  const components = [];
511
546
  const takenParams = /* @__PURE__ */ new Map();
@@ -520,7 +555,7 @@ function buildTransMessage(code, children) {
520
555
  text += escapeText(expr.value);
521
556
  continue;
522
557
  }
523
- if (containsTaggedT(expr)) return void 0;
558
+ if (containsTaggedT(expr, names)) return void 0;
524
559
  const source = code.slice(expr.start, expr.end);
525
560
  const name = uniqueName(deriveName(expr, params.length), source, takenParams);
526
561
  params.push({
@@ -573,24 +608,24 @@ function selfClosedSource(code, opening) {
573
608
  const src = code.slice(opening.start, opening.end);
574
609
  return src.endsWith("/>") ? src : `${src.slice(0, -1).trimEnd()} />`;
575
610
  }
576
- function containsTaggedT(node) {
611
+ function containsTaggedT(node, names) {
577
612
  let found = false;
578
613
  walk(node, (n) => {
579
614
  if (n.type !== "TaggedTemplateExpression") return;
580
615
  const tag = n.tag;
581
- if (isTReference(tag) || explicitId(tag)) found = true;
616
+ if (isTReference(tag, names) || explicitId(tag, names)) found = true;
582
617
  });
583
618
  return found;
584
619
  }
585
- function isTReference(node) {
586
- if (node.type === "Identifier") return node.name === "t";
620
+ function isTReference(node, names) {
621
+ if (node.type === "Identifier") return names.has(node.name);
587
622
  if (node.type === "MemberExpression" && !node.computed) {
588
623
  const prop = node.property;
589
- return prop.type === "Identifier" && prop.name === "t";
624
+ return prop.type === "Identifier" && names.has(prop.name);
590
625
  }
591
626
  return false;
592
627
  }
593
- function buildMessage(code, quasi) {
628
+ function buildMessage(code, quasi, names) {
594
629
  const quasis = quasi.quasis;
595
630
  const expressions = quasi.expressions;
596
631
  const params = [];
@@ -599,7 +634,7 @@ function buildMessage(code, quasi) {
599
634
  for (let i = 0; i < expressions.length; i++) {
600
635
  const expr = expressions[i];
601
636
  if (!expr) return void 0;
602
- if (containsTaggedT(expr)) return void 0;
637
+ if (containsTaggedT(expr, names)) return void 0;
603
638
  const source = code.slice(expr.start, expr.end);
604
639
  const name = uniqueName(deriveName(expr, i), source, taken);
605
640
  params.push({
@@ -653,37 +688,110 @@ function isNode(value) {
653
688
  return typeof value === "object" && value !== null && typeof value.type === "string";
654
689
  }
655
690
  //#endregion
656
- //#region src/registry.ts
657
- var MessageRegistry = class {
658
- files = /* @__PURE__ */ new Map();
659
- update(file, analysis) {
660
- this.files.set(file, analysis);
691
+ //#region src/sfc.ts
692
+ const SFC_FILE_RE = /\.(?:svelte|vue)$/;
693
+ function analyzeFile(code, file) {
694
+ return SFC_FILE_RE.test(file) ? analyzeSfc(code, file) : analyze(code, file);
695
+ }
696
+ const SCRIPT_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi;
697
+ const STYLE_RE = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
698
+ const COMMENT_RE = /<!--[\s\S]*?-->/g;
699
+ const CANDIDATE_SVELTE = /(?<![\w$])\$?t(?=[`(]|\.id\()/g;
700
+ const CANDIDATE_VUE = /(?<![\w$])t(?=[`(]|\.id\()/g;
701
+ function analyzeSfc(code, file) {
702
+ const svelte = file.endsWith(".svelte");
703
+ const options = svelte ? { tNames: ["t", "$t"] } : void 0;
704
+ const tagged = [];
705
+ const usedKeys = [];
706
+ for (const match of code.matchAll(SCRIPT_RE)) {
707
+ const content = match[2];
708
+ if (!content) continue;
709
+ merge(analyzeSegment(content, file, options), match.index + match[1].length, false);
661
710
  }
662
- remove(file) {
663
- this.files.delete(file);
711
+ const markup = blank(blank(blank(code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
712
+ const candidates = svelte ? CANDIDATE_SVELTE : CANDIDATE_VUE;
713
+ let consumedTo = 0;
714
+ for (const match of markup.matchAll(candidates)) {
715
+ const start = match.index;
716
+ if (start < consumedTo) continue;
717
+ const end = expressionExtent(markup, start);
718
+ if (end === void 0) continue;
719
+ merge(analyzeSegment(code.slice(start, end), file, options), start, true);
720
+ consumedTo = end;
664
721
  }
665
- messages() {
666
- const out = /* @__PURE__ */ new Map();
667
- for (const analysis of this.files.values()) for (const msg of analysis.tagged) {
668
- const existing = out.get(msg.key);
669
- if (existing) {
670
- if (existing.message !== msg.message) console.warn(`[verbaly] key collision "${msg.key}": ${JSON.stringify(existing.message)} vs ${JSON.stringify(msg.message)} — second dropped.`);
671
- continue;
672
- }
673
- out.set(msg.key, msg);
674
- }
675
- return out;
722
+ return {
723
+ tagged,
724
+ usedKeys
725
+ };
726
+ function merge(analysis, offset, markupSegment) {
727
+ if (!analysis) return;
728
+ for (const msg of analysis.tagged) tagged.push({
729
+ ...msg,
730
+ start: msg.start + offset,
731
+ end: msg.end + offset,
732
+ tagStart: msg.tagStart + offset,
733
+ tagEnd: msg.tagEnd + offset,
734
+ params: msg.params.map((p) => ({
735
+ ...p,
736
+ start: p.start + offset,
737
+ end: p.end + offset
738
+ })),
739
+ ...markupSegment && { singleQuote: true }
740
+ });
741
+ usedKeys.push(...analysis.usedKeys);
676
742
  }
677
- usedKeys() {
678
- const out = /* @__PURE__ */ new Map();
679
- for (const analysis of this.files.values()) for (const used of analysis.usedKeys) {
680
- const files = out.get(used.key) ?? [];
681
- if (!files.includes(used.file)) files.push(used.file);
682
- out.set(used.key, files);
743
+ }
744
+ function analyzeSegment(code, file, options) {
745
+ try {
746
+ return analyze(code, file, options);
747
+ } catch {
748
+ return;
749
+ }
750
+ }
751
+ function blank(source, re) {
752
+ return source.replace(re, (m) => " ".repeat(m.length));
753
+ }
754
+ function expressionExtent(code, start) {
755
+ let i = start;
756
+ if (code[i] === "$") i += 1;
757
+ i += 1;
758
+ if (code.startsWith(".id(", i)) {
759
+ const close = balancedEnd(code, i + 3);
760
+ if (close === void 0 || code[close] !== "`") return void 0;
761
+ return balancedEnd(code, close);
762
+ }
763
+ if (code[i] === "`" || code[i] === "(") return balancedEnd(code, i);
764
+ }
765
+ function balancedEnd(code, open) {
766
+ const stack = [];
767
+ let i = open;
768
+ while (i < code.length) {
769
+ const ch = code[i];
770
+ if (stack[stack.length - 1] === "`") {
771
+ if (ch === "\\") i += 1;
772
+ else if (ch === "`") stack.pop();
773
+ else if (ch === "$" && code[i + 1] === "{") {
774
+ stack.push("{");
775
+ i += 1;
776
+ }
777
+ } else if (ch === "'" || ch === "\"") i = stringEnd(code, i);
778
+ else if (ch === "`" || ch === "(" || ch === "{") stack.push(ch);
779
+ else if (ch === ")" || ch === "}") {
780
+ if (stack[stack.length - 1] !== (ch === ")" ? "(" : "{")) return void 0;
781
+ stack.pop();
683
782
  }
684
- return out;
783
+ i += 1;
784
+ if (stack.length === 0) return i;
685
785
  }
686
- };
786
+ }
787
+ function stringEnd(code, start) {
788
+ const quote = code[start];
789
+ let i = start + 1;
790
+ while (i < code.length) if (code[i] === "\\") i += 2;
791
+ else if (code[i] === quote) return i;
792
+ else i += 1;
793
+ return code.length;
794
+ }
687
795
  //#endregion
688
796
  //#region src/extract.ts
689
797
  async function extractProject(cfg) {
@@ -693,7 +801,7 @@ async function extractProject(cfg) {
693
801
  absolute: true
694
802
  });
695
803
  const registry = new MessageRegistry();
696
- for (const file of files) registry.update(file, analyze(readFileSync(file, "utf8"), file));
804
+ for (const file of files) registry.update(file, analyzeFile(readFileSync(file, "utf8"), file));
697
805
  return registry;
698
806
  }
699
807
  function syncCatalogs(cfg, catalogs, registry) {