@babelize/engine 0.0.0-stage → 0.1.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.
Files changed (246) hide show
  1. package/LICENSE +202 -0
  2. package/dist/BabelizeEngine.d.ts +12 -0
  3. package/dist/BabelizeEngine.js +47 -0
  4. package/dist/analysis/analyze.d.ts +106 -0
  5. package/dist/analysis/analyze.js +323 -0
  6. package/dist/config/env.d.ts +9 -0
  7. package/dist/config/env.js +16 -0
  8. package/dist/config.d.ts +74 -0
  9. package/dist/config.js +47 -0
  10. package/dist/index.d.ts +54 -0
  11. package/dist/index.js +126 -0
  12. package/dist/injection/InjectionStrategy.d.ts +62 -0
  13. package/dist/injection/InjectionStrategy.js +2 -0
  14. package/dist/injection/StrategyFactory.d.ts +4 -0
  15. package/dist/injection/StrategyFactory.js +29 -0
  16. package/dist/injection/django/django-template-injector.d.ts +29 -0
  17. package/dist/injection/django/django-template-injector.js +216 -0
  18. package/dist/injection/html/html-injector.d.ts +11 -0
  19. package/dist/injection/html/html-injector.js +173 -0
  20. package/dist/injection/strategies/NextIntlStrategy.d.ts +25 -0
  21. package/dist/injection/strategies/NextIntlStrategy.js +141 -0
  22. package/dist/injection/strategies/ReactI18nextStrategy.d.ts +19 -0
  23. package/dist/injection/strategies/ReactI18nextStrategy.js +116 -0
  24. package/dist/injection/strategies/VanillaStrategy.d.ts +11 -0
  25. package/dist/injection/strategies/VanillaStrategy.js +73 -0
  26. package/dist/injection/strategies/VueI18nStrategy.d.ts +11 -0
  27. package/dist/injection/strategies/VueI18nStrategy.js +84 -0
  28. package/dist/local/formatter.d.ts +26 -0
  29. package/dist/local/formatter.js +105 -0
  30. package/dist/local/git.d.ts +23 -0
  31. package/dist/local/git.js +136 -0
  32. package/dist/local/hosted-provider.d.ts +68 -0
  33. package/dist/local/hosted-provider.js +215 -0
  34. package/dist/local/index.d.ts +23 -0
  35. package/dist/local/index.js +75 -0
  36. package/dist/local/provider.d.ts +56 -0
  37. package/dist/local/provider.js +119 -0
  38. package/dist/local/pseudo-provider.d.ts +18 -0
  39. package/dist/local/pseudo-provider.js +207 -0
  40. package/dist/local/report.d.ts +177 -0
  41. package/dist/local/report.js +186 -0
  42. package/dist/local/translate-core.d.ts +215 -0
  43. package/dist/local/translate-core.js +461 -0
  44. package/dist/local/translate-messages.d.ts +49 -0
  45. package/dist/local/translate-messages.js +91 -0
  46. package/dist/local/workspace-copy.d.ts +91 -0
  47. package/dist/local/workspace-copy.js +355 -0
  48. package/dist/modules/extractor/extract.d.ts +20 -0
  49. package/dist/modules/extractor/extract.js +701 -0
  50. package/dist/modules/extractor/index.d.ts +2 -0
  51. package/dist/modules/extractor/index.js +63 -0
  52. package/dist/modules/finalization/django-finalize.d.ts +13 -0
  53. package/dist/modules/finalization/django-finalize.js +275 -0
  54. package/dist/modules/finalization/finalize.d.ts +36 -0
  55. package/dist/modules/finalization/finalize.js +717 -0
  56. package/dist/modules/finalization/index.d.ts +1 -0
  57. package/dist/modules/finalization/index.js +5 -0
  58. package/dist/modules/finalization/nextjs-finalize.d.ts +37 -0
  59. package/dist/modules/finalization/nextjs-finalize.js +329 -0
  60. package/dist/modules/finalization/static-runtime.d.ts +9 -0
  61. package/dist/modules/finalization/static-runtime.js +261 -0
  62. package/dist/modules/format-injection/index.d.ts +19 -0
  63. package/dist/modules/format-injection/index.js +272 -0
  64. package/dist/modules/format-injection/inject-formats.d.ts +31 -0
  65. package/dist/modules/format-injection/inject-formats.js +208 -0
  66. package/dist/modules/glossary/glossary.d.ts +9 -0
  67. package/dist/modules/glossary/glossary.js +27 -0
  68. package/dist/modules/glossary/index.d.ts +3 -0
  69. package/dist/modules/glossary/index.js +8 -0
  70. package/dist/modules/glossary/masker.d.ts +23 -0
  71. package/dist/modules/glossary/masker.js +56 -0
  72. package/dist/modules/glossary/types.d.ts +22 -0
  73. package/dist/modules/glossary/types.js +2 -0
  74. package/dist/modules/injection/index.d.ts +1 -0
  75. package/dist/modules/injection/index.js +6 -0
  76. package/dist/modules/injection/inject.d.ts +29 -0
  77. package/dist/modules/injection/inject.js +1066 -0
  78. package/dist/modules/keygen/index.d.ts +1 -0
  79. package/dist/modules/keygen/index.js +11 -0
  80. package/dist/modules/keygen/keygen.d.ts +4 -0
  81. package/dist/modules/keygen/keygen.js +116 -0
  82. package/dist/modules/keygen/locale-data.d.ts +7 -0
  83. package/dist/modules/keygen/locale-data.js +73 -0
  84. package/dist/modules/keygen/public.d.ts +2 -0
  85. package/dist/modules/keygen/public.js +12 -0
  86. package/dist/modules/locale-builder/index.d.ts +1 -0
  87. package/dist/modules/locale-builder/index.js +7 -0
  88. package/dist/modules/locale-builder/locales.d.ts +13 -0
  89. package/dist/modules/locale-builder/locales.js +107 -0
  90. package/dist/modules/locale-builder/po.d.ts +33 -0
  91. package/dist/modules/locale-builder/po.js +131 -0
  92. package/dist/modules/lockfile/hasher.d.ts +7 -0
  93. package/dist/modules/lockfile/hasher.js +13 -0
  94. package/dist/modules/lockfile/index.d.ts +3 -0
  95. package/dist/modules/lockfile/index.js +21 -0
  96. package/dist/modules/lockfile/lockfile-manager.d.ts +90 -0
  97. package/dist/modules/lockfile/lockfile-manager.js +482 -0
  98. package/dist/modules/lockfile/schema.d.ts +154 -0
  99. package/dist/modules/lockfile/schema.js +45 -0
  100. package/dist/modules/parser/index.d.ts +2 -0
  101. package/dist/modules/parser/index.js +9 -0
  102. package/dist/modules/summary/index.d.ts +1 -0
  103. package/dist/modules/summary/index.js +7 -0
  104. package/dist/modules/summary/summary.d.ts +9 -0
  105. package/dist/modules/summary/summary.js +195 -0
  106. package/dist/modules/sync-detector/index.d.ts +2 -0
  107. package/dist/modules/sync-detector/index.js +7 -0
  108. package/dist/modules/sync-detector/reporter.d.ts +3 -0
  109. package/dist/modules/sync-detector/reporter.js +16 -0
  110. package/dist/modules/sync-detector/sync-detector.d.ts +39 -0
  111. package/dist/modules/sync-detector/sync-detector.js +374 -0
  112. package/dist/modules/translator/batch-optimizer.d.ts +12 -0
  113. package/dist/modules/translator/batch-optimizer.js +30 -0
  114. package/dist/modules/translator/client/byok.provider.d.ts +64 -0
  115. package/dist/modules/translator/client/byok.provider.js +319 -0
  116. package/dist/modules/translator/client/direct-translator.client.d.ts +12 -0
  117. package/dist/modules/translator/client/direct-translator.client.js +71 -0
  118. package/dist/modules/translator/client/gemini.provider.d.ts +1 -0
  119. package/dist/modules/translator/client/gemini.provider.js +110 -0
  120. package/dist/modules/translator/client/groq.provider.d.ts +1 -0
  121. package/dist/modules/translator/client/groq.provider.js +111 -0
  122. package/dist/modules/translator/client/request.types.d.ts +1 -0
  123. package/dist/modules/translator/client/request.types.js +2 -0
  124. package/dist/modules/translator/client/retry.d.ts +29 -0
  125. package/dist/modules/translator/client/retry.js +94 -0
  126. package/dist/modules/translator/client/test-utils/mock.client.d.ts +31 -0
  127. package/dist/modules/translator/client/test-utils/mock.client.js +59 -0
  128. package/dist/modules/translator/client/translator.interface.d.ts +1 -0
  129. package/dist/modules/translator/client/translator.interface.js +2 -0
  130. package/dist/modules/translator/language-codes/errors.d.ts +10 -0
  131. package/dist/modules/translator/language-codes/errors.js +18 -0
  132. package/dist/modules/translator/language-codes/flores-map.d.ts +1 -0
  133. package/dist/modules/translator/language-codes/flores-map.js +107 -0
  134. package/dist/modules/translator/language-codes/index.d.ts +11 -0
  135. package/dist/modules/translator/language-codes/index.js +24 -0
  136. package/dist/modules/translator/language-codes/language-mapper.d.ts +25 -0
  137. package/dist/modules/translator/language-codes/language-mapper.js +132 -0
  138. package/dist/modules/translator/language-codes/rtl-languages.d.ts +2 -0
  139. package/dist/modules/translator/language-codes/rtl-languages.js +9 -0
  140. package/dist/modules/translator/memory/memory-matcher.d.ts +8 -0
  141. package/dist/modules/translator/memory/memory-matcher.js +218 -0
  142. package/dist/modules/translator/memory/types.d.ts +22 -0
  143. package/dist/modules/translator/memory/types.js +2 -0
  144. package/dist/modules/translator/translate.d.ts +38 -0
  145. package/dist/modules/translator/translate.js +932 -0
  146. package/dist/modules/translator/translator.d.ts +5 -0
  147. package/dist/modules/translator/translator.js +46 -0
  148. package/dist/modules/vue/migrate.d.ts +13 -0
  149. package/dist/modules/vue/migrate.js +40 -0
  150. package/dist/modules/vue/script-inject.d.ts +16 -0
  151. package/dist/modules/vue/script-inject.js +115 -0
  152. package/dist/modules/vue/sfc.d.ts +11 -0
  153. package/dist/modules/vue/sfc.js +51 -0
  154. package/dist/modules/vue/template-rewrite.d.ts +25 -0
  155. package/dist/modules/vue/template-rewrite.js +76 -0
  156. package/dist/modules/vue/template-scan.d.ts +29 -0
  157. package/dist/modules/vue/template-scan.js +264 -0
  158. package/dist/modules/workspace/index.d.ts +1 -0
  159. package/dist/modules/workspace/index.js +5 -0
  160. package/dist/modules/workspace/validate.d.ts +13 -0
  161. package/dist/modules/workspace/validate.js +298 -0
  162. package/dist/orchestrator.d.ts +8 -0
  163. package/dist/orchestrator.js +616 -0
  164. package/dist/providers/default-translation-provider.d.ts +20 -0
  165. package/dist/providers/default-translation-provider.js +30 -0
  166. package/dist/providers/index.d.ts +2 -0
  167. package/dist/providers/index.js +21 -0
  168. package/dist/providers/types.d.ts +145 -0
  169. package/dist/providers/types.js +2 -0
  170. package/dist/qa/check-project.d.ts +49 -0
  171. package/dist/qa/check-project.js +150 -0
  172. package/dist/qa/locale-files.d.ts +44 -0
  173. package/dist/qa/locale-files.js +159 -0
  174. package/dist/qa/locale-qa.d.ts +61 -0
  175. package/dist/qa/locale-qa.js +325 -0
  176. package/dist/qa/review.d.ts +43 -0
  177. package/dist/qa/review.js +175 -0
  178. package/dist/runtime/config.d.ts +8 -0
  179. package/dist/runtime/config.js +36 -0
  180. package/dist/runtime/index.d.ts +3 -0
  181. package/dist/runtime/index.js +8 -0
  182. package/dist/runtime/types.d.ts +363 -0
  183. package/dist/runtime/types.js +38 -0
  184. package/dist/types.d.ts +134 -0
  185. package/dist/types.js +12 -0
  186. package/dist/utils/ast-helpers.d.ts +57 -0
  187. package/dist/utils/ast-helpers.js +608 -0
  188. package/dist/utils/cache-keys.d.ts +8 -0
  189. package/dist/utils/cache-keys.js +18 -0
  190. package/dist/utils/circuit-breaker.d.ts +43 -0
  191. package/dist/utils/circuit-breaker.js +119 -0
  192. package/dist/utils/concurrent-map.d.ts +4 -0
  193. package/dist/utils/concurrent-map.js +21 -0
  194. package/dist/utils/detect-format.d.ts +5 -0
  195. package/dist/utils/detect-format.js +125 -0
  196. package/dist/utils/dtl-mask.d.ts +25 -0
  197. package/dist/utils/dtl-mask.js +58 -0
  198. package/dist/utils/extraction-options.d.ts +23 -0
  199. package/dist/utils/extraction-options.js +23 -0
  200. package/dist/utils/file-utils.d.ts +33 -0
  201. package/dist/utils/file-utils.js +262 -0
  202. package/dist/utils/glossary-match.d.ts +9 -0
  203. package/dist/utils/glossary-match.js +25 -0
  204. package/dist/utils/html-extractor.d.ts +8 -0
  205. package/dist/utils/html-extractor.js +317 -0
  206. package/dist/utils/i18next-icu.d.ts +38 -0
  207. package/dist/utils/i18next-icu.js +159 -0
  208. package/dist/utils/icu-message.d.ts +65 -0
  209. package/dist/utils/icu-message.js +346 -0
  210. package/dist/utils/icu-spans.d.ts +10 -0
  211. package/dist/utils/icu-spans.js +126 -0
  212. package/dist/utils/jsx-message.d.ts +93 -0
  213. package/dist/utils/jsx-message.js +621 -0
  214. package/dist/utils/key-usage.d.ts +35 -0
  215. package/dist/utils/key-usage.js +247 -0
  216. package/dist/utils/logger.d.ts +24 -0
  217. package/dist/utils/logger.js +33 -0
  218. package/dist/utils/metadata.d.ts +80 -0
  219. package/dist/utils/metadata.js +255 -0
  220. package/dist/utils/module-constants.d.ts +30 -0
  221. package/dist/utils/module-constants.js +265 -0
  222. package/dist/utils/not-injected.d.ts +6 -0
  223. package/dist/utils/not-injected.js +36 -0
  224. package/dist/utils/placeholder-protection.d.ts +57 -0
  225. package/dist/utils/placeholder-protection.js +136 -0
  226. package/dist/utils/pseudo-markers.d.ts +9 -0
  227. package/dist/utils/pseudo-markers.js +16 -0
  228. package/dist/utils/py-extractor.d.ts +14 -0
  229. package/dist/utils/py-extractor.js +285 -0
  230. package/dist/utils/string-utils.d.ts +52 -0
  231. package/dist/utils/string-utils.js +270 -0
  232. package/dist/utils/translation-quality.d.ts +34 -0
  233. package/dist/utils/translation-quality.js +156 -0
  234. package/dist/utils/translation-sanitizer.d.ts +10 -0
  235. package/dist/utils/translation-sanitizer.js +57 -0
  236. package/dist/utils/translator-caller.d.ts +32 -0
  237. package/dist/utils/translator-caller.js +217 -0
  238. package/dist/utils/translator-load-balancer.d.ts +73 -0
  239. package/dist/utils/translator-load-balancer.js +293 -0
  240. package/dist/utils/ui-copy.d.ts +32 -0
  241. package/dist/utils/ui-copy.js +126 -0
  242. package/dist/utils/workspace.d.ts +14 -0
  243. package/dist/utils/workspace.js +89 -0
  244. package/package.json +64 -4
  245. package/schemas/translate-report.schema.json +297 -0
  246. package/README.md +0 -3
@@ -0,0 +1,1066 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.injectTranslations = injectTranslations;
37
+ exports.injectIntoFile = injectIntoFile;
38
+ exports.injectIntoVueFile = injectIntoVueFile;
39
+ exports.checkIdempotency = checkIdempotency;
40
+ const path = __importStar(require("path"));
41
+ const types_1 = require("@babelize/types");
42
+ const StrategyFactory_1 = require("../../injection/StrategyFactory");
43
+ const jsx_message_1 = require("../../utils/jsx-message");
44
+ const module_constants_1 = require("../../utils/module-constants");
45
+ const html_injector_1 = require("../../injection/html/html-injector");
46
+ const django_template_injector_1 = require("../../injection/django/django-template-injector");
47
+ const dtl_mask_1 = require("../../utils/dtl-mask");
48
+ const types_2 = require("../../runtime/types");
49
+ const file_utils_1 = require("../../utils/file-utils");
50
+ const string_utils_1 = require("../../utils/string-utils");
51
+ const ast_helpers_1 = require("../../utils/ast-helpers");
52
+ /**
53
+ * An arrow function that defines a component: assigned to a capitalized name
54
+ * (`const Button = …`), possibly through memo()/forwardRef(), or the default
55
+ * export. Inline callbacks (`items.map(i => <li/>)`) are not components.
56
+ */
57
+ function isComponentDefinition(arrow) {
58
+ let parent = arrow.parentPath;
59
+ while (parent?.isCallExpression() &&
60
+ /^(?:React\.)?(?:memo|forwardRef)$/.test(ast_helpers_1.t.isMemberExpression(parent.node.callee) && ast_helpers_1.t.isIdentifier(parent.node.callee.property)
61
+ ? `React.${parent.node.callee.property.name}`
62
+ : ast_helpers_1.t.isIdentifier(parent.node.callee) ? parent.node.callee.name : '')) {
63
+ parent = parent.parentPath;
64
+ }
65
+ if (!parent)
66
+ return false;
67
+ if (parent.isExportDefaultDeclaration())
68
+ return true;
69
+ return parent.isVariableDeclarator() && ast_helpers_1.t.isIdentifier(parent.node.id) && /^[A-Z]/.test(parent.node.id.name);
70
+ }
71
+ function importSignature(decl) {
72
+ const names = decl.specifiers.map((sp) => (ast_helpers_1.t.isImportSpecifier(sp) && ast_helpers_1.t.isIdentifier(sp.imported) ? sp.imported.name : sp.local.name));
73
+ return `${decl.source.value}|${names.sort().join(',')}`;
74
+ }
75
+ /** True when every specifier of `decl` is already imported from the same module. */
76
+ function hasImportSpecifiers(ast, decl) {
77
+ const wanted = decl.specifiers.map((sp) => sp.local.name);
78
+ const have = new Set();
79
+ for (const node of ast.program.body) {
80
+ if (ast_helpers_1.t.isImportDeclaration(node) && node.source.value === decl.source.value) {
81
+ for (const sp of node.specifiers)
82
+ have.add(sp.local.name);
83
+ }
84
+ }
85
+ return wanted.length > 0 ? wanted.every((n) => have.has(n)) : have.size > 0;
86
+ }
87
+ /** Make sure `name` is imported from `source` as `local`, adding to an existing import of that module if there is one. */
88
+ function ensureNamedImport(ast, source, name, local = name) {
89
+ const decls = ast.program.body.filter((n) => ast_helpers_1.t.isImportDeclaration(n) && n.source.value === source && n.importKind !== 'type');
90
+ for (const decl of decls) {
91
+ if (decl.specifiers.some((sp) => ast_helpers_1.t.isImportSpecifier(sp) && ast_helpers_1.t.isIdentifier(sp.imported) && sp.imported.name === name && sp.local.name === local))
92
+ return false;
93
+ }
94
+ const specifier = ast_helpers_1.t.importSpecifier(ast_helpers_1.t.identifier(local), ast_helpers_1.t.identifier(name));
95
+ const target = decls.find((d) => !d.specifiers.some((sp) => ast_helpers_1.t.isImportNamespaceSpecifier(sp)));
96
+ if (target) {
97
+ target.specifiers.push(specifier);
98
+ }
99
+ else {
100
+ ast.program.body.unshift(ast_helpers_1.t.importDeclaration([specifier], ast_helpers_1.t.stringLiteral(source)));
101
+ }
102
+ return true;
103
+ }
104
+ /**
105
+ * The local name to use for react-i18next's Trans: the one already imported
106
+ * from react-i18next, else `Trans` — or `BabelizeTrans` when the file
107
+ * already uses the name `Trans` for something else (next-i18next, Lingui, a
108
+ * local component), which a second `Trans` import would redeclare.
109
+ */
110
+ function transLocalName(ast, content) {
111
+ for (const node of ast.program.body) {
112
+ if (!ast_helpers_1.t.isImportDeclaration(node) || node.source.value !== 'react-i18next')
113
+ continue;
114
+ for (const sp of node.specifiers) {
115
+ if (ast_helpers_1.t.isImportSpecifier(sp) && ast_helpers_1.t.isIdentifier(sp.imported) && sp.imported.name === 'Trans')
116
+ return sp.local.name;
117
+ }
118
+ }
119
+ return /\bTrans\b/.test(content) ? 'BabelizeTrans' : 'Trans';
120
+ }
121
+ /**
122
+ * `<p> {t(…)} </p>`: keep the spaces at the edges of the children that JSX
123
+ * renders (no line break), as the fragment path does.
124
+ */
125
+ function withEdgeSpaces(children, node) {
126
+ const first = children[0];
127
+ const last = children[children.length - 1];
128
+ const lead = ast_helpers_1.t.isJSXText(first) && /^[ \t]+/.test(first.value) && !/^[ \t]*[\r\n]/.test(first.value);
129
+ const trail = ast_helpers_1.t.isJSXText(last) && /[ \t]+$/.test(last.value) && !/[\r\n][ \t]*$/.test(last.value);
130
+ return [...(lead ? [ast_helpers_1.t.jsxText(' ')] : []), node, ...(trail ? [ast_helpers_1.t.jsxText(' ')] : [])];
131
+ }
132
+ const HOOK_CALLEES = new Set(['useTranslations', 'getTranslations', 'useTranslation']);
133
+ /** `const t = useTranslations()`, `const t = await getTranslations()`, `const { t } = useTranslation()`. */
134
+ function isTranslationHookDeclarator(path) {
135
+ if (!path.isVariableDeclarator())
136
+ return false;
137
+ let init = path.node.init;
138
+ if (ast_helpers_1.t.isAwaitExpression(init))
139
+ init = init.argument;
140
+ return ast_helpers_1.t.isCallExpression(init) && ast_helpers_1.t.isIdentifier(init.callee) && HOOK_CALLEES.has(init.callee.name);
141
+ }
142
+ /**
143
+ * Name for the translation function in a component. Normally `t`; when the
144
+ * component (or a callback inside it, e.g. `items.map((t) => …)`) binds `t`
145
+ * to something else, an injected `t("key")` there would call that value, so
146
+ * the component gets its own hook under a free name (`tr`, `tr2`, …).
147
+ */
148
+ function translatorNameFor(body) {
149
+ const fn = body.parentPath;
150
+ if (!fn)
151
+ return 't';
152
+ const names = new Set();
153
+ let shadowed = false;
154
+ const visitScope = (scope) => {
155
+ for (const [name, binding] of Object.entries(scope.bindings)) {
156
+ names.add(name);
157
+ if (name === 't' && !isTranslationHookDeclarator(binding.path))
158
+ shadowed = true;
159
+ }
160
+ };
161
+ visitScope(fn.scope);
162
+ fn.traverse({
163
+ Scopable(p) {
164
+ visitScope(p.scope);
165
+ },
166
+ // References to outer names count too: the alias must not hide them.
167
+ Identifier(p) {
168
+ names.add(p.node.name);
169
+ },
170
+ });
171
+ if (!shadowed)
172
+ return 't';
173
+ // A hook added under an alias by an earlier run: reuse it.
174
+ for (const [name, binding] of Object.entries(fn.scope.bindings)) {
175
+ if (name !== 't' && /^tr\d*$/.test(name) && isTranslationHookDeclarator(binding.path))
176
+ return name;
177
+ }
178
+ let alias = 'tr';
179
+ for (let n = 2; names.has(alias) || fn.scope.hasBinding(alias); n++)
180
+ alias = `tr${n}`;
181
+ return alias;
182
+ }
183
+ /** Point an injected `t(…)` / `t.rich(…)` / `<Trans t={t}>` at `name` (user expressions inside are left alone). */
184
+ function useTranslatorName(node, name) {
185
+ if (name === 't')
186
+ return node;
187
+ const call = ast_helpers_1.t.isJSXExpressionContainer(node) ? node.expression : node;
188
+ if (ast_helpers_1.t.isCallExpression(call)) {
189
+ if (ast_helpers_1.t.isIdentifier(call.callee, { name: 't' }))
190
+ call.callee = ast_helpers_1.t.identifier(name);
191
+ else if (ast_helpers_1.t.isMemberExpression(call.callee) && ast_helpers_1.t.isIdentifier(call.callee.object, { name: 't' }))
192
+ call.callee.object = ast_helpers_1.t.identifier(name);
193
+ }
194
+ else if (ast_helpers_1.t.isJSXElement(node)) {
195
+ for (const attr of node.openingElement.attributes) {
196
+ if (ast_helpers_1.t.isJSXAttribute(attr) && ast_helpers_1.t.isJSXIdentifier(attr.name, { name: 't' }) && ast_helpers_1.t.isJSXExpressionContainer(attr.value) && ast_helpers_1.t.isIdentifier(attr.value.expression, { name: 't' })) {
197
+ attr.value.expression = ast_helpers_1.t.identifier(name);
198
+ }
199
+ }
200
+ }
201
+ return node;
202
+ }
203
+ /** The hook statements, declaring the translator as `name` instead of `t`. */
204
+ function renameHookBinding(statements, name) {
205
+ if (name === 't')
206
+ return statements;
207
+ for (const st of statements) {
208
+ if (!ast_helpers_1.t.isVariableDeclaration(st))
209
+ continue;
210
+ for (const decl of st.declarations) {
211
+ if (ast_helpers_1.t.isIdentifier(decl.id, { name: 't' }))
212
+ decl.id = ast_helpers_1.t.identifier(name);
213
+ else if (ast_helpers_1.t.isObjectPattern(decl.id)) {
214
+ for (const prop of decl.id.properties) {
215
+ if (ast_helpers_1.t.isObjectProperty(prop) && ast_helpers_1.t.isIdentifier(prop.key, { name: 't' })) {
216
+ prop.value = ast_helpers_1.t.identifier(name);
217
+ prop.shorthand = false;
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ return statements;
224
+ }
225
+ /** The strategy's rendering of a sentence, or plain `t(key, values)` when it has no rich-text API. */
226
+ function renderMessage(strategy, key, message, options) {
227
+ if (strategy.createMessage)
228
+ return strategy.createMessage(key, message, options);
229
+ if ((0, jsx_message_1.hasTags)(message))
230
+ return null;
231
+ return { node: ast_helpers_1.t.jsxExpressionContainer((0, jsx_message_1.createMessageTCall)(key, message)) };
232
+ }
233
+ /**
234
+ * Replace a JSXText node with `{t(...)}` without changing the rendered
235
+ * spacing. In JSX, whitespace at the edge of a text node is significant when
236
+ * it doesn't contain a line break: `<p>You clicked {count} times</p>` renders
237
+ * "You clicked 0 times". Replacing the text with a bare t() call would drop
238
+ * those spaces ("You clicked0times"), so they are kept as their own text node.
239
+ */
240
+ function replaceJSXTextKeepingSpaces(path, tCall) {
241
+ const raw = path.node.value;
242
+ const leading = raw.match(/^\s*/)?.[0] ?? '';
243
+ const trailing = raw.match(/\s*$/)?.[0] ?? '';
244
+ const keepLeading = leading.length > 0 && !leading.includes('\n');
245
+ const keepTrailing = trailing.length > 0 && !trailing.includes('\n') && trailing.length < raw.length;
246
+ if (!keepLeading && !keepTrailing) {
247
+ path.replaceWith(tCall);
248
+ return;
249
+ }
250
+ const nodes = [];
251
+ if (keepLeading)
252
+ nodes.push(ast_helpers_1.t.jsxText(' '));
253
+ nodes.push(tCall);
254
+ if (keepTrailing)
255
+ nodes.push(ast_helpers_1.t.jsxText(' '));
256
+ path.replaceWithMultiple(nodes);
257
+ }
258
+ /**
259
+ * Phase 5: Injection
260
+ *
261
+ * Injects i18n into source files — JS/TS gets Babel AST injection,
262
+ * HTML files get DOM-based data-i18n attribute injection.
263
+ */
264
+ async function injectTranslations(workspace, keyMapping, // extractionId -> key
265
+ confidenceThreshold = 80, dryRun = false,
266
+ /** extractionId → source offsets of the nodes extraction selected. */
267
+ positions) {
268
+ const modifiedFiles = [];
269
+ const skippedFiles = [];
270
+ let totalInjections = 0;
271
+ // Find all source files
272
+ const frameworkConfig = types_1.FRAMEWORK_CONFIGS[workspace.framework] || types_1.FRAMEWORK_CONFIGS['unknown'];
273
+ const extensions = [...(frameworkConfig.extensions || types_2.SUPPORTED_EXTENSIONS)];
274
+ const sourceFiles = [];
275
+ for (const sourceDir of workspace.sourceDirs) {
276
+ const dirPath = path.join(workspace.path, sourceDir);
277
+ const files = await (0, file_utils_1.findFiles)(dirPath, extensions, types_2.EXCLUDED_PATTERNS);
278
+ sourceFiles.push(...files);
279
+ }
280
+ // Collect keys by file for efficient lookup
281
+ const keysByFile = new Map();
282
+ Array.from(keyMapping.entries()).forEach(([extractionId, key]) => {
283
+ const [filePath] = extractionId.split(':');
284
+ if (!keysByFile.has(filePath)) {
285
+ keysByFile.set(filePath, new Map());
286
+ }
287
+ keysByFile.get(filePath).set(extractionId, key);
288
+ });
289
+ // Process each file that has keys
290
+ for (const filePath of sourceFiles) {
291
+ const relativePath = (0, file_utils_1.getRelativePath)(workspace.path, filePath);
292
+ const fileKeys = keysByFile.get(relativePath);
293
+ if (!fileKeys || fileKeys.size === 0) {
294
+ continue; // No keys for this file
295
+ }
296
+ try {
297
+ const originalContent = await (0, file_utils_1.readFile)(filePath);
298
+ let result;
299
+ if (relativePath.endsWith('.html') || relativePath.endsWith('.htm')) {
300
+ if (workspace.framework === 'django') {
301
+ // Django: mask tags, run HTML injection, convert data-i18n to {% trans %}
302
+ const { masked, restore } = (0, dtl_mask_1.maskTemplateTags)(originalContent);
303
+ const maskedResult = await (0, html_injector_1.injectHtml)({
304
+ content: masked,
305
+ filePath: relativePath,
306
+ keyMapping: fileKeys,
307
+ injectSwitcher: false, // Django doesn't use JS switcher
308
+ });
309
+ result = await (0, django_template_injector_1.injectDjangoTemplates)({
310
+ maskedContent: maskedResult.modifiedContent,
311
+ restore,
312
+ filePath: relativePath,
313
+ keyMapping: fileKeys,
314
+ sourceContent: originalContent,
315
+ });
316
+ }
317
+ else {
318
+ result = await (0, html_injector_1.injectHtml)({
319
+ content: originalContent,
320
+ filePath: relativePath,
321
+ keyMapping: fileKeys,
322
+ injectSwitcher: workspace.framework === 'static',
323
+ targetLocales: workspace.existingLocales,
324
+ sourceLocale: workspace.sourceDirs[0],
325
+ });
326
+ }
327
+ }
328
+ else if (relativePath.endsWith('.vue')) {
329
+ const existingDeps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
330
+ result = await injectIntoVueFile(originalContent, relativePath, fileKeys, confidenceThreshold, workspace.framework, existingDeps);
331
+ }
332
+ else {
333
+ const existingDeps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
334
+ result = injectIntoFile(originalContent, relativePath, fileKeys, confidenceThreshold, workspace.framework, existingDeps, undefined, positions);
335
+ }
336
+ if (result.injections.length > 0) {
337
+ modifiedFiles.push(result);
338
+ totalInjections += result.injections.length;
339
+ // Write modified file
340
+ if (!dryRun) {
341
+ await (0, file_utils_1.writeFile)(filePath, result.modifiedContent);
342
+ }
343
+ }
344
+ }
345
+ catch (error) {
346
+ skippedFiles.push({
347
+ file: relativePath,
348
+ reason: 'parse-error',
349
+ details: error instanceof Error ? error.message : String(error),
350
+ });
351
+ }
352
+ }
353
+ return {
354
+ modifiedFiles,
355
+ skippedFiles,
356
+ totalInjections,
357
+ };
358
+ }
359
+ const ALLOWED_PARENT_TYPES = [
360
+ 'ObjectProperty',
361
+ 'ArrayExpression',
362
+ 'VariableDeclarator',
363
+ 'AssignmentExpression',
364
+ 'JSXExpressionContainer',
365
+ 'ReturnStatement',
366
+ 'ConditionalExpression',
367
+ 'LogicalExpression',
368
+ 'BinaryExpression',
369
+ 'CallExpression'
370
+ ];
371
+ /**
372
+ * Inject translations into a single file
373
+ */
374
+ function injectIntoFile(content, filePath, keyMapping, confidenceThreshold, framework, existingDeps, forceTypeScript,
375
+ /** extractionId → source offsets of the nodes extraction selected (see ExtractedString.offsets). */
376
+ positions) {
377
+ const injections = [];
378
+ let importAdded = false;
379
+ let hookAdded = false;
380
+ let needsHook = false;
381
+ // Skip minified third-party libraries that have no existing imports
382
+ if (filePath.endsWith('.js') || filePath.endsWith('.ts') || filePath.endsWith('.jsx') || filePath.endsWith('.tsx')) {
383
+ const hasImports = /\b(import|export)\b/.test(content);
384
+ const isMinified = content.split('\n').length <= 3 && content.length > 5000;
385
+ if (!hasImports && isMinified) {
386
+ return { file: filePath, originalContent: content, modifiedContent: content, injections, importAdded, hookAdded: false };
387
+ }
388
+ }
389
+ const isTypeScript = forceTypeScript || filePath.endsWith('.ts') || filePath.endsWith('.tsx');
390
+ const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript });
391
+ // Track components that need the hook
392
+ const componentsNeedingHook = new Set();
393
+ let activeStrategy = StrategyFactory_1.StrategyFactory.getStrategy(framework, existingDeps);
394
+ let hasNonComponentInjections = false;
395
+ // Concise-body arrow components (`const Button = () => <button …/>`) have no
396
+ // body to put the translation hook in. Give the ones that hold a string to
397
+ // inject a block body (`=> { return …; }`) so they can be wired like any
398
+ // other component instead of being silently skipped.
399
+ // Only the nodes extraction picked for an id (by source offset), never
400
+ // another node that happens to have the same text on the same line.
401
+ const selected = (id, node) => {
402
+ const starts = positions?.get(id);
403
+ return !starts || (typeof node.start === 'number' && starts.has(node.start));
404
+ };
405
+ const hasKey = (text, node) => {
406
+ const id = `${filePath}:${node.loc?.start.line || 0}:${(0, string_utils_1.hashString)(text)}`;
407
+ return !!text && keyMapping.has(id) && selected(id, node);
408
+ };
409
+ // A JSX sentence extracted as one message: same message, same id as the extractor.
410
+ const sentenceFor = (p) => {
411
+ if (p.node.children.length < 2 || (0, ast_helpers_1.isAlreadyTranslated)(p.node, p))
412
+ return null;
413
+ const line = p.node.loc?.start.line || 0;
414
+ // The extractor generates plurals only with an LLM translator; try both forms.
415
+ for (const plurals of [true, false]) {
416
+ const built = (0, jsx_message_1.buildJsxMessage)(p.node, { plurals });
417
+ if (built.kind !== 'message')
418
+ return null;
419
+ const id = `${filePath}:${line}:${(0, string_utils_1.hashString)(built.message.text)}`;
420
+ const key = keyMapping.get(id);
421
+ if (key && selected(id, p.node))
422
+ return { key, message: built.message, line };
423
+ if (!built.message.plural)
424
+ return null;
425
+ }
426
+ return null;
427
+ };
428
+ (0, ast_helpers_1.traverse)(ast, {
429
+ ArrowFunctionExpression(arrow) {
430
+ if (ast_helpers_1.t.isBlockStatement(arrow.node.body) || !isComponentDefinition(arrow))
431
+ return;
432
+ let needsHook = false;
433
+ arrow.traverse({
434
+ 'JSXElement|JSXFragment'(visited) {
435
+ const p = visited;
436
+ if (sentenceFor(p))
437
+ needsHook = true;
438
+ },
439
+ JSXText(p) {
440
+ if (hasKey((0, ast_helpers_1.extractJSXTextContent)(p.node), p.node))
441
+ needsHook = true;
442
+ },
443
+ JSXAttribute(p) {
444
+ const value = p.node.value;
445
+ if (ast_helpers_1.t.isStringLiteral(value) && hasKey(value.value.trim(), p.node))
446
+ needsHook = true;
447
+ },
448
+ StringLiteral(p) {
449
+ if (!p.parentPath.isJSXAttribute() && hasKey(p.node.value.trim(), p.node))
450
+ needsHook = true;
451
+ },
452
+ });
453
+ if (needsHook) {
454
+ const ensureBlock = arrow.ensureBlock.bind(arrow);
455
+ ensureBlock();
456
+ }
457
+ },
458
+ });
459
+ // Module-level UI constants rendered by components (#248): when extraction
460
+ // keyed their strings (every use is a render), the strings become keys and
461
+ // each render becomes t(…). Their literals are left to this, not to the
462
+ // StringLiteral visitors (which would put a module-level t() call there).
463
+ const moduleLiterals = new WeakSet();
464
+ const moduleRewrites = [];
465
+ if (activeStrategy.id !== 'vanilla') {
466
+ for (const plan of (0, module_constants_1.analyzeModuleConstants)(ast)) {
467
+ if (!plan.provable)
468
+ continue;
469
+ const keys = plan.literals.map((lit) => {
470
+ const id = `${filePath}:${lit.node.loc?.start.line || 0}:${(0, string_utils_1.hashString)(lit.node.value.trim())}`;
471
+ const key = keyMapping.get(id);
472
+ return key && selected(id, lit.node) ? key : null;
473
+ });
474
+ if (keys.some((k) => !k) || confidenceThreshold > 100)
475
+ continue;
476
+ for (const lit of plan.literals)
477
+ moduleLiterals.add(lit.node);
478
+ for (const site of plan.renderSites) {
479
+ const body = (0, ast_helpers_1.findComponentBody)(site);
480
+ if (body)
481
+ componentsNeedingHook.add(body);
482
+ }
483
+ moduleRewrites.push({ plan, keys: keys });
484
+ }
485
+ }
486
+ // Pre-scan to determine if we should fall back to vanilla strategy and identify hooks needed
487
+ (0, ast_helpers_1.traverse)(ast, {
488
+ 'JSXElement|JSXFragment'(visited) {
489
+ const path = visited;
490
+ if (!sentenceFor(path) || confidenceThreshold > 100)
491
+ return;
492
+ if ((0, ast_helpers_1.isInsideComponent)(path)) {
493
+ const componentPath = (0, ast_helpers_1.findComponentBody)(path);
494
+ if (componentPath)
495
+ componentsNeedingHook.add(componentPath);
496
+ }
497
+ else {
498
+ hasNonComponentInjections = true;
499
+ }
500
+ },
501
+ JSXText(path) {
502
+ const text = (0, ast_helpers_1.extractJSXTextContent)(path.node);
503
+ if (!text)
504
+ return;
505
+ const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
506
+ const hasDate = (0, string_utils_1.matchesDatePattern)(text);
507
+ const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3;
508
+ if (!(0, string_utils_1.isTranslatableText)(text) && !isDateOrCurrencyValue)
509
+ return;
510
+ if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
511
+ return;
512
+ const line = path.node.loc?.start.line || 0;
513
+ const extractionId = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
514
+ const key = keyMapping.get(extractionId);
515
+ if (!key || !selected(extractionId, path.node))
516
+ return;
517
+ if (confidenceThreshold > 100)
518
+ return;
519
+ const isComponent = (0, ast_helpers_1.isInsideComponent)(path);
520
+ if (isComponent) {
521
+ if (activeStrategy.id !== 'vanilla') {
522
+ const componentPath = (0, ast_helpers_1.findComponentBody)(path);
523
+ if (!componentPath)
524
+ return;
525
+ componentsNeedingHook.add(componentPath);
526
+ }
527
+ else {
528
+ const componentPath = (0, ast_helpers_1.findComponentBody)(path);
529
+ if (componentPath) {
530
+ componentsNeedingHook.add(componentPath);
531
+ }
532
+ }
533
+ }
534
+ else {
535
+ hasNonComponentInjections = true;
536
+ }
537
+ },
538
+ JSXAttribute(attributes) {
539
+ const name = attributes.node.name;
540
+ if (!ast_helpers_1.t.isJSXIdentifier(name))
541
+ return;
542
+ const value = attributes.node.value;
543
+ if (!ast_helpers_1.t.isStringLiteral(value))
544
+ return;
545
+ const text = value.value.trim();
546
+ if (!text)
547
+ return;
548
+ const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
549
+ const hasDate = (0, string_utils_1.matchesDatePattern)(text);
550
+ const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3;
551
+ if (!(0, string_utils_1.isTranslatableText)(text) && !isDateOrCurrencyValue)
552
+ return;
553
+ if ((0, ast_helpers_1.isAlreadyTranslated)(attributes.node, attributes))
554
+ return;
555
+ const line = attributes.node.loc?.start.line || 0;
556
+ const extractionId = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
557
+ const key = keyMapping.get(extractionId);
558
+ if (!key || !selected(extractionId, attributes.node))
559
+ return;
560
+ if (confidenceThreshold > 100)
561
+ return;
562
+ const isComponent = (0, ast_helpers_1.isInsideComponent)(attributes);
563
+ if (isComponent) {
564
+ if (activeStrategy.id !== 'vanilla') {
565
+ const componentPath = (0, ast_helpers_1.findComponentBody)(attributes);
566
+ if (!componentPath)
567
+ return;
568
+ componentsNeedingHook.add(componentPath);
569
+ }
570
+ else {
571
+ const componentPath = (0, ast_helpers_1.findComponentBody)(attributes);
572
+ if (componentPath) {
573
+ componentsNeedingHook.add(componentPath);
574
+ }
575
+ }
576
+ }
577
+ else {
578
+ hasNonComponentInjections = true;
579
+ }
580
+ },
581
+ StringLiteral(path) {
582
+ if (moduleLiterals.has(path.node))
583
+ return;
584
+ const parent = path.parentPath;
585
+ if (!parent)
586
+ return;
587
+ if (parent.isJSXAttribute())
588
+ return;
589
+ if (!ALLOWED_PARENT_TYPES.includes(parent.node.type))
590
+ return;
591
+ if (parent.isObjectProperty() && parent.node.key === path.node)
592
+ return;
593
+ if (parent.isConditionalExpression() && parent.node.test === path.node)
594
+ return;
595
+ if (parent.isCallExpression() && parent.node.callee === path.node)
596
+ return;
597
+ const text = path.node.value.trim();
598
+ if (!text)
599
+ return;
600
+ const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
601
+ const hasDate = (0, string_utils_1.matchesDatePattern)(text);
602
+ const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3;
603
+ if (!(0, string_utils_1.isTranslatableText)(text) && !isDateOrCurrencyValue)
604
+ return;
605
+ if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
606
+ return;
607
+ if ((0, ast_helpers_1.isNonTranslatableContext)(path))
608
+ return;
609
+ const line = path.node.loc?.start.line || 0;
610
+ const extractionId = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
611
+ const key = keyMapping.get(extractionId);
612
+ if (!key || !selected(extractionId, path.node))
613
+ return;
614
+ if (confidenceThreshold > 100)
615
+ return;
616
+ const isComponent = (0, ast_helpers_1.isInsideComponent)(path);
617
+ if (isComponent) {
618
+ if (activeStrategy.id !== 'vanilla') {
619
+ const componentPath = (0, ast_helpers_1.findComponentBody)(path);
620
+ if (!componentPath)
621
+ return;
622
+ componentsNeedingHook.add(componentPath);
623
+ }
624
+ else {
625
+ const componentPath = (0, ast_helpers_1.findComponentBody)(path);
626
+ if (componentPath) {
627
+ componentsNeedingHook.add(componentPath);
628
+ }
629
+ }
630
+ }
631
+ else {
632
+ hasNonComponentInjections = true;
633
+ }
634
+ }
635
+ });
636
+ // Determine active strategy based on pre-scan results.
637
+ // If there are ONLY non-component injections (no components needing hooks),
638
+ // fall back to vanilla strategy which doesn't require React hooks.
639
+ if (hasNonComponentInjections && componentsNeedingHook.size === 0 && activeStrategy.id !== 'vanilla') {
640
+ activeStrategy = StrategyFactory_1.StrategyFactory.getStrategy('vanilla');
641
+ }
642
+ // Compute hasUseClient AFTER strategy selection — the strategy may have fallen back
643
+ // to vanilla, which doesn't care about use client directives.
644
+ // Before this point the AST hasn't been modified, so the result is the same as
645
+ // if we had computed it pre-scan.
646
+ const hasUseClient = (0, ast_helpers_1.hasUseClientDirective)(ast);
647
+ const extraImports = [];
648
+ // Translator name per component body (`t`, or an alias where `t` is shadowed).
649
+ const translatorNames = new Map();
650
+ const nameFor = (p) => {
651
+ if (activeStrategy.id === 'vanilla')
652
+ return 't';
653
+ const body = (0, ast_helpers_1.findComponentBody)(p);
654
+ if (!body)
655
+ return 't';
656
+ if (!translatorNames.has(body.node))
657
+ translatorNames.set(body.node, translatorNameFor(body));
658
+ return translatorNames.get(body.node);
659
+ };
660
+ for (const { plan, keys } of moduleRewrites) {
661
+ plan.literals.forEach((lit, i) => {
662
+ injections.push({
663
+ file: filePath,
664
+ line: lit.node.loc?.start.line || 0,
665
+ column: lit.node.loc?.start.column || 0,
666
+ original: lit.node.value,
667
+ replacement: JSON.stringify(keys[i]),
668
+ key: keys[i],
669
+ confidence: 100,
670
+ });
671
+ lit.replaceWith(ast_helpers_1.t.stringLiteral(keys[i]));
672
+ });
673
+ for (const site of plan.renderSites) {
674
+ site.replaceWith(ast_helpers_1.t.callExpression(ast_helpers_1.t.identifier(nameFor(site)), [site.node]));
675
+ }
676
+ needsHook = true;
677
+ }
678
+ const messageOptions = { transName: transLocalName(ast, content) };
679
+ // Second pass: identify and replace strings
680
+ (0, ast_helpers_1.traverse)(ast, {
681
+ // A whole JSX sentence → one t() / t.rich() / <Trans> replacing the children.
682
+ 'JSXElement|JSXFragment'(visited) {
683
+ const path = visited;
684
+ const sentence = sentenceFor(path);
685
+ if (!sentence)
686
+ return;
687
+ const confidence = 100;
688
+ if (confidence < confidenceThreshold)
689
+ return;
690
+ const isComponent = (0, ast_helpers_1.isInsideComponent)(path);
691
+ if (isComponent && activeStrategy.id !== 'vanilla' && !(0, ast_helpers_1.findComponentBody)(path))
692
+ return;
693
+ // Outside a component there is no hook: only the plain t() call works.
694
+ const strategy = isComponent || activeStrategy.id === 'vanilla' ? activeStrategy : StrategyFactory_1.StrategyFactory.getStrategy('vanilla');
695
+ const rendering = renderMessage(strategy, sentence.key, sentence.message, messageOptions);
696
+ if (!rendering)
697
+ return;
698
+ path.node.children = withEdgeSpaces(path.node.children, isComponent ? useTranslatorName(rendering.node, nameFor(path)) : rendering.node);
699
+ extraImports.push(...(rendering.imports ?? []));
700
+ injections.push({
701
+ file: filePath,
702
+ line: sentence.line,
703
+ column: path.node.loc?.start.column || 0,
704
+ original: sentence.message.text,
705
+ replacement: (0, ast_helpers_1.generate)(rendering.node, { compact: false }).code,
706
+ key: sentence.key,
707
+ confidence,
708
+ });
709
+ needsHook = true;
710
+ },
711
+ // Handle JSXText
712
+ JSXText(path) {
713
+ const text = (0, ast_helpers_1.extractJSXTextContent)(path.node);
714
+ if (!text)
715
+ return;
716
+ const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
717
+ const hasDate = (0, string_utils_1.matchesDatePattern)(text);
718
+ const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3;
719
+ if (!(0, string_utils_1.isTranslatableText)(text) && !isDateOrCurrencyValue)
720
+ return;
721
+ if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
722
+ return;
723
+ const line = path.node.loc?.start.line || 0;
724
+ const column = path.node.loc?.start.column || 0;
725
+ const extractionId = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
726
+ const key = keyMapping.get(extractionId);
727
+ if (!key || !selected(extractionId, path.node))
728
+ return;
729
+ const confidence = 100;
730
+ if (confidence < confidenceThreshold)
731
+ return;
732
+ const isComponent = (0, ast_helpers_1.isInsideComponent)(path);
733
+ const isSetupSafe = isComponent || activeStrategy.id === 'vanilla';
734
+ // Skip injection entirely when isComponent is true but componentPath is null and the activeStrategy requires hooks
735
+ if (isComponent && activeStrategy.id !== 'vanilla') {
736
+ const componentPath = (0, ast_helpers_1.findComponentBody)(path);
737
+ if (!componentPath)
738
+ return;
739
+ }
740
+ // Fallback for non-component JSX text: use vanilla t() call instead of silently skipping
741
+ if (!isSetupSafe) {
742
+ const tCall = (0, ast_helpers_1.createJSXTCall)(key);
743
+ replaceJSXTextKeepingSpaces(path, tCall);
744
+ injections.push({
745
+ file: filePath,
746
+ line,
747
+ column,
748
+ original: text,
749
+ replacement: `{t('${key}')}`,
750
+ key,
751
+ confidence,
752
+ });
753
+ needsHook = true;
754
+ return;
755
+ }
756
+ const tFunctionName = nameFor(path);
757
+ const tCall = useTranslatorName((0, ast_helpers_1.createJSXTCall)(key), tFunctionName);
758
+ replaceJSXTextKeepingSpaces(path, tCall);
759
+ injections.push({
760
+ file: filePath,
761
+ line,
762
+ column,
763
+ original: text,
764
+ replacement: `{${tFunctionName}('${key}')}`,
765
+ key,
766
+ confidence,
767
+ });
768
+ needsHook = true;
769
+ },
770
+ // Handle JSX attribute strings
771
+ JSXAttribute(attributes) {
772
+ const name = attributes.node.name;
773
+ if (!ast_helpers_1.t.isJSXIdentifier(name))
774
+ return;
775
+ const value = attributes.node.value;
776
+ if (!ast_helpers_1.t.isStringLiteral(value))
777
+ return;
778
+ const text = value.value.trim();
779
+ if (!text)
780
+ return;
781
+ const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
782
+ const hasDate = (0, string_utils_1.matchesDatePattern)(text);
783
+ const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3;
784
+ if (!(0, string_utils_1.isTranslatableText)(text) && !isDateOrCurrencyValue)
785
+ return;
786
+ if ((0, ast_helpers_1.isAlreadyTranslated)(attributes.node, attributes))
787
+ return;
788
+ const line = attributes.node.loc?.start.line || 0;
789
+ const extractionId = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
790
+ const key = keyMapping.get(extractionId);
791
+ if (!key || !selected(extractionId, attributes.node))
792
+ return;
793
+ const confidence = 100;
794
+ if (confidence < confidenceThreshold)
795
+ return;
796
+ const isComponent = (0, ast_helpers_1.isInsideComponent)(attributes);
797
+ const isSetupSafe = isComponent || activeStrategy.id === 'vanilla';
798
+ // Skip injection entirely when isComponent is true but componentPath is null and the activeStrategy requires hooks
799
+ if (isComponent && activeStrategy.id !== 'vanilla') {
800
+ const componentPath = (0, ast_helpers_1.findComponentBody)(attributes);
801
+ if (!componentPath)
802
+ return;
803
+ }
804
+ // Fallback for non-component JSX attributes: use vanilla t() call
805
+ if (!isSetupSafe) {
806
+ const tFunctionName = 't';
807
+ attributes.node.value = ast_helpers_1.t.jsxExpressionContainer((0, ast_helpers_1.createTCall)(key));
808
+ injections.push({
809
+ file: filePath,
810
+ line,
811
+ column: attributes.node.loc?.start.column || 0,
812
+ original: text,
813
+ replacement: `{${tFunctionName}('${key}')}`,
814
+ key,
815
+ confidence,
816
+ });
817
+ needsHook = true;
818
+ return;
819
+ }
820
+ const tFunctionName = nameFor(attributes);
821
+ attributes.node.value = ast_helpers_1.t.jsxExpressionContainer(useTranslatorName((0, ast_helpers_1.createTCall)(key), tFunctionName));
822
+ injections.push({
823
+ file: filePath,
824
+ line,
825
+ column: attributes.node.loc?.start.column || 0,
826
+ original: text,
827
+ replacement: `{${tFunctionName}('${key}')}`,
828
+ key,
829
+ confidence,
830
+ });
831
+ needsHook = true;
832
+ },
833
+ // Handle string literals in safe contexts (ObjectProperty, ArrayExpression, VariableDeclarator, etc.)
834
+ StringLiteral(path) {
835
+ if (moduleLiterals.has(path.node))
836
+ return;
837
+ const parent = path.parentPath;
838
+ if (!parent)
839
+ return;
840
+ // Skip if parent is JSXAttribute since JSXAttribute is handled by its own visitor
841
+ if (parent.isJSXAttribute())
842
+ return;
843
+ const parentType = parent.node.type;
844
+ if (!ALLOWED_PARENT_TYPES.includes(parentType))
845
+ return;
846
+ // Specific checks to ensure the string literal is in a value/translatable position
847
+ if (parent.isObjectProperty() && parent.node.key === path.node)
848
+ return;
849
+ if (parent.isConditionalExpression() && parent.node.test === path.node)
850
+ return;
851
+ if (parent.isCallExpression() && parent.node.callee === path.node)
852
+ return;
853
+ const text = path.node.value.trim();
854
+ if (!text)
855
+ return;
856
+ const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
857
+ const hasDate = (0, string_utils_1.matchesDatePattern)(text);
858
+ const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3;
859
+ if (!(0, string_utils_1.isTranslatableText)(text) && !isDateOrCurrencyValue)
860
+ return;
861
+ if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
862
+ return;
863
+ if ((0, ast_helpers_1.isNonTranslatableContext)(path))
864
+ return;
865
+ const line = path.node.loc?.start.line || 0;
866
+ const extractionId = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
867
+ const key = keyMapping.get(extractionId);
868
+ if (!key || !selected(extractionId, path.node))
869
+ return;
870
+ if (confidenceThreshold > 100)
871
+ return;
872
+ const isComponent = (0, ast_helpers_1.isInsideComponent)(path);
873
+ const isSetupSafe = isComponent || activeStrategy.id === 'vanilla';
874
+ // Skip injection entirely when isComponent is true but componentPath is null and the activeStrategy requires hooks
875
+ if (isComponent && activeStrategy.id !== 'vanilla') {
876
+ const componentPath = (0, ast_helpers_1.findComponentBody)(path);
877
+ if (!componentPath)
878
+ return;
879
+ }
880
+ // Fallback for non-component string literals: use vanilla t() call directly
881
+ if (!isSetupSafe) {
882
+ path.replaceWith((0, ast_helpers_1.createTCall)(key));
883
+ injections.push({
884
+ file: filePath,
885
+ line,
886
+ column: path.node.loc?.start.column || 0,
887
+ original: text,
888
+ replacement: `t('${key}')`,
889
+ key,
890
+ confidence: 100,
891
+ });
892
+ needsHook = true;
893
+ return;
894
+ }
895
+ const tFunctionName = nameFor(path);
896
+ path.replaceWith(useTranslatorName((0, ast_helpers_1.createTCall)(key), tFunctionName));
897
+ injections.push({
898
+ file: filePath,
899
+ line,
900
+ column: path.node.loc?.start.column || 0,
901
+ original: text,
902
+ replacement: `${tFunctionName}('${key}')`,
903
+ key,
904
+ confidence: 100,
905
+ });
906
+ needsHook = true;
907
+ },
908
+ });
909
+ // Second pass: add import and hooks if needed
910
+ const insertedStatements = new Set();
911
+ if (needsHook || injections.length > 0) {
912
+ const fileContext = {
913
+ filePath,
914
+ isComponent: componentsNeedingHook.size > 0,
915
+ hasUseClient
916
+ };
917
+ // Add setup hooks to each component that needs it. The hook (and so the
918
+ // import) can differ per component — next-intl uses `await
919
+ // getTranslations()` only in components that are already async — so
920
+ // imports are collected from the hooks actually added.
921
+ const neededImports = new Map();
922
+ Array.from(componentsNeedingHook).forEach((componentBody) => {
923
+ const translatorName = translatorNames.get(componentBody.node) ?? 't';
924
+ if (translatorName === 't' && activeStrategy.hasHook(componentBody.parentPath))
925
+ return;
926
+ const aliasBinding = translatorName === 't' ? undefined : componentBody.parentPath?.scope.getBinding(translatorName);
927
+ if (aliasBinding && isTranslationHookDeclarator(aliasBinding.path))
928
+ return;
929
+ const fnNode = componentBody.parentPath?.node;
930
+ const componentContext = { ...fileContext, isAsyncComponent: fnNode?.async === true };
931
+ const statements = renameHookBinding(activeStrategy.createHook(componentContext), translatorName);
932
+ if (!componentBody.node?.body || statements.length === 0)
933
+ return;
934
+ if (activeStrategy.requiresAsyncComponent(componentContext) && fnNode && 'async' in fnNode && !fnNode.async) {
935
+ fnNode.async = true;
936
+ componentContext.isAsyncComponent = true;
937
+ }
938
+ const body = componentBody.node.body;
939
+ body.unshift(...statements);
940
+ for (const statement of statements) {
941
+ insertedStatements.add((0, ast_helpers_1.generate)(statement, { compact: false }).code);
942
+ }
943
+ hookAdded = true;
944
+ const decl = activeStrategy.createImport(componentContext);
945
+ neededImports.set(importSignature(decl), decl);
946
+ });
947
+ if (neededImports.size === 0 && !activeStrategy.hasImport(ast)) {
948
+ const decl = activeStrategy.createImport(fileContext);
949
+ neededImports.set(importSignature(decl), decl);
950
+ }
951
+ for (const decl of [...neededImports.values()].reverse()) {
952
+ if (hasImportSpecifiers(ast, decl))
953
+ continue;
954
+ ast.program.body.unshift(decl);
955
+ importAdded = true;
956
+ }
957
+ for (const { source, name, local } of extraImports) {
958
+ if (ensureNamedImport(ast, source, name, local))
959
+ importAdded = true;
960
+ }
961
+ }
962
+ const modifiedContent = (0, ast_helpers_1.placeStatementsOnOwnLines)((0, ast_helpers_1.generateCode)(ast, content), [...insertedStatements]);
963
+ return {
964
+ file: filePath,
965
+ originalContent: content,
966
+ modifiedContent,
967
+ injections,
968
+ importAdded,
969
+ hookAdded,
970
+ };
971
+ }
972
+ /**
973
+ * Inject translations into a Vue SFC file.
974
+ * Uses @vue/compiler-sfc to parse, vue/template-scan to find translatable
975
+ * units, vue/template-rewrite to inject $t() calls, and vue/script-inject
976
+ * to add useI18n() in <script setup>.
977
+ */
978
+ async function injectIntoVueFile(content, filePath, keyMapping, _confidenceThreshold, _framework, _existingDeps) {
979
+ const { parseSfc } = await Promise.resolve().then(() => __importStar(require('../vue/sfc')));
980
+ const { scanTemplate } = await Promise.resolve().then(() => __importStar(require('../vue/template-scan')));
981
+ const { rewriteTemplate } = await Promise.resolve().then(() => __importStar(require('../vue/template-rewrite')));
982
+ const { injectVueScript } = await Promise.resolve().then(() => __importStar(require('../vue/script-inject')));
983
+ const { hashString } = await Promise.resolve().then(() => __importStar(require('../../utils/string-utils')));
984
+ const sfc = parseSfc(content);
985
+ if (!sfc) {
986
+ return { file: filePath, originalContent: content, modifiedContent: content, injections: [], importAdded: false, hookAdded: false };
987
+ }
988
+ const originalContent = content;
989
+ const injections = [];
990
+ let importAdded = false;
991
+ let hookAdded = false;
992
+ // 1. Scan template for translatable units
993
+ if (sfc.descriptor.template) {
994
+ const templateContent = sfc.descriptor.template.content;
995
+ const scanResult = scanTemplate(templateContent, filePath, sfc.templateOffset, hashString);
996
+ // 2. Rewrite template with $t() calls
997
+ const rewriteResult = rewriteTemplate(content, templateContent, sfc.templateOffset, scanResult.units, keyMapping, filePath);
998
+ if (rewriteResult.parseFailed) {
999
+ return { file: filePath, originalContent, modifiedContent: content, injections: [], importAdded: false, hookAdded: false };
1000
+ }
1001
+ injections.push(...rewriteResult.injections);
1002
+ content = rewriteResult.content;
1003
+ }
1004
+ // 3. Inject into script (only for <script setup>)
1005
+ if (sfc.scriptBlock === 'setup' && sfc.descriptor.scriptSetup) {
1006
+ const scriptResult = injectVueScript({
1007
+ scriptContent: sfc.descriptor.scriptSetup.content,
1008
+ filePath,
1009
+ templateOffset: sfc.scriptOffset,
1010
+ isSetup: true,
1011
+ hasTs: 'lang' in sfc.descriptor.scriptSetup.attrs && /(?:ts|typescript)/.test(String(sfc.descriptor.scriptSetup.attrs.lang)),
1012
+ hasI18nImport: false,
1013
+ hasUseI18n: false,
1014
+ });
1015
+ if (scriptResult.importAdded)
1016
+ importAdded = true;
1017
+ if (scriptResult.hookAdded)
1018
+ hookAdded = true;
1019
+ injections.push(...scriptResult.injections);
1020
+ // Replace the script content in the file
1021
+ if (scriptResult.content !== sfc.descriptor.scriptSetup.content) {
1022
+ const scriptStart = content.indexOf(sfc.descriptor.scriptSetup.content);
1023
+ if (scriptStart !== -1) {
1024
+ content = content.substring(0, scriptStart) + scriptResult.content + content.substring(scriptStart + sfc.descriptor.scriptSetup.content.length);
1025
+ }
1026
+ else {
1027
+ // Padded content (from @vue/compiler-sfc pad:'line') not found in original file.
1028
+ // Find the script section by tags and replace its body.
1029
+ const scriptEndTag = '</script>';
1030
+ const scriptEndIndex = content.lastIndexOf(scriptEndTag);
1031
+ if (scriptEndIndex !== -1) {
1032
+ const beforeScript = content.substring(0, scriptEndIndex);
1033
+ const scriptTagIndex = beforeScript.lastIndexOf('<script');
1034
+ if (scriptTagIndex !== -1) {
1035
+ const tagEnd = content.indexOf('>', scriptTagIndex) + 1;
1036
+ const stripped = scriptResult.content.replace(/^\/\/[^\n]*\n/gm, '').replace(/^\n+/, '');
1037
+ content = content.substring(0, tagEnd) + '\n' + stripped + '\n' + content.substring(scriptEndIndex);
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ return {
1044
+ file: filePath,
1045
+ originalContent,
1046
+ modifiedContent: content,
1047
+ injections,
1048
+ importAdded,
1049
+ hookAdded,
1050
+ };
1051
+ }
1052
+ /**
1053
+ * Check if injection would be idempotent
1054
+ * (running again would produce the same output)
1055
+ */
1056
+ function checkIdempotency(originalContent, modifiedContent, filePath, keyMapping, framework = 'unknown') {
1057
+ try {
1058
+ // Try to inject again into the modified content
1059
+ const result = injectIntoFile(modifiedContent, filePath, keyMapping, 80, framework, undefined);
1060
+ // If no new injections, it's idempotent
1061
+ return result.injections.length === 0;
1062
+ }
1063
+ catch {
1064
+ return false;
1065
+ }
1066
+ }