translate-kit 0.4.0 → 0.4.1

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
@@ -9,6 +9,108 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/scanner/filters.ts
13
+ function isLikelyKebabIdentifier(text2) {
14
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(text2)) return false;
15
+ return text2.split("-").length >= 3;
16
+ }
17
+ function isLikelyConstantIdentifier(text2) {
18
+ return /^(?=.*[_\d])[A-Z0-9_]+$/.test(text2);
19
+ }
20
+ function isContentProperty(propName) {
21
+ return CONTENT_PROPERTY_NAMES.includes(propName);
22
+ }
23
+ function isTranslatableProp(propName, customProps) {
24
+ if (NEVER_TRANSLATE_PROPS.includes(propName)) return false;
25
+ const allowed = customProps ?? DEFAULT_TRANSLATABLE_PROPS;
26
+ return allowed.includes(propName);
27
+ }
28
+ function isIgnoredTag(tagName) {
29
+ return IGNORE_TAGS.includes(tagName.toLowerCase());
30
+ }
31
+ function shouldIgnore(text2) {
32
+ const trimmed = text2.trim();
33
+ if (trimmed.length === 0) return true;
34
+ if (isLikelyKebabIdentifier(trimmed)) return true;
35
+ if (isLikelyConstantIdentifier(trimmed)) return true;
36
+ return IGNORE_PATTERNS.some((pattern) => pattern.test(trimmed));
37
+ }
38
+ var DEFAULT_TRANSLATABLE_PROPS, NEVER_TRANSLATE_PROPS, IGNORE_TAGS, IGNORE_PATTERNS, CONTENT_PROPERTY_NAMES;
39
+ var init_filters = __esm({
40
+ "src/scanner/filters.ts"() {
41
+ "use strict";
42
+ DEFAULT_TRANSLATABLE_PROPS = [
43
+ "placeholder",
44
+ "title",
45
+ "alt",
46
+ "aria-label",
47
+ "aria-description",
48
+ "aria-placeholder",
49
+ "label"
50
+ ];
51
+ NEVER_TRANSLATE_PROPS = [
52
+ "className",
53
+ "class",
54
+ "id",
55
+ "key",
56
+ "ref",
57
+ "href",
58
+ "src",
59
+ "type",
60
+ "name",
61
+ "value",
62
+ "htmlFor",
63
+ "for",
64
+ "role",
65
+ "style",
66
+ "data-testid",
67
+ "data-cy",
68
+ "onClick",
69
+ "onChange",
70
+ "onSubmit",
71
+ "onFocus",
72
+ "onBlur"
73
+ ];
74
+ IGNORE_TAGS = [
75
+ "script",
76
+ "style",
77
+ "code",
78
+ "pre",
79
+ "svg",
80
+ "path",
81
+ "circle",
82
+ "rect",
83
+ "line",
84
+ "polyline",
85
+ "polygon"
86
+ ];
87
+ IGNORE_PATTERNS = [
88
+ /^\s*$/,
89
+ // Whitespace only
90
+ /^https?:\/\//,
91
+ // URLs
92
+ /^[\d.,%$€£¥]+$/,
93
+ // Numbers, currency
94
+ /^[^\p{L}]*$/u
95
+ // No letters at all (Unicode-aware)
96
+ ];
97
+ CONTENT_PROPERTY_NAMES = [
98
+ "title",
99
+ "description",
100
+ "label",
101
+ "text",
102
+ "content",
103
+ "heading",
104
+ "subtitle",
105
+ "caption",
106
+ "summary",
107
+ "message",
108
+ "placeholder",
109
+ "alt"
110
+ ];
111
+ }
112
+ });
113
+
12
114
  // src/config.ts
13
115
  import { loadConfig } from "c12";
14
116
  import { z } from "zod";
@@ -33,6 +135,7 @@ var configSchema;
33
135
  var init_config = __esm({
34
136
  "src/config.ts"() {
35
137
  "use strict";
138
+ init_filters();
36
139
  configSchema = z.object({
37
140
  model: z.custom(
38
141
  (val) => val != null && typeof val === "object",
@@ -56,7 +159,7 @@ var init_config = __esm({
56
159
  scan: z.object({
57
160
  include: z.array(z.string()),
58
161
  exclude: z.array(z.string()).optional(),
59
- translatableProps: z.array(z.string()).default(["placeholder", "title", "alt", "aria-label"]),
162
+ translatableProps: z.array(z.string()).default([...DEFAULT_TRANSLATABLE_PROPS]),
60
163
  i18nImport: z.string().default("next-intl")
61
164
  }).optional(),
62
165
  inline: z.object({
@@ -188,108 +291,6 @@ var init_parser = __esm({
188
291
  }
189
292
  });
190
293
 
191
- // src/scanner/filters.ts
192
- function isLikelyKebabIdentifier(text2) {
193
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(text2)) return false;
194
- return text2.split("-").length >= 3;
195
- }
196
- function isLikelyConstantIdentifier(text2) {
197
- return /^(?=.*[_\d])[A-Z0-9_]+$/.test(text2);
198
- }
199
- function isContentProperty(propName) {
200
- return CONTENT_PROPERTY_NAMES.includes(propName);
201
- }
202
- function isTranslatableProp(propName, customProps) {
203
- if (NEVER_TRANSLATE_PROPS.includes(propName)) return false;
204
- const allowed = customProps ?? DEFAULT_TRANSLATABLE_PROPS;
205
- return allowed.includes(propName);
206
- }
207
- function isIgnoredTag(tagName) {
208
- return IGNORE_TAGS.includes(tagName.toLowerCase());
209
- }
210
- function shouldIgnore(text2) {
211
- const trimmed = text2.trim();
212
- if (trimmed.length === 0) return true;
213
- if (isLikelyKebabIdentifier(trimmed)) return true;
214
- if (isLikelyConstantIdentifier(trimmed)) return true;
215
- return IGNORE_PATTERNS.some((pattern) => pattern.test(trimmed));
216
- }
217
- var DEFAULT_TRANSLATABLE_PROPS, NEVER_TRANSLATE_PROPS, IGNORE_TAGS, IGNORE_PATTERNS, CONTENT_PROPERTY_NAMES;
218
- var init_filters = __esm({
219
- "src/scanner/filters.ts"() {
220
- "use strict";
221
- DEFAULT_TRANSLATABLE_PROPS = [
222
- "placeholder",
223
- "title",
224
- "alt",
225
- "aria-label",
226
- "aria-description",
227
- "aria-placeholder",
228
- "label"
229
- ];
230
- NEVER_TRANSLATE_PROPS = [
231
- "className",
232
- "class",
233
- "id",
234
- "key",
235
- "ref",
236
- "href",
237
- "src",
238
- "type",
239
- "name",
240
- "value",
241
- "htmlFor",
242
- "for",
243
- "role",
244
- "style",
245
- "data-testid",
246
- "data-cy",
247
- "onClick",
248
- "onChange",
249
- "onSubmit",
250
- "onFocus",
251
- "onBlur"
252
- ];
253
- IGNORE_TAGS = [
254
- "script",
255
- "style",
256
- "code",
257
- "pre",
258
- "svg",
259
- "path",
260
- "circle",
261
- "rect",
262
- "line",
263
- "polyline",
264
- "polygon"
265
- ];
266
- IGNORE_PATTERNS = [
267
- /^\s*$/,
268
- // Whitespace only
269
- /^https?:\/\//,
270
- // URLs
271
- /^[\d.,%$€£¥]+$/,
272
- // Numbers, currency
273
- /^[^\p{L}]*$/u
274
- // No letters at all (Unicode-aware)
275
- ];
276
- CONTENT_PROPERTY_NAMES = [
277
- "title",
278
- "description",
279
- "label",
280
- "text",
281
- "content",
282
- "heading",
283
- "subtitle",
284
- "caption",
285
- "summary",
286
- "message",
287
- "placeholder",
288
- "alt"
289
- ];
290
- }
291
- });
292
-
293
294
  // src/utils/ast-helpers.ts
294
295
  function resolveDefault(mod) {
295
296
  if (typeof mod === "function") return mod;
@@ -374,6 +375,31 @@ function getComponentName(path) {
374
375
  }
375
376
  return void 0;
376
377
  }
378
+ function isPascalCase(name) {
379
+ if (name === "__default__") return true;
380
+ return /^[A-Z]/.test(name);
381
+ }
382
+ function getTopLevelConstName(path) {
383
+ let current = path.parentPath;
384
+ while (current) {
385
+ if (current.isFunctionDeclaration() || current.isFunctionExpression() || current.isArrowFunctionExpression() || current.isClassDeclaration() || current.isClassExpression() || current.isClassMethod() || current.isClassPrivateMethod()) {
386
+ return void 0;
387
+ }
388
+ if (current.isVariableDeclarator()) {
389
+ const declParent = current.parentPath;
390
+ if (declParent?.isVariableDeclaration() && declParent.node.kind === "const") {
391
+ const grandParent = declParent.parentPath;
392
+ if (grandParent?.isProgram() || grandParent?.isExportNamedDeclaration()) {
393
+ const id = current.node.id;
394
+ if (id.type === "Identifier") return id.name;
395
+ }
396
+ }
397
+ return void 0;
398
+ }
399
+ current = current.parentPath;
400
+ }
401
+ return void 0;
402
+ }
377
403
  function getParentTagName(path) {
378
404
  let current = path.parentPath;
379
405
  while (current) {
@@ -617,11 +643,59 @@ function extractStrings(ast, filePath, translatableProps) {
617
643
  });
618
644
  },
619
645
  ObjectProperty(path) {
620
- if (!isInsideFunction(path)) return;
646
+ const inFunction = isInsideFunction(path);
647
+ if (!inFunction) {
648
+ const constName = getTopLevelConstName(path);
649
+ if (!constName) return;
650
+ const keyNode2 = path.node.key;
651
+ if (keyNode2.type !== "Identifier" && keyNode2.type !== "StringLiteral")
652
+ return;
653
+ const propName2 = keyNode2.type === "Identifier" ? keyNode2.name : keyNode2.value;
654
+ if (!isContentProperty(propName2)) return;
655
+ const valueNode2 = path.node.value;
656
+ if (valueNode2.type === "ConditionalExpression") {
657
+ const texts = collectConditionalTexts(valueNode2);
658
+ for (const t3 of texts) {
659
+ results.push({
660
+ text: t3,
661
+ type: "module-object-property",
662
+ file: filePath,
663
+ line: valueNode2.loc?.start.line ?? 0,
664
+ column: valueNode2.loc?.start.column ?? 0,
665
+ propName: propName2,
666
+ parentConstName: constName
667
+ });
668
+ }
669
+ return;
670
+ }
671
+ let text3;
672
+ if (valueNode2.type === "StringLiteral") {
673
+ text3 = valueNode2.value.trim();
674
+ } else if (valueNode2.type === "TemplateLiteral") {
675
+ const info = buildTemplateLiteralText(
676
+ valueNode2.quasis,
677
+ valueNode2.expressions
678
+ );
679
+ if (info) text3 = info.text;
680
+ }
681
+ if (!text3 || shouldIgnore(text3)) return;
682
+ results.push({
683
+ text: text3,
684
+ type: "module-object-property",
685
+ file: filePath,
686
+ line: valueNode2.loc?.start.line ?? 0,
687
+ column: valueNode2.loc?.start.column ?? 0,
688
+ propName: propName2,
689
+ parentConstName: constName
690
+ });
691
+ return;
692
+ }
621
693
  const ownerFn = getNearestFunctionPath(path);
622
694
  if (!ownerFn) return;
623
695
  if (!functionContainsJSX(ownerFn)) return;
624
- const componentName = getComponentName(ownerFn);
696
+ const componentName = getComponentName(
697
+ ownerFn
698
+ );
625
699
  if (!componentName) return;
626
700
  const keyNode = path.node.key;
627
701
  if (keyNode.type !== "Identifier" && keyNode.type !== "StringLiteral")
@@ -740,14 +814,14 @@ var init_extractor = __esm({
740
814
 
741
815
  // src/scanner/context-enricher.ts
742
816
  function deriveRoutePath(filePath) {
743
- const appMatch = filePath.match(/src\/app\/(.+?)\/page\.[jt]sx?$/);
817
+ const appMatch = filePath.match(/(?:src\/)?app\/(.+?)\/page\.[jt]sx?$/);
744
818
  if (appMatch) return appMatch[1].replace(/\//g, ".");
745
819
  const pagesMatch = filePath.match(/(?:src\/)?pages\/(.+?)\.[jt]sx?$/);
746
820
  if (pagesMatch) {
747
821
  const route = pagesMatch[1].replace(/\/index$/, "").replace(/\//g, ".");
748
822
  return route || void 0;
749
823
  }
750
- const compMatch = filePath.match(/src\/components\/(.+?)\//);
824
+ const compMatch = filePath.match(/(?:src\/)?components\/(.+?)\//);
751
825
  if (compMatch) return compMatch[1];
752
826
  return void 0;
753
827
  }
@@ -981,6 +1055,7 @@ function buildPrompt(strings, existingMap) {
981
1055
  if (str.componentName) parts.push(`component: ${str.componentName}`);
982
1056
  if (str.parentTag) parts.push(`tag: ${str.parentTag}`);
983
1057
  if (str.propName) parts.push(`prop: ${str.propName}`);
1058
+ if (str.parentConstName) parts.push(`const: ${str.parentConstName}`);
984
1059
  if (str.sectionHeading) parts.push(`section: "${str.sectionHeading}"`);
985
1060
  if (str.siblingTexts?.length) {
986
1061
  parts.push(
@@ -1368,7 +1443,11 @@ function resolveNamedImportLocal(ast, importSource, importedName) {
1368
1443
  return null;
1369
1444
  }
1370
1445
  function ensureNamedImportLocal(ast, importSource, importedName) {
1371
- const existingLocal = resolveNamedImportLocal(ast, importSource, importedName);
1446
+ const existingLocal = resolveNamedImportLocal(
1447
+ ast,
1448
+ importSource,
1449
+ importedName
1450
+ );
1372
1451
  if (existingLocal) return existingLocal;
1373
1452
  for (const node of ast.program.body) {
1374
1453
  if (node.type !== "ImportDeclaration" || node.source.value !== importSource) {
@@ -1380,12 +1459,7 @@ function ensureNamedImportLocal(ast, importSource, importedName) {
1380
1459
  return importedName;
1381
1460
  }
1382
1461
  const importDecl = t2.importDeclaration(
1383
- [
1384
- t2.importSpecifier(
1385
- t2.identifier(importedName),
1386
- t2.identifier(importedName)
1387
- )
1388
- ],
1462
+ [t2.importSpecifier(t2.identifier(importedName), t2.identifier(importedName))],
1389
1463
  t2.stringLiteral(importSource)
1390
1464
  );
1391
1465
  const lastImportIndex = findLastImportIndex(ast);
@@ -1562,6 +1636,142 @@ function transformConditionalBranchInline(node, textToKey) {
1562
1636
  }
1563
1637
  return { node, count: 0 };
1564
1638
  }
1639
+ function preDiscoverFactoryRefComponents(ast, importedNames) {
1640
+ if (importedNames.length === 0) return /* @__PURE__ */ new Set();
1641
+ const nameSet = new Set(importedNames);
1642
+ const comps = /* @__PURE__ */ new Set();
1643
+ traverse2(ast, {
1644
+ Identifier(path) {
1645
+ if (!nameSet.has(path.node.name)) return;
1646
+ const parent = path.parent;
1647
+ if (parent.type === "ImportSpecifier" || parent.type === "ImportDefaultSpecifier" || parent.type === "ImportNamespaceSpecifier" || parent.type === "ExportSpecifier") return;
1648
+ if (parent.type === "VariableDeclarator" && parent.id === path.node) return;
1649
+ if (parent.type === "CallExpression" && parent.callee === path.node) return;
1650
+ const binding = path.scope?.getBinding(path.node.name);
1651
+ if (binding) {
1652
+ const bPath = binding.path;
1653
+ const isImportBinding = bPath.isImportSpecifier() || bPath.isImportDefaultSpecifier();
1654
+ const isTopLevelConst = bPath.isVariableDeclarator() && bPath.parentPath?.isVariableDeclaration() && (bPath.parentPath.parentPath?.isProgram() || bPath.parentPath.parentPath?.isExportNamedDeclaration());
1655
+ if (!isImportBinding && !isTopLevelConst) return;
1656
+ }
1657
+ if (!isInsideFunction(path)) return;
1658
+ const comp = getComponentName(path);
1659
+ if (comp && isPascalCase(comp)) comps.add(comp);
1660
+ }
1661
+ });
1662
+ return comps;
1663
+ }
1664
+ function wrapModuleFactoryDeclarations(ast, constNames) {
1665
+ if (constNames.length === 0) return false;
1666
+ const nameSet = new Set(constNames);
1667
+ let wrapped = false;
1668
+ for (const node of ast.program.body) {
1669
+ let decl;
1670
+ if (node.type === "VariableDeclaration") {
1671
+ decl = node;
1672
+ } else if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
1673
+ decl = node.declaration;
1674
+ }
1675
+ if (!decl || decl.kind !== "const") continue;
1676
+ for (const declarator of decl.declarations) {
1677
+ if (declarator.id.type !== "Identifier") continue;
1678
+ if (!nameSet.has(declarator.id.name)) continue;
1679
+ if (!declarator.init) continue;
1680
+ const typeAnnotation = declarator.id.typeAnnotation;
1681
+ if (typeAnnotation) {
1682
+ declarator.id.typeAnnotation = null;
1683
+ }
1684
+ if (declarator.init.type === "ArrowFunctionExpression" && declarator.init.params.length === 1 && declarator.init.params[0].type === "Identifier" && declarator.init.params[0].name === "t") {
1685
+ continue;
1686
+ }
1687
+ const originalInit = declarator.init;
1688
+ const tParam = t2.identifier("t");
1689
+ tParam.typeAnnotation = t2.tsTypeAnnotation(t2.tsAnyKeyword());
1690
+ const arrowFn = t2.arrowFunctionExpression(
1691
+ [tParam],
1692
+ t2.parenthesizedExpression(originalInit)
1693
+ );
1694
+ if (typeAnnotation) {
1695
+ arrowFn.returnType = typeAnnotation;
1696
+ }
1697
+ declarator.init = arrowFn;
1698
+ wrapped = true;
1699
+ }
1700
+ }
1701
+ return wrapped;
1702
+ }
1703
+ function isInsideFunctionParam(path) {
1704
+ let current = path;
1705
+ while (current.parentPath) {
1706
+ if (current.parentPath.isFunction()) {
1707
+ const fn = current.parentPath.node;
1708
+ return fn.params.includes(current.node);
1709
+ }
1710
+ current = current.parentPath;
1711
+ }
1712
+ return false;
1713
+ }
1714
+ function rewriteModuleFactoryReferences(ast, importedNames, componentTranslatorIds) {
1715
+ if (importedNames.length === 0) return { componentsNeedingT: /* @__PURE__ */ new Set(), rewrote: false };
1716
+ const nameSet = new Set(importedNames);
1717
+ const componentsNeedingT = /* @__PURE__ */ new Set();
1718
+ let rewrote = false;
1719
+ const rewrites = [];
1720
+ traverse2(ast, {
1721
+ Identifier(path) {
1722
+ if (!nameSet.has(path.node.name)) return;
1723
+ const parent = path.parent;
1724
+ if (parent.type === "ImportSpecifier" || parent.type === "ImportDefaultSpecifier" || parent.type === "ImportNamespaceSpecifier" || parent.type === "ExportSpecifier") {
1725
+ return;
1726
+ }
1727
+ if (parent.type.startsWith("TS") && parent.type !== "TSNonNullExpression" && parent.type !== "TSAsExpression" && parent.type !== "TSSatisfiesExpression") {
1728
+ return;
1729
+ }
1730
+ if (parent.type === "CallExpression" && parent.callee === path.node) {
1731
+ return;
1732
+ }
1733
+ if (parent.type === "ObjectProperty" && parent.key === path.node) {
1734
+ if (parent.shorthand) {
1735
+ if (path.key === "key") return;
1736
+ } else {
1737
+ return;
1738
+ }
1739
+ }
1740
+ const binding = path.scope?.getBinding(path.node.name);
1741
+ if (binding) {
1742
+ const bPath = binding.path;
1743
+ const isImportBinding = bPath.isImportSpecifier() || bPath.isImportDefaultSpecifier();
1744
+ const isTopLevelConst = bPath.isVariableDeclarator() && bPath.parentPath?.isVariableDeclaration() && (bPath.parentPath.parentPath?.isProgram() || bPath.parentPath.parentPath?.isExportNamedDeclaration());
1745
+ if (!isImportBinding && !isTopLevelConst) return;
1746
+ }
1747
+ if (isInsideFunctionParam(path)) return;
1748
+ if (!isInsideFunction(path)) return;
1749
+ const compName = getComponentName(path);
1750
+ if (!compName || !isPascalCase(compName)) return;
1751
+ const translatorId = componentTranslatorIds.get(compName) ?? "t";
1752
+ if (parent.type === "ObjectProperty" && parent.shorthand) {
1753
+ rewrites.push({ path, translatorId, compName, shorthandProp: parent });
1754
+ } else {
1755
+ rewrites.push({ path, translatorId, compName });
1756
+ }
1757
+ }
1758
+ });
1759
+ for (const { path, translatorId, compName, shorthandProp } of rewrites) {
1760
+ const callExpr = t2.callExpression(
1761
+ t2.identifier(path.node.name),
1762
+ [t2.identifier(translatorId)]
1763
+ );
1764
+ if (shorthandProp) {
1765
+ shorthandProp.shorthand = false;
1766
+ shorthandProp.value = callExpr;
1767
+ } else {
1768
+ path.replaceWith(callExpr);
1769
+ }
1770
+ componentsNeedingT.add(compName);
1771
+ rewrote = true;
1772
+ }
1773
+ return { componentsNeedingT, rewrote };
1774
+ }
1565
1775
  function transform(ast, textToKey, options = {}) {
1566
1776
  if (options.mode === "inline") {
1567
1777
  return transformInline(ast, textToKey, options);
@@ -1592,6 +1802,8 @@ function transform(ast, textToKey, options = {}) {
1592
1802
  if (isInsideClass(path)) return;
1593
1803
  const text2 = path.node.value.trim();
1594
1804
  if (!text2 || !(text2 in textToKey)) return;
1805
+ const compName = getComponentName(path);
1806
+ if (!compName || !isPascalCase(compName)) return;
1595
1807
  const parent = path.parentPath;
1596
1808
  if (!parent?.isJSXElement()) return;
1597
1809
  const key = textToKey[text2];
@@ -1623,6 +1835,8 @@ function transform(ast, textToKey, options = {}) {
1623
1835
  if (isInsideClass(path)) return;
1624
1836
  const expr = path.node.expression;
1625
1837
  if (path.parent.type === "JSXAttribute") return;
1838
+ const compName = getComponentName(path);
1839
+ if (!compName || !isPascalCase(compName)) return;
1626
1840
  if (expr.type === "ConditionalExpression") {
1627
1841
  const result = transformConditionalBranch(expr, textToKey);
1628
1842
  if (result.count > 0) {
@@ -1654,6 +1868,11 @@ function transform(ast, textToKey, options = {}) {
1654
1868
  if (isInsideClass(path)) return;
1655
1869
  const value = path.node.value;
1656
1870
  if (!value) return;
1871
+ const attrName = path.node.name;
1872
+ const propName = attrName.type === "JSXIdentifier" ? attrName.name : attrName.name.name;
1873
+ if (!isTranslatableProp(propName, options.translatableProps)) return;
1874
+ const compName = getComponentName(path);
1875
+ if (!compName || !isPascalCase(compName)) return;
1657
1876
  if (value.type === "JSXExpressionContainer" && value.expression.type === "ConditionalExpression") {
1658
1877
  const result = transformConditionalBranch(value.expression, textToKey);
1659
1878
  if (result.count > 0) {
@@ -1708,8 +1927,66 @@ function transform(ast, textToKey, options = {}) {
1708
1927
  },
1709
1928
  ObjectProperty(path) {
1710
1929
  if (isInsideClass(path)) return;
1711
- if (!isInsideFunction(path)) return;
1712
- const ownerFn = getNearestFunctionPath2(path);
1930
+ const factoryConstNames = options.moduleFactoryConstNames ?? [];
1931
+ const inFunction = isInsideFunction(path);
1932
+ if (!inFunction) {
1933
+ const constName = getTopLevelConstName(path);
1934
+ if (!constName || !factoryConstNames.includes(constName)) return;
1935
+ const keyNode2 = path.node.key;
1936
+ if (keyNode2.type !== "Identifier" && keyNode2.type !== "StringLiteral")
1937
+ return;
1938
+ const propName2 = keyNode2.type === "Identifier" ? keyNode2.name : keyNode2.value;
1939
+ if (!isContentProperty(propName2)) return;
1940
+ const valueNode2 = path.node.value;
1941
+ if (valueNode2.type === "ConditionalExpression") {
1942
+ const result = transformConditionalBranch(valueNode2, textToKey);
1943
+ if (result.count > 0) {
1944
+ path.node.value = result.node;
1945
+ stringsWrapped += result.count;
1946
+ collectConditionalKeys(valueNode2, textToKey).forEach(
1947
+ (k) => allUsedKeys.push(k)
1948
+ );
1949
+ }
1950
+ return;
1951
+ }
1952
+ let text3;
1953
+ let templateInfo2;
1954
+ if (valueNode2.type === "StringLiteral") {
1955
+ text3 = valueNode2.value;
1956
+ } else if (valueNode2.type === "TemplateLiteral") {
1957
+ const info = buildTemplateLiteralText(
1958
+ valueNode2.quasis,
1959
+ valueNode2.expressions
1960
+ );
1961
+ if (info) {
1962
+ text3 = info.text;
1963
+ templateInfo2 = {
1964
+ placeholders: info.placeholders,
1965
+ expressions: valueNode2.expressions
1966
+ };
1967
+ }
1968
+ }
1969
+ if (!text3 || !(text3 in textToKey)) return;
1970
+ const key2 = textToKey[text3];
1971
+ const args2 = [t2.stringLiteral(key2)];
1972
+ if (templateInfo2 && templateInfo2.placeholders.length > 0) {
1973
+ args2.push(
1974
+ buildValuesObject(
1975
+ templateInfo2.expressions,
1976
+ templateInfo2.placeholders
1977
+ )
1978
+ );
1979
+ }
1980
+ path.node.value = markGeneratedCall(
1981
+ t2.callExpression(t2.identifier("t"), args2)
1982
+ );
1983
+ stringsWrapped++;
1984
+ allUsedKeys.push(key2);
1985
+ return;
1986
+ }
1987
+ const ownerFn = getNearestFunctionPath2(
1988
+ path
1989
+ );
1713
1990
  if (!ownerFn) return;
1714
1991
  if (!functionContainsJSX2(ownerFn.node)) return;
1715
1992
  const compName = getComponentName(ownerFn);
@@ -1766,7 +2043,8 @@ function transform(ast, textToKey, options = {}) {
1766
2043
  trackKey(path, key);
1767
2044
  }
1768
2045
  });
1769
- if (stringsWrapped === 0) {
2046
+ const hasModuleFactoryWork = (options.moduleFactoryConstNames?.length ?? 0) > 0 || (options.moduleFactoryImportedNames?.length ?? 0) > 0;
2047
+ if (stringsWrapped === 0 && !hasModuleFactoryWork) {
1770
2048
  return {
1771
2049
  code: generate(ast).code,
1772
2050
  stringsWrapped: 0,
@@ -1774,18 +2052,28 @@ function transform(ast, textToKey, options = {}) {
1774
2052
  usedKeys: []
1775
2053
  };
1776
2054
  }
2055
+ const moduleFactoryImportedNames = options.moduleFactoryImportedNames ?? [];
2056
+ const factoryRefComponents = preDiscoverFactoryRefComponents(ast, moduleFactoryImportedNames);
2057
+ for (const comp of factoryRefComponents) {
2058
+ componentsNeedingT.add(comp);
2059
+ }
1777
2060
  const componentNamespaces = /* @__PURE__ */ new Map();
1778
2061
  for (const [comp, keys] of componentKeys) {
1779
- componentNamespaces.set(comp, detectNamespace([...keys]));
2062
+ if (factoryRefComponents.has(comp)) {
2063
+ componentNamespaces.set(comp, null);
2064
+ } else {
2065
+ componentNamespaces.set(comp, detectNamespace([...keys]));
2066
+ }
2067
+ }
2068
+ for (const comp of factoryRefComponents) {
2069
+ if (!componentNamespaces.has(comp)) {
2070
+ componentNamespaces.set(comp, null);
2071
+ }
1780
2072
  }
1781
2073
  const namespacedComponents = /* @__PURE__ */ new Set();
1782
2074
  const componentTranslatorIds = /* @__PURE__ */ new Map();
1783
2075
  if (isClient) {
1784
- const useTranslationsLocal = ensureNamedImportLocal(
1785
- ast,
1786
- importSource,
1787
- "useTranslations"
1788
- );
2076
+ const useTranslationsLocal = componentsNeedingT.size > 0 ? ensureNamedImportLocal(ast, importSource, "useTranslations") : "useTranslations";
1789
2077
  traverse2(ast, {
1790
2078
  FunctionDeclaration(path) {
1791
2079
  const name = getComponentName(path);
@@ -1881,11 +2169,7 @@ function transform(ast, textToKey, options = {}) {
1881
2169
  });
1882
2170
  } else {
1883
2171
  const serverSource = `${importSource}/server`;
1884
- const getTranslationsLocal = ensureNamedImportLocal(
1885
- ast,
1886
- serverSource,
1887
- "getTranslations"
1888
- );
2172
+ const getTranslationsLocal = componentsNeedingT.size > 0 ? ensureNamedImportLocal(ast, serverSource, "getTranslations") : "getTranslations";
1889
2173
  traverse2(ast, {
1890
2174
  FunctionDeclaration(path) {
1891
2175
  const name = getComponentName(path);
@@ -1991,20 +2275,31 @@ function transform(ast, textToKey, options = {}) {
1991
2275
  noScope: true
1992
2276
  });
1993
2277
  }
2278
+ const moduleFactoryConstNames = options.moduleFactoryConstNames ?? [];
2279
+ const didWrapFactory = wrapModuleFactoryDeclarations(ast, moduleFactoryConstNames);
1994
2280
  const effectiveNamespaces = /* @__PURE__ */ new Map();
1995
2281
  for (const [comp, ns] of componentNamespaces) {
1996
2282
  effectiveNamespaces.set(comp, namespacedComponents.has(comp) ? ns : null);
1997
2283
  }
1998
2284
  rewriteKeysForNamespaces(ast, effectiveNamespaces);
1999
2285
  rewriteGeneratedCallsForTranslator(ast, componentTranslatorIds);
2286
+ const { componentsNeedingT: factoryComponentsNeedingT, rewrote: didRewriteRefs } = rewriteModuleFactoryReferences(
2287
+ ast,
2288
+ moduleFactoryImportedNames,
2289
+ componentTranslatorIds
2290
+ );
2291
+ for (const comp of factoryComponentsNeedingT) {
2292
+ componentsNeedingT.add(comp);
2293
+ }
2000
2294
  if (needsClientDirective && componentsNeedingT.size > 0) {
2001
2295
  addUseClientDirective(ast);
2002
2296
  }
2297
+ const didModify = stringsWrapped > 0 || didWrapFactory || didRewriteRefs;
2003
2298
  const output = generate(ast, { retainLines: false });
2004
2299
  return {
2005
2300
  code: output.code,
2006
2301
  stringsWrapped,
2007
- modified: true,
2302
+ modified: didModify,
2008
2303
  usedKeys: allUsedKeys
2009
2304
  };
2010
2305
  }
@@ -2044,7 +2339,10 @@ function injectTIntoBlock(block, useTranslationsLocal, namespace, componentName,
2044
2339
  if (d.id.type !== "Identifier") continue;
2045
2340
  if (isTranslationFactoryCall(d.init, useTranslationsLocal)) {
2046
2341
  return {
2047
- namespaced: updateCallNamespace(d.init, namespace),
2342
+ namespaced: updateCallNamespace(
2343
+ d.init,
2344
+ namespace
2345
+ ),
2048
2346
  translatorId: d.id.name
2049
2347
  };
2050
2348
  }
@@ -2088,7 +2386,10 @@ function injectAsyncTIntoBlock(block, getTranslationsLocal, namespace, component
2088
2386
  }
2089
2387
  if (isGetTranslationsCall(d.init, getTranslationsLocal)) {
2090
2388
  return {
2091
- namespaced: updateCallNamespace(d.init, namespace),
2389
+ namespaced: updateCallNamespace(
2390
+ d.init,
2391
+ namespace
2392
+ ),
2092
2393
  translatorId: d.id.name
2093
2394
  };
2094
2395
  }
@@ -2291,9 +2592,7 @@ function addUseClientDirective(ast) {
2291
2592
  if (!ast.program.directives) {
2292
2593
  ast.program.directives = [];
2293
2594
  }
2294
- ast.program.directives.unshift(
2295
- t2.directive(t2.directiveLiteral("use client"))
2296
- );
2595
+ ast.program.directives.unshift(t2.directive(t2.directiveLiteral("use client")));
2297
2596
  }
2298
2597
  function transformInline(ast, textToKey, options) {
2299
2598
  const componentPath = options.componentPath ?? "@/components/t";
@@ -2327,6 +2626,8 @@ function transformInline(ast, textToKey, options) {
2327
2626
  if (isInsideClass(path)) return;
2328
2627
  const text2 = path.node.value.trim();
2329
2628
  if (!text2 || !(text2 in textToKey)) return;
2629
+ const compName = getComponentName(path);
2630
+ if (!compName || !isPascalCase(compName)) return;
2330
2631
  const parent = path.parentPath;
2331
2632
  if (!parent?.isJSXElement()) return;
2332
2633
  const parentOpening = parent.node.openingElement;
@@ -2365,13 +2666,14 @@ function transformInline(ast, textToKey, options) {
2365
2666
  if (isInsideClass(path)) return;
2366
2667
  const expr = path.node.expression;
2367
2668
  if (path.parent.type === "JSXAttribute") return;
2669
+ const compName = getComponentName(path);
2670
+ if (!compName || !isPascalCase(compName)) return;
2368
2671
  if (expr.type === "ConditionalExpression") {
2369
2672
  const result = transformConditionalBranchInline(expr, textToKey);
2370
2673
  if (result.count > 0) {
2371
2674
  path.node.expression = result.node;
2372
2675
  stringsWrapped += result.count;
2373
- const compName2 = getComponentName(path);
2374
- if (compName2) componentsNeedingT.add(compName2);
2676
+ componentsNeedingT.add(compName);
2375
2677
  }
2376
2678
  return;
2377
2679
  }
@@ -2390,13 +2692,17 @@ function transformInline(ast, textToKey, options) {
2390
2692
  t2.callExpression(t2.identifier("t"), args)
2391
2693
  );
2392
2694
  stringsWrapped++;
2393
- const compName = getComponentName(path);
2394
- if (compName) componentsNeedingT.add(compName);
2695
+ componentsNeedingT.add(compName);
2395
2696
  },
2396
2697
  JSXAttribute(path) {
2397
2698
  if (isInsideClass(path)) return;
2398
2699
  const value = path.node.value;
2399
2700
  if (!value) return;
2701
+ const attrName = path.node.name;
2702
+ const propName = attrName.type === "JSXIdentifier" ? attrName.name : attrName.name.name;
2703
+ if (!isTranslatableProp(propName, options.translatableProps)) return;
2704
+ const compName = getComponentName(path);
2705
+ if (!compName || !isPascalCase(compName)) return;
2400
2706
  if (value.type === "JSXExpressionContainer" && value.expression.type === "ConditionalExpression") {
2401
2707
  const result = transformConditionalBranchInline(
2402
2708
  value.expression,
@@ -2405,8 +2711,7 @@ function transformInline(ast, textToKey, options) {
2405
2711
  if (result.count > 0) {
2406
2712
  path.node.value = t2.jsxExpressionContainer(result.node);
2407
2713
  stringsWrapped += result.count;
2408
- const compName2 = getComponentName(path);
2409
- if (compName2) componentsNeedingT.add(compName2);
2714
+ componentsNeedingT.add(compName);
2410
2715
  }
2411
2716
  return;
2412
2717
  }
@@ -2452,13 +2757,69 @@ function transformInline(ast, textToKey, options) {
2452
2757
  markGeneratedCall(t2.callExpression(t2.identifier("t"), args))
2453
2758
  );
2454
2759
  stringsWrapped++;
2455
- const compName = getComponentName(path);
2456
- if (compName) componentsNeedingT.add(compName);
2760
+ componentsNeedingT.add(compName);
2457
2761
  },
2458
2762
  ObjectProperty(path) {
2459
2763
  if (isInsideClass(path)) return;
2460
- if (!isInsideFunction(path)) return;
2461
- const ownerFn = getNearestFunctionPath2(path);
2764
+ const factoryConstNames = options.moduleFactoryConstNames ?? [];
2765
+ const inFunction = isInsideFunction(path);
2766
+ if (!inFunction) {
2767
+ const constName = getTopLevelConstName(path);
2768
+ if (!constName || !factoryConstNames.includes(constName)) return;
2769
+ const keyNode2 = path.node.key;
2770
+ if (keyNode2.type !== "Identifier" && keyNode2.type !== "StringLiteral")
2771
+ return;
2772
+ const propName2 = keyNode2.type === "Identifier" ? keyNode2.name : keyNode2.value;
2773
+ if (!isContentProperty(propName2)) return;
2774
+ const valueNode2 = path.node.value;
2775
+ if (valueNode2.type === "ConditionalExpression") {
2776
+ const result = transformConditionalBranchInline(valueNode2, textToKey);
2777
+ if (result.count > 0) {
2778
+ path.node.value = result.node;
2779
+ stringsWrapped += result.count;
2780
+ }
2781
+ return;
2782
+ }
2783
+ let text3;
2784
+ let templateInfo2;
2785
+ if (valueNode2.type === "StringLiteral") {
2786
+ text3 = valueNode2.value;
2787
+ } else if (valueNode2.type === "TemplateLiteral") {
2788
+ const info = buildTemplateLiteralText(
2789
+ valueNode2.quasis,
2790
+ valueNode2.expressions
2791
+ );
2792
+ if (info) {
2793
+ text3 = info.text;
2794
+ templateInfo2 = {
2795
+ placeholders: info.placeholders,
2796
+ expressions: valueNode2.expressions
2797
+ };
2798
+ }
2799
+ }
2800
+ if (!text3 || !(text3 in textToKey)) return;
2801
+ const key2 = textToKey[text3];
2802
+ const args2 = [
2803
+ t2.stringLiteral(text3),
2804
+ t2.stringLiteral(key2)
2805
+ ];
2806
+ if (templateInfo2 && templateInfo2.placeholders.length > 0) {
2807
+ args2.push(
2808
+ buildValuesObject(
2809
+ templateInfo2.expressions,
2810
+ templateInfo2.placeholders
2811
+ )
2812
+ );
2813
+ }
2814
+ path.node.value = markGeneratedCall(
2815
+ t2.callExpression(t2.identifier("t"), args2)
2816
+ );
2817
+ stringsWrapped++;
2818
+ return;
2819
+ }
2820
+ const ownerFn = getNearestFunctionPath2(
2821
+ path
2822
+ );
2462
2823
  if (!ownerFn) return;
2463
2824
  if (!functionContainsJSX2(ownerFn.node)) return;
2464
2825
  const compName = getComponentName(ownerFn);
@@ -2516,7 +2877,8 @@ function transformInline(ast, textToKey, options) {
2516
2877
  componentsNeedingT.add(compName);
2517
2878
  }
2518
2879
  });
2519
- if (stringsWrapped === 0 && !repaired && !boundaryRepaired) {
2880
+ const hasModuleFactoryWorkInline = (options.moduleFactoryConstNames?.length ?? 0) > 0 || (options.moduleFactoryImportedNames?.length ?? 0) > 0;
2881
+ if (stringsWrapped === 0 && !repaired && !boundaryRepaired && !hasModuleFactoryWorkInline) {
2520
2882
  return {
2521
2883
  code: generate(ast).code,
2522
2884
  stringsWrapped: 0,
@@ -2524,7 +2886,7 @@ function transformInline(ast, textToKey, options) {
2524
2886
  usedKeys: []
2525
2887
  };
2526
2888
  }
2527
- if (stringsWrapped === 0 && (repaired || boundaryRepaired)) {
2889
+ if (stringsWrapped === 0 && !hasModuleFactoryWorkInline && (repaired || boundaryRepaired)) {
2528
2890
  if (needsClientDirective && boundaryRepaired) {
2529
2891
  addUseClientDirective(ast);
2530
2892
  }
@@ -2536,6 +2898,11 @@ function transformInline(ast, textToKey, options) {
2536
2898
  usedKeys: []
2537
2899
  };
2538
2900
  }
2901
+ const moduleFactoryImportedNamesInline = options.moduleFactoryImportedNames ?? [];
2902
+ const factoryRefComponentsInline = preDiscoverFactoryRefComponents(ast, moduleFactoryImportedNamesInline);
2903
+ for (const comp of factoryRefComponentsInline) {
2904
+ componentsNeedingT.add(comp);
2905
+ }
2539
2906
  const needsHook = componentsNeedingT.size > 0;
2540
2907
  const hookName = isClient ? "useT" : "createT";
2541
2908
  const importPath = isClient ? componentPath : `${componentPath}-server`;
@@ -2585,6 +2952,7 @@ function transformInline(ast, textToKey, options) {
2585
2952
  const importedHookName = isClient ? "useT" : "createT";
2586
2953
  const hookLocalName = resolveImportedLocalName(ast, hookSources, importedHookName) ?? importedHookName;
2587
2954
  const hookCall = isClient ? t2.callExpression(t2.identifier(hookLocalName), []) : t2.callExpression(t2.identifier(hookLocalName), []);
2955
+ const serverAwait = !isClient;
2588
2956
  traverse2(ast, {
2589
2957
  FunctionDeclaration(path) {
2590
2958
  const name = getComponentName(path);
@@ -2595,7 +2963,8 @@ function transformInline(ast, textToKey, options) {
2595
2963
  body,
2596
2964
  hookCall,
2597
2965
  name,
2598
- path.node
2966
+ path.node,
2967
+ serverAwait
2599
2968
  );
2600
2969
  componentTranslatorIds.set(name, translatorId);
2601
2970
  },
@@ -2611,7 +2980,8 @@ function transformInline(ast, textToKey, options) {
2611
2980
  block,
2612
2981
  hookCall,
2613
2982
  name,
2614
- init
2983
+ init,
2984
+ serverAwait
2615
2985
  );
2616
2986
  componentTranslatorIds.set(name, translatorId);
2617
2987
  return;
@@ -2624,7 +2994,8 @@ function transformInline(ast, textToKey, options) {
2624
2994
  block,
2625
2995
  hookCall,
2626
2996
  name,
2627
- wrappedFn
2997
+ wrappedFn,
2998
+ serverAwait
2628
2999
  );
2629
3000
  componentTranslatorIds.set(name, translatorId);
2630
3001
  }
@@ -2638,7 +3009,8 @@ function transformInline(ast, textToKey, options) {
2638
3009
  decl.body,
2639
3010
  hookCall,
2640
3011
  name,
2641
- decl
3012
+ decl,
3013
+ serverAwait
2642
3014
  );
2643
3015
  componentTranslatorIds.set(name, translatorId);
2644
3016
  return;
@@ -2649,7 +3021,8 @@ function transformInline(ast, textToKey, options) {
2649
3021
  block,
2650
3022
  hookCall,
2651
3023
  name,
2652
- decl
3024
+ decl,
3025
+ serverAwait
2653
3026
  );
2654
3027
  componentTranslatorIds.set(name, translatorId);
2655
3028
  return;
@@ -2662,7 +3035,8 @@ function transformInline(ast, textToKey, options) {
2662
3035
  block,
2663
3036
  hookCall,
2664
3037
  name,
2665
- wrappedFn
3038
+ wrappedFn,
3039
+ serverAwait
2666
3040
  );
2667
3041
  componentTranslatorIds.set(name, translatorId);
2668
3042
  }
@@ -2670,24 +3044,44 @@ function transformInline(ast, textToKey, options) {
2670
3044
  noScope: true
2671
3045
  });
2672
3046
  }
3047
+ const moduleFactoryConstNames = options.moduleFactoryConstNames ?? [];
3048
+ const didWrapFactoryInline = wrapModuleFactoryDeclarations(ast, moduleFactoryConstNames);
2673
3049
  rewriteGeneratedCallsForTranslator(ast, componentTranslatorIds);
3050
+ const moduleFactoryImportedNames = options.moduleFactoryImportedNames ?? [];
3051
+ const { componentsNeedingT: factoryComponentsNeedingT, rewrote: didRewriteRefsInline } = rewriteModuleFactoryReferences(
3052
+ ast,
3053
+ moduleFactoryImportedNames,
3054
+ componentTranslatorIds
3055
+ );
3056
+ for (const comp of factoryComponentsNeedingT) {
3057
+ componentsNeedingT.add(comp);
3058
+ }
2674
3059
  if (needsClientDirective && (needsHook || boundaryRepaired)) {
2675
3060
  addUseClientDirective(ast);
2676
3061
  }
3062
+ const didModifyInline = stringsWrapped > 0 || repaired || boundaryRepaired || didWrapFactoryInline || didRewriteRefsInline;
2677
3063
  const output = generate(ast, { retainLines: false });
2678
- return { code: output.code, stringsWrapped, modified: true, usedKeys: [] };
3064
+ return { code: output.code, stringsWrapped, modified: didModifyInline, usedKeys: [] };
2679
3065
  }
2680
- function injectInlineHookIntoBlock(block, hookCall, componentName, ownerFn) {
3066
+ function injectInlineHookIntoBlock(block, hookCall, componentName, ownerFn, useAwait) {
2681
3067
  const targetName = hookCall.callee.type === "Identifier" ? hookCall.callee.name : void 0;
2682
3068
  let sawConflictingT = false;
2683
3069
  for (const stmt of block.body) {
2684
3070
  if (stmt.type !== "VariableDeclaration") continue;
2685
3071
  for (const d of stmt.declarations) {
2686
3072
  if (d.id.type !== "Identifier") continue;
2687
- if (d.init?.type === "CallExpression" && d.init.callee.type === "Identifier" && (d.init.callee.name === "useT" || d.init.callee.name === "createT" || (targetName ? d.init.callee.name === targetName : false))) {
3073
+ const callExpr = d.init?.type === "AwaitExpression" && d.init.argument.type === "CallExpression" ? d.init.argument : d.init?.type === "CallExpression" ? d.init : null;
3074
+ if (callExpr && callExpr.callee.type === "Identifier" && (callExpr.callee.name === "useT" || callExpr.callee.name === "createT" || (targetName ? callExpr.callee.name === targetName : false))) {
2688
3075
  const translatorId2 = d.id.name;
2689
- if (targetName && d.init.callee.name !== targetName) {
2690
- d.init.callee = t2.identifier(targetName);
3076
+ if (targetName && callExpr.callee.name !== targetName) {
3077
+ callExpr.callee = t2.identifier(targetName);
3078
+ }
3079
+ if (useAwait && d.init?.type !== "AwaitExpression") {
3080
+ d.init = t2.awaitExpression(d.init);
3081
+ if (ownerFn && !ownerFn.async) {
3082
+ ownerFn.async = true;
3083
+ ownerFn.returnType = null;
3084
+ }
2691
3085
  }
2692
3086
  return translatorId2;
2693
3087
  }
@@ -2701,13 +3095,15 @@ function injectInlineHookIntoBlock(block, hookCall, componentName, ownerFn) {
2701
3095
  Injected fallback translator "${translatorId}" to avoid conflicts.` : `Detected "const t = ..." conflict. Injected fallback translator "${translatorId}".`
2702
3096
  );
2703
3097
  }
3098
+ const initExpr = useAwait ? t2.awaitExpression(t2.cloneNode(hookCall, true)) : t2.cloneNode(hookCall, true);
2704
3099
  const tDecl = t2.variableDeclaration("const", [
2705
- t2.variableDeclarator(
2706
- t2.identifier(translatorId),
2707
- t2.cloneNode(hookCall, true)
2708
- )
3100
+ t2.variableDeclarator(t2.identifier(translatorId), initExpr)
2709
3101
  ]);
2710
3102
  block.body.unshift(tDecl);
3103
+ if (useAwait && ownerFn && !ownerFn.async) {
3104
+ ownerFn.async = true;
3105
+ ownerFn.returnType = null;
3106
+ }
2711
3107
  return translatorId;
2712
3108
  }
2713
3109
  var traverse2, generate;
@@ -2724,7 +3120,7 @@ var init_transform = __esm({
2724
3120
  });
2725
3121
 
2726
3122
  // src/codegen/index.ts
2727
- import { dirname, extname, join as join2, resolve } from "path";
3123
+ import { basename, dirname, extname, join as join2, resolve } from "path";
2728
3124
  import { readFile as readFile3, writeFile } from "fs/promises";
2729
3125
  import _traverse3 from "@babel/traverse";
2730
3126
  import { glob as glob2 } from "tinyglobby";
@@ -2896,6 +3292,445 @@ function buildClientGraph(entries, cwd, pathAliases) {
2896
3292
  }
2897
3293
  return clientReachable;
2898
3294
  }
3295
+ function hasTranslatableValue(node, textToKey) {
3296
+ if (node.type === "StringLiteral") {
3297
+ const text2 = node.value.trim();
3298
+ return !!text2 && text2 in textToKey;
3299
+ }
3300
+ if (node.type === "TemplateLiteral") {
3301
+ const info = buildTemplateLiteralText(node.quasis, node.expressions);
3302
+ return !!info && info.text in textToKey;
3303
+ }
3304
+ if (node.type === "ConditionalExpression") {
3305
+ return hasTranslatableValue(node.consequent, textToKey) || hasTranslatableValue(node.alternate, textToKey);
3306
+ }
3307
+ return false;
3308
+ }
3309
+ function collectModuleFactoryCandidates(entries, textToKey) {
3310
+ const candidates = /* @__PURE__ */ new Map();
3311
+ for (const entry of entries) {
3312
+ if (!entry.ast) continue;
3313
+ const topLevelConsts = /* @__PURE__ */ new Map();
3314
+ for (const node of entry.ast.program.body) {
3315
+ let decl;
3316
+ let exported = false;
3317
+ if (node.type === "VariableDeclaration" && node.kind === "const") {
3318
+ decl = node;
3319
+ } else if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration" && node.declaration.kind === "const") {
3320
+ decl = node.declaration;
3321
+ exported = true;
3322
+ }
3323
+ if (!decl) continue;
3324
+ for (const declarator of decl.declarations) {
3325
+ if (declarator.id.type !== "Identifier") continue;
3326
+ const typed = !!declarator.id.typeAnnotation;
3327
+ topLevelConsts.set(declarator.id.name, { exported, typed });
3328
+ }
3329
+ }
3330
+ traverse3(entry.ast, {
3331
+ ObjectProperty(path) {
3332
+ let current = path.parentPath;
3333
+ let insideFunction = false;
3334
+ let constName;
3335
+ while (current) {
3336
+ if (current.isFunctionDeclaration?.() || current.isFunctionExpression?.() || current.isArrowFunctionExpression?.()) {
3337
+ insideFunction = true;
3338
+ break;
3339
+ }
3340
+ if (current.isVariableDeclarator?.()) {
3341
+ const id = current.node.id;
3342
+ if (id.type === "Identifier" && topLevelConsts.has(id.name)) {
3343
+ constName = id.name;
3344
+ }
3345
+ }
3346
+ current = current.parentPath;
3347
+ }
3348
+ if (insideFunction || !constName) return;
3349
+ const info = topLevelConsts.get(constName);
3350
+ if (!info) return;
3351
+ if (info.exported && FRAMEWORK_RESERVED_EXPORTS.has(constName)) return;
3352
+ const keyNode = path.node.key;
3353
+ if (keyNode.type !== "Identifier" && keyNode.type !== "StringLiteral") return;
3354
+ const propName = keyNode.type === "Identifier" ? keyNode.name : keyNode.value;
3355
+ if (!isContentProperty(propName)) return;
3356
+ const valueNode = path.node.value;
3357
+ if (!hasTranslatableValue(valueNode, textToKey)) return;
3358
+ if (!candidates.has(entry.filePath)) {
3359
+ candidates.set(entry.filePath, /* @__PURE__ */ new Set());
3360
+ }
3361
+ candidates.get(entry.filePath).add(constName);
3362
+ },
3363
+ noScope: true
3364
+ });
3365
+ }
3366
+ return candidates;
3367
+ }
3368
+ function collectModuleFactoryImportEdges(entries, candidates, cwd, knownFiles, pathAliases) {
3369
+ const importsByFile = /* @__PURE__ */ new Map();
3370
+ const factoryExports = /* @__PURE__ */ new Map();
3371
+ for (const [file, names] of candidates) {
3372
+ factoryExports.set(file, names);
3373
+ }
3374
+ for (const entry of entries) {
3375
+ if (!entry.ast) continue;
3376
+ for (const node of entry.ast.program.body) {
3377
+ if (node.type !== "ImportDeclaration") continue;
3378
+ if (node.specifiers.some((s) => s.type === "ImportNamespaceSpecifier")) continue;
3379
+ const source = node.source.value;
3380
+ const resolved = resolveLocalImport(
3381
+ entry.filePath,
3382
+ source,
3383
+ cwd,
3384
+ knownFiles,
3385
+ pathAliases
3386
+ );
3387
+ if (!resolved || !factoryExports.has(resolved)) continue;
3388
+ const exportedNames = factoryExports.get(resolved);
3389
+ for (const spec of node.specifiers) {
3390
+ if (spec.type !== "ImportSpecifier") continue;
3391
+ const importedName = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
3392
+ if (!exportedNames.has(importedName)) continue;
3393
+ const localName = spec.local.name;
3394
+ if (!importsByFile.has(entry.filePath)) {
3395
+ importsByFile.set(entry.filePath, []);
3396
+ }
3397
+ importsByFile.get(entry.filePath).push(localName);
3398
+ }
3399
+ }
3400
+ }
3401
+ return importsByFile;
3402
+ }
3403
+ function collectLocalFactoryUsages(ast, constNames) {
3404
+ const used = /* @__PURE__ */ new Set();
3405
+ traverse3(ast, {
3406
+ Identifier(path) {
3407
+ if (!constNames.has(path.node.name)) return;
3408
+ const parent = path.parent;
3409
+ if (parent.type === "VariableDeclarator" && parent.id === path.node) return;
3410
+ if (parent.type === "ImportSpecifier" || parent.type === "ExportSpecifier" || parent.type === "ImportDefaultSpecifier") return;
3411
+ const compName = getComponentName(path);
3412
+ if (compName && isPascalCase(compName)) {
3413
+ used.add(path.node.name);
3414
+ }
3415
+ },
3416
+ noScope: true
3417
+ });
3418
+ return [...used];
3419
+ }
3420
+ function markUnsafeExport(importerAst, localName, importerFile, safeCandidates, entries, cwd, knownFiles, pathAliases, unsafeExportedNames) {
3421
+ for (const node of importerAst.program.body) {
3422
+ if (node.type !== "ImportDeclaration") continue;
3423
+ for (const spec of node.specifiers) {
3424
+ if (spec.type !== "ImportSpecifier") continue;
3425
+ if (spec.local.name !== localName) continue;
3426
+ const exportedName = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
3427
+ const resolved = resolveLocalImport(
3428
+ importerFile,
3429
+ node.source.value,
3430
+ cwd,
3431
+ knownFiles,
3432
+ pathAliases
3433
+ );
3434
+ if (resolved && safeCandidates.has(resolved)) {
3435
+ if (!unsafeExportedNames.has(resolved)) {
3436
+ unsafeExportedNames.set(resolved, /* @__PURE__ */ new Set());
3437
+ }
3438
+ unsafeExportedNames.get(resolved).add(exportedName);
3439
+ }
3440
+ }
3441
+ }
3442
+ if (safeCandidates.has(importerFile) && safeCandidates.get(importerFile).has(localName)) {
3443
+ if (!unsafeExportedNames.has(importerFile)) {
3444
+ unsafeExportedNames.set(importerFile, /* @__PURE__ */ new Set());
3445
+ }
3446
+ unsafeExportedNames.get(importerFile).add(localName);
3447
+ }
3448
+ }
3449
+ function resolveLocalToExportedName(importerAst, localName, importerFile, sourceFile, cwd, knownFiles, pathAliases) {
3450
+ if (!importerAst) return void 0;
3451
+ for (const node of importerAst.program.body) {
3452
+ if (node.type !== "ImportDeclaration") continue;
3453
+ for (const spec of node.specifiers) {
3454
+ if (spec.type !== "ImportSpecifier") continue;
3455
+ if (spec.local.name !== localName) continue;
3456
+ const resolved = resolveLocalImport(
3457
+ importerFile,
3458
+ node.source.value,
3459
+ cwd,
3460
+ knownFiles,
3461
+ pathAliases
3462
+ );
3463
+ if (resolved !== sourceFile) continue;
3464
+ return spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
3465
+ }
3466
+ }
3467
+ if (importerFile === sourceFile) return localName;
3468
+ return void 0;
3469
+ }
3470
+ function isInsideFunctionParam2(path) {
3471
+ let current = path;
3472
+ while (current.parentPath) {
3473
+ if (current.parentPath.isFunction()) {
3474
+ const fn = current.parentPath.node;
3475
+ return fn.params.includes(current.node);
3476
+ }
3477
+ current = current.parentPath;
3478
+ }
3479
+ return false;
3480
+ }
3481
+ function getBindingSafety(ast, bindingName) {
3482
+ let unsafe = false;
3483
+ let reason;
3484
+ traverse3(ast, {
3485
+ Identifier(path) {
3486
+ if (unsafe) return;
3487
+ if (path.node.name !== bindingName) return;
3488
+ const binding = path.scope?.getBinding(bindingName);
3489
+ if (!binding) return;
3490
+ const bPath = binding.path;
3491
+ const isTopLevel = bPath.isVariableDeclarator?.() && bPath.parentPath?.isVariableDeclaration?.() && (bPath.parentPath.parentPath?.isProgram?.() || bPath.parentPath.parentPath?.isExportNamedDeclaration?.());
3492
+ const isImportBinding = bPath.isImportSpecifier?.() || bPath.isImportDefaultSpecifier?.();
3493
+ if (!isTopLevel && !isImportBinding) return;
3494
+ const parent = path.parent;
3495
+ if (parent.type === "ImportSpecifier" || parent.type === "ImportDefaultSpecifier" || parent.type === "ExportSpecifier") {
3496
+ return;
3497
+ }
3498
+ if (parent.type === "TSTypeReference" || parent.type === "TSTypeQuery" || parent.type === "TSTypeAnnotation") {
3499
+ return;
3500
+ }
3501
+ if (parent.type === "VariableDeclarator" && parent.id === path.node) {
3502
+ return;
3503
+ }
3504
+ const compName = getComponentName(path);
3505
+ if (!compName || !isPascalCase(compName)) {
3506
+ unsafe = true;
3507
+ reason = `"${bindingName}" is referenced outside a React component`;
3508
+ return;
3509
+ }
3510
+ if (isInsideFunctionParam2(path)) {
3511
+ unsafe = true;
3512
+ reason = `"${bindingName}" is referenced in a function parameter default`;
3513
+ return;
3514
+ }
3515
+ if (parent.type === "AssignmentExpression" && parent.left === path.node) {
3516
+ unsafe = true;
3517
+ reason = `"${bindingName}" is mutated via assignment`;
3518
+ return;
3519
+ }
3520
+ if (parent.type === "MemberExpression" && parent.object === path.node) {
3521
+ const grandParent = path.parentPath?.parent;
3522
+ if (grandParent?.type === "CallExpression" && grandParent.callee === parent && parent.property.type === "Identifier" && ["push", "splice", "pop", "shift", "unshift", "sort", "reverse", "fill"].includes(parent.property.name)) {
3523
+ unsafe = true;
3524
+ reason = `"${bindingName}" is mutated via .${parent.property.name}()`;
3525
+ return;
3526
+ }
3527
+ if (grandParent?.type === "AssignmentExpression" && grandParent.left === parent) {
3528
+ unsafe = true;
3529
+ reason = `"${bindingName}" is mutated via property/indexed assignment`;
3530
+ return;
3531
+ }
3532
+ if (grandParent?.type === "UnaryExpression" && grandParent.operator === "delete") {
3533
+ unsafe = true;
3534
+ reason = `"${bindingName}" is mutated via delete`;
3535
+ return;
3536
+ }
3537
+ }
3538
+ if (parent.type === "UpdateExpression") {
3539
+ unsafe = true;
3540
+ reason = `"${bindingName}" is mutated via ${parent.operator}`;
3541
+ return;
3542
+ }
3543
+ }
3544
+ });
3545
+ return unsafe ? { safe: false, reason } : { safe: true };
3546
+ }
3547
+ async function findExternalImportersOfCandidates(candidates, entries, cwd, pathAliases) {
3548
+ const blocked = /* @__PURE__ */ new Map();
3549
+ if (candidates.size === 0) return blocked;
3550
+ const allFiles = await glob2(["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"], {
3551
+ ignore: ["node_modules/**", "dist/**", "build/**", ".next/**", ".output/**"],
3552
+ cwd,
3553
+ absolute: true
3554
+ });
3555
+ const includedFiles = new Set(entries.map((e) => e.filePath));
3556
+ const externalFiles = allFiles.filter((f) => !includedFiles.has(f));
3557
+ if (externalFiles.length === 0) return blocked;
3558
+ const candidateBasenames = /* @__PURE__ */ new Set();
3559
+ for (const file of candidates.keys()) {
3560
+ candidateBasenames.add(basename(file).replace(/\.[^.]+$/, ""));
3561
+ }
3562
+ const allKnownFiles = new Set(allFiles);
3563
+ const parseLimit = pLimit3(10);
3564
+ await Promise.all(
3565
+ externalFiles.map(
3566
+ (extFile) => parseLimit(async () => {
3567
+ let content;
3568
+ try {
3569
+ content = await readFile3(extFile, "utf-8");
3570
+ } catch {
3571
+ return;
3572
+ }
3573
+ let mightImport = false;
3574
+ for (const base of candidateBasenames) {
3575
+ if (content.includes(base)) {
3576
+ mightImport = true;
3577
+ break;
3578
+ }
3579
+ }
3580
+ if (!mightImport) return;
3581
+ let ast;
3582
+ try {
3583
+ ast = parseFile(content, extFile);
3584
+ } catch {
3585
+ return;
3586
+ }
3587
+ for (const node of ast.program.body) {
3588
+ if (node.type !== "ImportDeclaration") continue;
3589
+ const resolved = resolveLocalImport(
3590
+ extFile,
3591
+ node.source.value,
3592
+ cwd,
3593
+ allKnownFiles,
3594
+ pathAliases
3595
+ );
3596
+ if (!resolved || !candidates.has(resolved)) continue;
3597
+ const exportedNames = candidates.get(resolved);
3598
+ if (node.specifiers.some((s) => s.type === "ImportNamespaceSpecifier")) {
3599
+ if (!blocked.has(resolved)) blocked.set(resolved, /* @__PURE__ */ new Set());
3600
+ for (const name of exportedNames) {
3601
+ blocked.get(resolved).add(name);
3602
+ }
3603
+ continue;
3604
+ }
3605
+ for (const spec of node.specifiers) {
3606
+ if (spec.type !== "ImportSpecifier") continue;
3607
+ const importedName = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
3608
+ if (exportedNames.has(importedName)) {
3609
+ if (!blocked.has(resolved)) blocked.set(resolved, /* @__PURE__ */ new Set());
3610
+ blocked.get(resolved).add(importedName);
3611
+ }
3612
+ }
3613
+ }
3614
+ })
3615
+ )
3616
+ );
3617
+ return blocked;
3618
+ }
3619
+ async function buildModuleFactoryPlan(entries, textToKey, cwd, pathAliases) {
3620
+ const candidates = collectModuleFactoryCandidates(entries, textToKey);
3621
+ const knownFiles = new Set(entries.filter((e) => e.ast).map((e) => e.filePath));
3622
+ const externallyBlocked = await findExternalImportersOfCandidates(
3623
+ candidates,
3624
+ entries,
3625
+ cwd,
3626
+ pathAliases
3627
+ );
3628
+ for (const [file, names] of externallyBlocked) {
3629
+ const candidateNames = candidates.get(file);
3630
+ if (!candidateNames) continue;
3631
+ for (const name of names) {
3632
+ if (candidateNames.has(name)) {
3633
+ candidateNames.delete(name);
3634
+ logWarning(
3635
+ `Module factory: blocking "${name}" in ${file} \u2014 imported by a file outside the include scope`
3636
+ );
3637
+ }
3638
+ }
3639
+ if (candidateNames.size === 0) {
3640
+ candidates.delete(file);
3641
+ }
3642
+ }
3643
+ const safeCandidates = /* @__PURE__ */ new Map();
3644
+ for (const [file, names] of candidates) {
3645
+ const entry = entries.find((e) => e.filePath === file);
3646
+ if (!entry?.ast) continue;
3647
+ const safeNames = /* @__PURE__ */ new Set();
3648
+ for (const name of names) {
3649
+ const safety = getBindingSafety(entry.ast, name);
3650
+ if (safety.safe) {
3651
+ safeNames.add(name);
3652
+ } else {
3653
+ logWarning(`Module factory: skipping "${name}" in ${file}: ${safety.reason}`);
3654
+ }
3655
+ }
3656
+ if (safeNames.size > 0) {
3657
+ safeCandidates.set(file, safeNames);
3658
+ }
3659
+ }
3660
+ const importEdges = collectModuleFactoryImportEdges(
3661
+ entries,
3662
+ safeCandidates,
3663
+ cwd,
3664
+ knownFiles,
3665
+ pathAliases
3666
+ );
3667
+ for (const [file, names] of safeCandidates) {
3668
+ const entry = entries.find((e) => e.filePath === file);
3669
+ if (!entry?.ast) continue;
3670
+ const localRefs = collectLocalFactoryUsages(entry.ast, names);
3671
+ if (localRefs.length > 0) {
3672
+ const existing = importEdges.get(file) ?? [];
3673
+ importEdges.set(file, [...existing, ...localRefs]);
3674
+ }
3675
+ }
3676
+ const unsafeExportedNames = /* @__PURE__ */ new Map();
3677
+ const safeImports = /* @__PURE__ */ new Map();
3678
+ for (const [file, localNames] of importEdges) {
3679
+ const entry = entries.find((e) => e.filePath === file);
3680
+ if (!entry?.ast) continue;
3681
+ const safeLocalNames = [];
3682
+ for (const name of localNames) {
3683
+ const safety = getBindingSafety(entry.ast, name);
3684
+ if (safety.safe) {
3685
+ safeLocalNames.push(name);
3686
+ } else {
3687
+ logWarning(`Module factory: skipping import "${name}" in ${file}: ${safety.reason}`);
3688
+ markUnsafeExport(entry.ast, name, file, safeCandidates, entries, cwd, knownFiles, pathAliases, unsafeExportedNames);
3689
+ }
3690
+ }
3691
+ if (safeLocalNames.length > 0) {
3692
+ safeImports.set(file, safeLocalNames);
3693
+ }
3694
+ }
3695
+ for (const [sourceFile, unsafeNames] of unsafeExportedNames) {
3696
+ const safeNames = safeCandidates.get(sourceFile);
3697
+ if (!safeNames) continue;
3698
+ for (const name of unsafeNames) {
3699
+ safeNames.delete(name);
3700
+ logWarning(`Module factory: blocking "${name}" in ${sourceFile} because an importer cannot be safely rewritten`);
3701
+ }
3702
+ if (safeNames.size === 0) {
3703
+ safeCandidates.delete(sourceFile);
3704
+ }
3705
+ for (const [importFile, locals] of safeImports) {
3706
+ const filtered = locals.filter((l) => {
3707
+ const exportedName = resolveLocalToExportedName(
3708
+ entries.find((e) => e.filePath === importFile)?.ast,
3709
+ l,
3710
+ importFile,
3711
+ sourceFile,
3712
+ cwd,
3713
+ knownFiles,
3714
+ pathAliases
3715
+ );
3716
+ return !exportedName || !unsafeNames.has(exportedName);
3717
+ });
3718
+ if (filtered.length > 0) {
3719
+ safeImports.set(importFile, filtered);
3720
+ } else {
3721
+ safeImports.delete(importFile);
3722
+ }
3723
+ }
3724
+ }
3725
+ const factoryConstsByFile = /* @__PURE__ */ new Map();
3726
+ for (const [file, names] of safeCandidates) {
3727
+ factoryConstsByFile.set(file, [...names]);
3728
+ }
3729
+ return {
3730
+ factoryConstsByFile,
3731
+ factoryImportsByFile: safeImports
3732
+ };
3733
+ }
2899
3734
  async function codegen(options, cwd = process.cwd()) {
2900
3735
  const files = await glob2(options.include, {
2901
3736
  ignore: options.exclude ?? [],
@@ -2905,7 +3740,8 @@ async function codegen(options, cwd = process.cwd()) {
2905
3740
  const transformOpts = {
2906
3741
  i18nImport: options.i18nImport,
2907
3742
  mode: options.mode,
2908
- componentPath: options.componentPath
3743
+ componentPath: options.componentPath,
3744
+ translatableProps: options.translatableProps
2909
3745
  };
2910
3746
  const parseLimit = pLimit3(10);
2911
3747
  const parsedEntries = await Promise.all(
@@ -2933,6 +3769,15 @@ async function codegen(options, cwd = process.cwd()) {
2933
3769
  );
2934
3770
  const pathAliases = await loadPathAliasResolvers(cwd);
2935
3771
  const forceClientSet = buildClientGraph(parsedEntries, cwd, pathAliases);
3772
+ let factoryPlan;
3773
+ if (options.moduleFactory) {
3774
+ factoryPlan = await buildModuleFactoryPlan(
3775
+ parsedEntries,
3776
+ options.textToKey,
3777
+ cwd,
3778
+ pathAliases
3779
+ );
3780
+ }
2936
3781
  const limit = pLimit3(10);
2937
3782
  let completed = 0;
2938
3783
  const clientNamespacesSet = /* @__PURE__ */ new Set();
@@ -2950,7 +3795,9 @@ async function codegen(options, cwd = process.cwd()) {
2950
3795
  const isClient = forceClientSet.has(entry.filePath) || entry.isClientRoot;
2951
3796
  const fileTransformOpts = {
2952
3797
  ...transformOpts,
2953
- forceClient: forceClientSet.has(entry.filePath)
3798
+ forceClient: forceClientSet.has(entry.filePath),
3799
+ moduleFactoryConstNames: factoryPlan?.factoryConstsByFile.get(entry.filePath),
3800
+ moduleFactoryImportedNames: factoryPlan?.factoryImportsByFile.get(entry.filePath)
2954
3801
  };
2955
3802
  const result = transform(
2956
3803
  entry.ast,
@@ -3009,13 +3856,15 @@ async function codegen(options, cwd = process.cwd()) {
3009
3856
  clientNamespaces: [...clientNamespacesSet].sort()
3010
3857
  };
3011
3858
  }
3012
- var traverse3, SOURCE_EXTENSIONS;
3859
+ var traverse3, SOURCE_EXTENSIONS, FRAMEWORK_RESERVED_EXPORTS;
3013
3860
  var init_codegen = __esm({
3014
3861
  "src/codegen/index.ts"() {
3015
3862
  "use strict";
3016
3863
  init_parser();
3017
3864
  init_ast_helpers();
3018
3865
  init_transform();
3866
+ init_filters();
3867
+ init_template_literal();
3019
3868
  init_logger();
3020
3869
  traverse3 = resolveDefault(_traverse3);
3021
3870
  SOURCE_EXTENSIONS = [
@@ -3028,6 +3877,20 @@ var init_codegen = __esm({
3028
3877
  ".mjs",
3029
3878
  ".cjs"
3030
3879
  ];
3880
+ FRAMEWORK_RESERVED_EXPORTS = /* @__PURE__ */ new Set([
3881
+ "metadata",
3882
+ "generateMetadata",
3883
+ "viewport",
3884
+ "generateViewport",
3885
+ "revalidate",
3886
+ "dynamic",
3887
+ "dynamicParams",
3888
+ "fetchCache",
3889
+ "runtime",
3890
+ "preferredRegion",
3891
+ "maxDuration",
3892
+ "generateStaticParams"
3893
+ ]);
3031
3894
  }
3032
3895
  });
3033
3896
 
@@ -3224,7 +4087,10 @@ async function translateAll(input) {
3224
4087
  )
3225
4088
  );
3226
4089
  if (totalInputTokens > 0 || totalOutputTokens > 0) {
3227
- onUsage?.({ inputTokens: totalInputTokens, outputTokens: totalOutputTokens });
4090
+ onUsage?.({
4091
+ inputTokens: totalInputTokens,
4092
+ outputTokens: totalOutputTokens
4093
+ });
3228
4094
  }
3229
4095
  return results;
3230
4096
  }
@@ -3266,7 +4132,9 @@ async function writeTranslationSplit(dir, flatEntries) {
3266
4132
  const content = JSON.stringify(nested, null, 2) + "\n";
3267
4133
  await writeFile2(filePath, content, "utf-8");
3268
4134
  }
3269
- const currentFiles = new Set([...byNamespace.keys()].map((ns) => `${ns}.json`));
4135
+ const currentFiles = new Set(
4136
+ [...byNamespace.keys()].map((ns) => `${ns}.json`)
4137
+ );
3270
4138
  let existing;
3271
4139
  try {
3272
4140
  existing = await readdir(dir);
@@ -3483,7 +4351,11 @@ async function runScanStep(input) {
3483
4351
  await writeFile4(sourceFile, content, "utf-8");
3484
4352
  }
3485
4353
  if (config.typeSafe) {
3486
- await generateNextIntlTypes(config.messagesDir, config.sourceLocale, config.splitByNamespace);
4354
+ await generateNextIntlTypes(
4355
+ config.messagesDir,
4356
+ config.sourceLocale,
4357
+ config.splitByNamespace
4358
+ );
3487
4359
  }
3488
4360
  }
3489
4361
  return {
@@ -3513,6 +4385,8 @@ async function runCodegenStep(input) {
3513
4385
  i18nImport: config.scan.i18nImport,
3514
4386
  mode,
3515
4387
  componentPath: config.inline?.componentPath,
4388
+ moduleFactory: input.moduleFactory,
4389
+ translatableProps: config.scan.translatableProps,
3516
4390
  onProgress: callbacks?.onProgress
3517
4391
  },
3518
4392
  cwd
@@ -3659,7 +4533,11 @@ function createUsageTracker() {
3659
4533
  outputTokens += usage.outputTokens ?? 0;
3660
4534
  },
3661
4535
  get() {
3662
- return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
4536
+ return {
4537
+ inputTokens,
4538
+ outputTokens,
4539
+ totalTokens: inputTokens + outputTokens
4540
+ };
3663
4541
  }
3664
4542
  };
3665
4543
  }
@@ -3679,7 +4557,11 @@ async function estimateCost(model, usage) {
3679
4557
  providers
3680
4558
  });
3681
4559
  if (costs.totalUSD == null) return null;
3682
- return { totalUSD: costs.totalUSD, inputUSD: costs.inputUSD ?? 0, outputUSD: costs.outputUSD ?? 0 };
4560
+ return {
4561
+ totalUSD: costs.totalUSD,
4562
+ inputUSD: costs.inputUSD ?? 0,
4563
+ outputUSD: costs.outputUSD ?? 0
4564
+ };
3683
4565
  } catch {
3684
4566
  return null;
3685
4567
  }
@@ -3724,6 +4606,19 @@ var init_cli_utils = __esm({
3724
4606
  });
3725
4607
 
3726
4608
  // src/templates/t-component.ts
4609
+ function buildSingleFileLoadBody(targetLocales, relativeMessagesDir) {
4610
+ const cases = targetLocales.map(
4611
+ (locale) => ` case "${locale}": return (await import("${relativeMessagesDir}/${locale}.json")).default;`
4612
+ ).join("\n");
4613
+ return ` try {
4614
+ switch (locale) {
4615
+ ${cases}
4616
+ default: return {};
4617
+ }
4618
+ } catch {
4619
+ return {};
4620
+ }`;
4621
+ }
3727
4622
  function serverTemplate(clientBasename, opts) {
3728
4623
  if (!opts) {
3729
4624
  return `import type { ReactNode } from "react";
@@ -3757,6 +4652,15 @@ export function createT(messages?: Messages) {
3757
4652
  }
3758
4653
  const allLocales = [opts.sourceLocale, ...opts.targetLocales];
3759
4654
  const allLocalesStr = allLocales.map((l) => `"${l}"`).join(", ");
4655
+ const isSplit = !!opts.splitByNamespace;
4656
+ let messagesImportPath = opts.relativeMessagesDir ?? opts.messagesDir;
4657
+ if (!messagesImportPath.startsWith(".") && !messagesImportPath.startsWith("/")) {
4658
+ messagesImportPath = `./${messagesImportPath}`;
4659
+ }
4660
+ const loadBody = isSplit ? SPLIT_LOAD_BODY : buildSingleFileLoadBody(opts.targetLocales, messagesImportPath);
4661
+ const messagesDirConst = isSplit ? `
4662
+ const messagesDir = "${opts.messagesDir}";
4663
+ ` : "\n";
3760
4664
  return `import type { ReactNode } from "react";
3761
4665
  import { cache } from "react";
3762
4666
  export { I18nProvider } from "./${clientBasename}";
@@ -3765,9 +4669,7 @@ type Messages = Record<string, string>;
3765
4669
 
3766
4670
  const supported = [${allLocalesStr}] as const;
3767
4671
  type Locale = (typeof supported)[number];
3768
- const defaultLocale: Locale = "${opts.sourceLocale}";
3769
- const messagesDir = "${opts.messagesDir}";
3770
-
4672
+ const defaultLocale: Locale = "${opts.sourceLocale}";${messagesDirConst}
3771
4673
  function parseAcceptLanguage(header: string): Locale {
3772
4674
  const langs = header
3773
4675
  .split(",")
@@ -3789,8 +4691,7 @@ export function setLocale(locale: string) {
3789
4691
  getLocaleStore().current = locale;
3790
4692
  }
3791
4693
 
3792
- // Per-request cached message loading \u2014 works even when layout is cached during client-side navigation
3793
- // Uses dynamic imports so this file can be safely imported from client components
4694
+ // Per-request cached message loading \u2014 works on all platforms via static imports
3794
4695
  const getCachedMessages = cache(async (): Promise<Messages> => {
3795
4696
  let locale: Locale | null = getLocaleStore().current as Locale | null;
3796
4697
 
@@ -3813,7 +4714,7 @@ const getCachedMessages = cache(async (): Promise<Messages> => {
3813
4714
  }
3814
4715
 
3815
4716
  if (locale === defaultLocale) return {};
3816
- ${opts.splitByNamespace ? SPLIT_LOAD_BODY : SINGLE_FILE_LOAD_BODY}
4717
+ ${loadBody}
3817
4718
  });
3818
4719
 
3819
4720
  // Per-request message store (populated by setServerMessages in layout)
@@ -3942,7 +4843,7 @@ ${getMessagesBody}
3942
4843
  }
3943
4844
  `;
3944
4845
  }
3945
- var CLIENT_TEMPLATE, SINGLE_FILE_LOAD_BODY, SPLIT_LOAD_BODY;
4846
+ var CLIENT_TEMPLATE, SPLIT_LOAD_BODY;
3946
4847
  var init_t_component = __esm({
3947
4848
  "src/templates/t-component.ts"() {
3948
4849
  "use strict";
@@ -3976,18 +4877,9 @@ export function useT() {
3976
4877
  };
3977
4878
  }
3978
4879
  `;
3979
- SINGLE_FILE_LOAD_BODY = ` const { readFile } = await import("node:fs/promises");
3980
- const { join } = await import("node:path");
3981
- try {
3982
- const filePath = join(process.cwd(), messagesDir, \`\${locale}.json\`);
3983
- const content = await readFile(filePath, "utf-8");
3984
- return JSON.parse(content);
3985
- } catch {
3986
- return {};
3987
- }`;
3988
- SPLIT_LOAD_BODY = ` const { readFile, readdir } = await import("node:fs/promises");
3989
- const { join } = await import("node:path");
3990
- try {
4880
+ SPLIT_LOAD_BODY = ` try {
4881
+ const { readFile, readdir } = await import("node:fs/promises");
4882
+ const { join } = await import("node:path");
3991
4883
  const dir = join(process.cwd(), messagesDir, locale);
3992
4884
  let files: string[];
3993
4885
  try { files = (await readdir(dir)).filter(f => f.endsWith(".json")); } catch { return {}; }
@@ -4013,24 +4905,32 @@ export function useT() {
4013
4905
  // src/init.ts
4014
4906
  var init_exports = {};
4015
4907
  __export(init_exports, {
4908
+ detectIncludePatterns: () => detectIncludePatterns,
4016
4909
  generateConfigFile: () => generateConfigFile,
4017
4910
  runInitWizard: () => runInitWizard,
4018
4911
  updateLayoutWithSelectiveMessages: () => updateLayoutWithSelectiveMessages
4019
4912
  });
4020
4913
  import * as p from "@clack/prompts";
4021
4914
  import { existsSync } from "fs";
4022
- import { basename, join as join6, relative } from "path";
4915
+ import { basename as basename2, join as join6, relative } from "path";
4023
4916
  import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
4024
4917
  function detectIncludePatterns(cwd) {
4025
4918
  const patterns = [];
4026
- if (existsSync(join6(cwd, "app")))
4919
+ const hasSrc = existsSync(join6(cwd, "src"));
4920
+ if (hasSrc && existsSync(join6(cwd, "src", "app"))) {
4921
+ patterns.push("src/app/**/*.tsx", "src/app/**/*.jsx");
4922
+ } else if (existsSync(join6(cwd, "app"))) {
4027
4923
  patterns.push("app/**/*.tsx", "app/**/*.jsx");
4028
- if (existsSync(join6(cwd, "src")))
4029
- patterns.push("src/**/*.tsx", "src/**/*.jsx");
4030
- if (existsSync(join6(cwd, "pages")))
4924
+ }
4925
+ if (hasSrc && existsSync(join6(cwd, "src", "pages"))) {
4926
+ patterns.push("src/pages/**/*.tsx", "src/pages/**/*.jsx");
4927
+ } else if (existsSync(join6(cwd, "pages"))) {
4031
4928
  patterns.push("pages/**/*.tsx", "pages/**/*.jsx");
4032
- if (existsSync(join6(cwd, "src", "app"))) {
4033
- return patterns.filter((p2) => !p2.startsWith("app/"));
4929
+ }
4930
+ if (hasSrc && existsSync(join6(cwd, "src", "components"))) {
4931
+ patterns.push("src/components/**/*.tsx", "src/components/**/*.jsx");
4932
+ } else if (existsSync(join6(cwd, "components"))) {
4933
+ patterns.push("components/**/*.tsx", "components/**/*.jsx");
4034
4934
  }
4035
4935
  return patterns.length > 0 ? patterns : ["**/*.tsx", "**/*.jsx"];
4036
4936
  }
@@ -4097,6 +4997,13 @@ function generateConfigFile(opts) {
4097
4997
  lines.push(
4098
4998
  ` include: [${opts.includePatterns.map((p2) => `"${p2}"`).join(", ")}],`
4099
4999
  );
5000
+ lines.push(` // Add more directories as needed. Examples:`);
5001
+ lines.push(` // "config/**/*.ts" \u2014 for data/config files (needed with --module-factory)`);
5002
+ lines.push(` // "lib/**/*.ts" \u2014 for utility files with translatable strings`);
5003
+ lines.push(` // "layouts/**/*.tsx" \u2014 for layout components`);
5004
+ lines.push(` //`);
5005
+ lines.push(` // Note: When using codegen --module-factory, include any directories`);
5006
+ lines.push(` // that contain exported constants with translatable strings (e.g. config/).`);
4100
5007
  lines.push(` exclude: ["**/*.test.*", "**/*.spec.*"],`);
4101
5008
  if (opts.mode === "keys" && opts.i18nImport) {
4102
5009
  lines.push(` i18nImport: "${opts.i18nImport}",`);
@@ -4259,11 +5166,7 @@ export default getRequestConfig(async () => {
4259
5166
  "export default withNextIntl($1);"
4260
5167
  );
4261
5168
  const updated = importLine + "\n" + pluginLine + "\n" + wrapped;
4262
- if (await safeWriteModifiedFile(
4263
- nextConfigPath,
4264
- updated,
4265
- "next.config.ts"
4266
- )) {
5169
+ if (await safeWriteModifiedFile(nextConfigPath, updated, "next.config.ts")) {
4267
5170
  filesCreated.push("next.config.ts (updated)");
4268
5171
  }
4269
5172
  }
@@ -4287,16 +5190,16 @@ export default getRequestConfig(async () => {
4287
5190
  /<\/body>/,
4288
5191
  " </NextIntlClientProvider>\n </body>"
4289
5192
  );
4290
- if (await safeWriteModifiedFile(
4291
- layoutPath,
4292
- layoutContent,
4293
- "root layout"
4294
- )) {
5193
+ if (await safeWriteModifiedFile(layoutPath, layoutContent, "root layout")) {
4295
5194
  filesCreated.push(relative(cwd, layoutPath) + " (updated)");
4296
5195
  }
4297
5196
  }
4298
5197
  }
4299
- await createEmptyMessageFiles(join6(cwd, messagesDir), allLocales, splitByNamespace);
5198
+ await createEmptyMessageFiles(
5199
+ join6(cwd, messagesDir),
5200
+ allLocales,
5201
+ splitByNamespace
5202
+ );
4300
5203
  if (filesCreated.length > 0) {
4301
5204
  p.log.success(`next-intl configured: ${filesCreated.join(", ")}`);
4302
5205
  }
@@ -4307,9 +5210,19 @@ async function dropInlineComponents(cwd, componentPath, localeOpts) {
4307
5210
  await mkdir4(dir, { recursive: true });
4308
5211
  const clientFile = `${fsPath}.tsx`;
4309
5212
  const serverFile = `${fsPath}-server.tsx`;
4310
- const clientBasename = basename(fsPath);
5213
+ const clientBasename = basename2(fsPath);
5214
+ const serverDir = join6(serverFile, "..");
5215
+ const absoluteMessagesDir = join6(cwd, localeOpts.messagesDir);
5216
+ let relativeMessagesDir = relative(serverDir, absoluteMessagesDir);
5217
+ if (!relativeMessagesDir.startsWith(".")) {
5218
+ relativeMessagesDir = `./${relativeMessagesDir}`;
5219
+ }
4311
5220
  await writeFile5(clientFile, CLIENT_TEMPLATE, "utf-8");
4312
- await writeFile5(serverFile, serverTemplate(clientBasename, localeOpts), "utf-8");
5221
+ await writeFile5(
5222
+ serverFile,
5223
+ serverTemplate(clientBasename, { ...localeOpts, relativeMessagesDir }),
5224
+ "utf-8"
5225
+ );
4313
5226
  const relClient = relative(cwd, clientFile);
4314
5227
  const relServer = relative(cwd, serverFile);
4315
5228
  p.log.success(`Created inline components: ${relClient}, ${relServer}`);
@@ -4353,19 +5266,16 @@ import { getLocale, getMessages } from "@/i18n";
4353
5266
  /<\/body>/,
4354
5267
  " </I18nProvider>\n </body>"
4355
5268
  );
4356
- if (await safeWriteModifiedFile(
4357
- layoutPath,
4358
- layoutContent,
4359
- "root layout"
4360
- )) {
5269
+ if (await safeWriteModifiedFile(layoutPath, layoutContent, "root layout")) {
4361
5270
  filesCreated.push(relative(cwd, layoutPath) + " (updated)");
4362
5271
  }
4363
5272
  }
4364
5273
  }
4365
- await createEmptyMessageFiles(join6(cwd, messagesDir), [
4366
- sourceLocale,
4367
- ...targetLocales
4368
- ], splitByNamespace);
5274
+ await createEmptyMessageFiles(
5275
+ join6(cwd, messagesDir),
5276
+ [sourceLocale, ...targetLocales],
5277
+ splitByNamespace
5278
+ );
4369
5279
  if (filesCreated.length > 0) {
4370
5280
  p.log.success(`Inline i18n configured: ${filesCreated.join(", ")}`);
4371
5281
  }
@@ -4402,7 +5312,11 @@ async function updateLayoutWithSelectiveMessages(cwd, clientNamespaces) {
4402
5312
  "messages={clientMessages}"
4403
5313
  );
4404
5314
  }
4405
- if (await safeWriteModifiedFile(layoutPath, content, "root layout (selective messages)")) {
5315
+ if (await safeWriteModifiedFile(
5316
+ layoutPath,
5317
+ content,
5318
+ "root layout (selective messages)"
5319
+ )) {
4406
5320
  const rel = relative(cwd, layoutPath);
4407
5321
  p.log.success(
4408
5322
  `Updated ${rel} with selective message passing (${clientNamespaces.length} namespaces)`
@@ -4485,21 +5399,13 @@ async function runInitWizard() {
4485
5399
  splitByNamespace = split;
4486
5400
  }
4487
5401
  const detected = detectIncludePatterns(cwd);
4488
- let includePatterns;
4489
- const useDetected = await p.confirm({
4490
- message: `Detected: ${detected.join(", ")} \u2014 Use these patterns?`
5402
+ const patternsInput = await p.text({
5403
+ message: "Include patterns (comma-separated):",
5404
+ initialValue: detected.join(", "),
5405
+ placeholder: "app/**/*.tsx, components/**/*.tsx"
4491
5406
  });
4492
- if (p.isCancel(useDetected)) cancel2();
4493
- if (useDetected) {
4494
- includePatterns = detected;
4495
- } else {
4496
- const customPatterns = await p.text({
4497
- message: "Include patterns (comma-separated):",
4498
- initialValue: "src/**/*.tsx, src/**/*.jsx"
4499
- });
4500
- if (p.isCancel(customPatterns)) cancel2();
4501
- includePatterns = customPatterns.split(",").map((s) => s.trim());
4502
- }
5407
+ if (p.isCancel(patternsInput)) cancel2();
5408
+ const includePatterns = patternsInput.split(",").map((s) => s.trim());
4503
5409
  let i18nImport = "";
4504
5410
  let componentPath;
4505
5411
  let typeSafe = false;
@@ -4570,7 +5476,13 @@ async function runInitWizard() {
4570
5476
  splitByNamespace
4571
5477
  );
4572
5478
  } else {
4573
- await setupNextIntl(cwd, sourceLocale, targetLocales, messagesDir, splitByNamespace);
5479
+ await setupNextIntl(
5480
+ cwd,
5481
+ sourceLocale,
5482
+ targetLocales,
5483
+ messagesDir,
5484
+ splitByNamespace
5485
+ );
4574
5486
  }
4575
5487
  const runPipeline = await p.confirm({
4576
5488
  message: "Run the full pipeline now?"
@@ -4623,7 +5535,10 @@ async function runInitWizard() {
4623
5535
  `Codegen... ${codegenResult.stringsWrapped} strings wrapped in ${codegenResult.filesModified} files`
4624
5536
  );
4625
5537
  if (codegenResult.clientNamespaces.length > 0) {
4626
- await updateLayoutWithSelectiveMessages(cwd, codegenResult.clientNamespaces);
5538
+ await updateLayoutWithSelectiveMessages(
5539
+ cwd,
5540
+ codegenResult.clientNamespaces
5541
+ );
4627
5542
  }
4628
5543
  for (const locale of targetLocales) {
4629
5544
  const st = p.spinner();
@@ -4685,6 +5600,7 @@ var init_init = __esm({
4685
5600
  }
4686
5601
  };
4687
5602
  LOCALE_OPTIONS = [
5603
+ { value: "en", label: "English (en)" },
4688
5604
  { value: "es", label: "Spanish (es)" },
4689
5605
  { value: "fr", label: "French (fr)" },
4690
5606
  { value: "de", label: "German (de)" },
@@ -4946,6 +5862,11 @@ var codegenCommand = defineCommand({
4946
5862
  type: "boolean",
4947
5863
  description: "Show what would be changed without modifying files",
4948
5864
  default: false
5865
+ },
5866
+ "module-factory": {
5867
+ type: "boolean",
5868
+ description: "Convert module-level constants with translatable strings into factory functions",
5869
+ default: false
4949
5870
  }
4950
5871
  },
4951
5872
  async run({ args }) {
@@ -4989,6 +5910,7 @@ var codegenCommand = defineCommand({
4989
5910
  const result = await runCodegenStep({
4990
5911
  config,
4991
5912
  cwd: process.cwd(),
5913
+ moduleFactory: args["module-factory"],
4992
5914
  callbacks: {
4993
5915
  onProgress: (c, t3) => logProgress(c, t3, "Processing files...")
4994
5916
  }
@@ -5015,7 +5937,11 @@ var typegenCommand = defineCommand({
5015
5937
  logWarning("Type generation is only available in keys mode.");
5016
5938
  return;
5017
5939
  }
5018
- await generateNextIntlTypes(config.messagesDir, config.sourceLocale, config.splitByNamespace);
5940
+ await generateNextIntlTypes(
5941
+ config.messagesDir,
5942
+ config.sourceLocale,
5943
+ config.splitByNamespace
5944
+ );
5019
5945
  logSuccess(`Generated ${join7(config.messagesDir, "next-intl.d.ts")}`);
5020
5946
  }
5021
5947
  });
@@ -5035,6 +5961,11 @@ var runCommand = defineCommand({
5035
5961
  default: false,
5036
5962
  description: "Ignore translation cache"
5037
5963
  },
5964
+ "module-factory": {
5965
+ type: "boolean",
5966
+ description: "Convert module-level constants with translatable strings into factory functions",
5967
+ default: false
5968
+ },
5038
5969
  verbose: {
5039
5970
  type: "boolean",
5040
5971
  default: false,
@@ -5044,7 +5975,9 @@ var runCommand = defineCommand({
5044
5975
  async run({ args }) {
5045
5976
  const config = await loadTranslateKitConfig();
5046
5977
  if (!config.scan) {
5047
- logError("No scan configuration found. Add a 'scan' section to your config.");
5978
+ logError(
5979
+ "No scan configuration found. Add a 'scan' section to your config."
5980
+ );
5048
5981
  process.exit(1);
5049
5982
  }
5050
5983
  const usageTracker = createUsageTracker();
@@ -5059,7 +5992,9 @@ var runCommand = defineCommand({
5059
5992
  }
5060
5993
  });
5061
5994
  logProgressClear();
5062
- logSuccess(`Scan: ${scanResult.bareStringCount} strings from ${scanResult.fileCount} files`);
5995
+ logSuccess(
5996
+ `Scan: ${scanResult.bareStringCount} strings from ${scanResult.fileCount} files`
5997
+ );
5063
5998
  if (scanResult.bareStringCount === 0 && Object.keys(scanResult.textToKey).length === 0) {
5064
5999
  logWarning("No translatable strings found.");
5065
6000
  return;
@@ -5068,12 +6003,15 @@ var runCommand = defineCommand({
5068
6003
  config,
5069
6004
  cwd: process.cwd(),
5070
6005
  textToKey: scanResult.textToKey,
6006
+ moduleFactory: args["module-factory"],
5071
6007
  callbacks: {
5072
6008
  onProgress: (c, t3) => logProgress(c, t3, "Codegen...")
5073
6009
  }
5074
6010
  });
5075
6011
  logProgressClear();
5076
- logSuccess(`Codegen: ${codegenResult.stringsWrapped} strings wrapped in ${codegenResult.filesModified} files`);
6012
+ logSuccess(
6013
+ `Codegen: ${codegenResult.stringsWrapped} strings wrapped in ${codegenResult.filesModified} files`
6014
+ );
5077
6015
  const locales = config.targetLocales;
5078
6016
  logStart(config.sourceLocale, locales);
5079
6017
  const translateResult = await runTranslateStep({
@@ -5095,7 +6033,10 @@ var runCommand = defineCommand({
5095
6033
  const usage = usageTracker.get();
5096
6034
  if (usage.totalTokens > 0) {
5097
6035
  const cost = await estimateCost(config.model, usage);
5098
- logUsage(formatUsage(usage), cost ? formatCost(cost.totalUSD) : void 0);
6036
+ logUsage(
6037
+ formatUsage(usage),
6038
+ cost ? formatCost(cost.totalUSD) : void 0
6039
+ );
5099
6040
  }
5100
6041
  }
5101
6042
  });
@@ -5140,10 +6081,11 @@ var main = defineCommand({
5140
6081
  typegen Generate TypeScript types for message keys
5141
6082
 
5142
6083
  Flags:
5143
- --dry-run Preview without writing files
5144
- --force Ignore translation cache
5145
- --locale Only translate a specific locale
5146
- --verbose Verbose output
6084
+ --dry-run Preview without writing files
6085
+ --force Ignore translation cache
6086
+ --locale Only translate a specific locale
6087
+ --module-factory Convert module-level constants into factory functions
6088
+ --verbose Verbose output
5147
6089
 
5148
6090
  Examples:
5149
6091
  translate-kit init # Set up a new project