@babelize/engine 0.0.0-stage → 0.1.2

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 (270) 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/detection/stack.d.ts +42 -0
  11. package/dist/detection/stack.js +277 -0
  12. package/dist/django/catalog.d.ts +92 -0
  13. package/dist/django/catalog.js +677 -0
  14. package/dist/django/extract.d.ts +20 -0
  15. package/dist/django/extract.js +79 -0
  16. package/dist/django/inject-file.d.ts +6 -0
  17. package/dist/django/inject-file.js +52 -0
  18. package/dist/django/inject.d.ts +11 -0
  19. package/dist/django/inject.js +205 -0
  20. package/dist/django/keys.d.ts +21 -0
  21. package/dist/django/keys.js +63 -0
  22. package/dist/django/locale-names.d.ts +13 -0
  23. package/dist/django/locale-names.js +60 -0
  24. package/dist/django/mo-file.d.ts +6 -0
  25. package/dist/django/mo-file.js +60 -0
  26. package/dist/django/plural-forms.d.ts +34 -0
  27. package/dist/django/plural-forms.js +216 -0
  28. package/dist/django/po-file.d.ts +38 -0
  29. package/dist/django/po-file.js +273 -0
  30. package/dist/django/python-extract.d.ts +34 -0
  31. package/dist/django/python-extract.js +619 -0
  32. package/dist/django/python-lexer.d.ts +65 -0
  33. package/dist/django/python-lexer.js +398 -0
  34. package/dist/django/template-extract.d.ts +32 -0
  35. package/dist/django/template-extract.js +680 -0
  36. package/dist/django/types.d.ts +49 -0
  37. package/dist/django/types.js +15 -0
  38. package/dist/index.d.ts +59 -0
  39. package/dist/index.js +140 -0
  40. package/dist/injection/InjectionStrategy.d.ts +62 -0
  41. package/dist/injection/InjectionStrategy.js +2 -0
  42. package/dist/injection/StrategyFactory.d.ts +4 -0
  43. package/dist/injection/StrategyFactory.js +29 -0
  44. package/dist/injection/html/html-injector.d.ts +11 -0
  45. package/dist/injection/html/html-injector.js +173 -0
  46. package/dist/injection/strategies/NextIntlStrategy.d.ts +25 -0
  47. package/dist/injection/strategies/NextIntlStrategy.js +141 -0
  48. package/dist/injection/strategies/ReactI18nextStrategy.d.ts +19 -0
  49. package/dist/injection/strategies/ReactI18nextStrategy.js +116 -0
  50. package/dist/injection/strategies/VanillaStrategy.d.ts +11 -0
  51. package/dist/injection/strategies/VanillaStrategy.js +73 -0
  52. package/dist/injection/strategies/VueI18nStrategy.d.ts +11 -0
  53. package/dist/injection/strategies/VueI18nStrategy.js +84 -0
  54. package/dist/local/formatter.d.ts +26 -0
  55. package/dist/local/formatter.js +105 -0
  56. package/dist/local/git.d.ts +23 -0
  57. package/dist/local/git.js +136 -0
  58. package/dist/local/hosted-provider.d.ts +68 -0
  59. package/dist/local/hosted-provider.js +215 -0
  60. package/dist/local/index.d.ts +23 -0
  61. package/dist/local/index.js +75 -0
  62. package/dist/local/provider.d.ts +56 -0
  63. package/dist/local/provider.js +119 -0
  64. package/dist/local/pseudo-provider.d.ts +18 -0
  65. package/dist/local/pseudo-provider.js +208 -0
  66. package/dist/local/report.d.ts +177 -0
  67. package/dist/local/report.js +186 -0
  68. package/dist/local/translate-core.d.ts +218 -0
  69. package/dist/local/translate-core.js +499 -0
  70. package/dist/local/translate-messages.d.ts +49 -0
  71. package/dist/local/translate-messages.js +89 -0
  72. package/dist/local/workspace-copy.d.ts +91 -0
  73. package/dist/local/workspace-copy.js +355 -0
  74. package/dist/modules/extractor/extract.d.ts +20 -0
  75. package/dist/modules/extractor/extract.js +709 -0
  76. package/dist/modules/extractor/index.d.ts +2 -0
  77. package/dist/modules/extractor/index.js +63 -0
  78. package/dist/modules/finalization/django-finalize.d.ts +28 -0
  79. package/dist/modules/finalization/django-finalize.js +412 -0
  80. package/dist/modules/finalization/finalize.d.ts +38 -0
  81. package/dist/modules/finalization/finalize.js +745 -0
  82. package/dist/modules/finalization/index.d.ts +1 -0
  83. package/dist/modules/finalization/index.js +5 -0
  84. package/dist/modules/finalization/nextjs-finalize.d.ts +40 -0
  85. package/dist/modules/finalization/nextjs-finalize.js +328 -0
  86. package/dist/modules/finalization/static-runtime.d.ts +9 -0
  87. package/dist/modules/finalization/static-runtime.js +261 -0
  88. package/dist/modules/finalization/writes.d.ts +29 -0
  89. package/dist/modules/finalization/writes.js +78 -0
  90. package/dist/modules/format-injection/index.d.ts +19 -0
  91. package/dist/modules/format-injection/index.js +273 -0
  92. package/dist/modules/format-injection/inject-formats.d.ts +31 -0
  93. package/dist/modules/format-injection/inject-formats.js +208 -0
  94. package/dist/modules/glossary/index.d.ts +2 -0
  95. package/dist/modules/glossary/index.js +16 -0
  96. package/dist/modules/glossary/rules.d.ts +81 -0
  97. package/dist/modules/glossary/rules.js +244 -0
  98. package/dist/modules/glossary/types.d.ts +22 -0
  99. package/dist/modules/glossary/types.js +2 -0
  100. package/dist/modules/injection/index.d.ts +1 -0
  101. package/dist/modules/injection/index.js +6 -0
  102. package/dist/modules/injection/inject.d.ts +29 -0
  103. package/dist/modules/injection/inject.js +1050 -0
  104. package/dist/modules/keygen/index.d.ts +1 -0
  105. package/dist/modules/keygen/index.js +11 -0
  106. package/dist/modules/keygen/keygen.d.ts +4 -0
  107. package/dist/modules/keygen/keygen.js +123 -0
  108. package/dist/modules/keygen/locale-data.d.ts +7 -0
  109. package/dist/modules/keygen/locale-data.js +73 -0
  110. package/dist/modules/keygen/public.d.ts +2 -0
  111. package/dist/modules/keygen/public.js +12 -0
  112. package/dist/modules/locale-builder/index.d.ts +1 -0
  113. package/dist/modules/locale-builder/index.js +7 -0
  114. package/dist/modules/locale-builder/locales.d.ts +13 -0
  115. package/dist/modules/locale-builder/locales.js +130 -0
  116. package/dist/modules/lockfile/hasher.d.ts +7 -0
  117. package/dist/modules/lockfile/hasher.js +13 -0
  118. package/dist/modules/lockfile/index.d.ts +3 -0
  119. package/dist/modules/lockfile/index.js +22 -0
  120. package/dist/modules/lockfile/lockfile-manager.d.ts +99 -0
  121. package/dist/modules/lockfile/lockfile-manager.js +497 -0
  122. package/dist/modules/lockfile/schema.d.ts +196 -0
  123. package/dist/modules/lockfile/schema.js +56 -0
  124. package/dist/modules/parser/index.d.ts +2 -0
  125. package/dist/modules/parser/index.js +9 -0
  126. package/dist/modules/summary/index.d.ts +1 -0
  127. package/dist/modules/summary/index.js +7 -0
  128. package/dist/modules/summary/summary.d.ts +9 -0
  129. package/dist/modules/summary/summary.js +195 -0
  130. package/dist/modules/sync-detector/index.d.ts +2 -0
  131. package/dist/modules/sync-detector/index.js +7 -0
  132. package/dist/modules/sync-detector/reporter.d.ts +3 -0
  133. package/dist/modules/sync-detector/reporter.js +16 -0
  134. package/dist/modules/sync-detector/sync-detector.d.ts +39 -0
  135. package/dist/modules/sync-detector/sync-detector.js +374 -0
  136. package/dist/modules/translator/batch-optimizer.d.ts +12 -0
  137. package/dist/modules/translator/batch-optimizer.js +30 -0
  138. package/dist/modules/translator/client/byok.provider.d.ts +64 -0
  139. package/dist/modules/translator/client/byok.provider.js +319 -0
  140. package/dist/modules/translator/client/direct-translator.client.d.ts +12 -0
  141. package/dist/modules/translator/client/direct-translator.client.js +71 -0
  142. package/dist/modules/translator/client/gemini.provider.d.ts +1 -0
  143. package/dist/modules/translator/client/gemini.provider.js +110 -0
  144. package/dist/modules/translator/client/groq.provider.d.ts +1 -0
  145. package/dist/modules/translator/client/groq.provider.js +111 -0
  146. package/dist/modules/translator/client/request.types.d.ts +1 -0
  147. package/dist/modules/translator/client/request.types.js +2 -0
  148. package/dist/modules/translator/client/retry.d.ts +29 -0
  149. package/dist/modules/translator/client/retry.js +94 -0
  150. package/dist/modules/translator/client/test-utils/mock.client.d.ts +31 -0
  151. package/dist/modules/translator/client/test-utils/mock.client.js +59 -0
  152. package/dist/modules/translator/client/translator.interface.d.ts +1 -0
  153. package/dist/modules/translator/client/translator.interface.js +2 -0
  154. package/dist/modules/translator/icu-branches.d.ts +138 -0
  155. package/dist/modules/translator/icu-branches.js +393 -0
  156. package/dist/modules/translator/language-codes/errors.d.ts +10 -0
  157. package/dist/modules/translator/language-codes/errors.js +18 -0
  158. package/dist/modules/translator/language-codes/flores-map.d.ts +1 -0
  159. package/dist/modules/translator/language-codes/flores-map.js +107 -0
  160. package/dist/modules/translator/language-codes/index.d.ts +11 -0
  161. package/dist/modules/translator/language-codes/index.js +24 -0
  162. package/dist/modules/translator/language-codes/language-mapper.d.ts +25 -0
  163. package/dist/modules/translator/language-codes/language-mapper.js +132 -0
  164. package/dist/modules/translator/language-codes/rtl-languages.d.ts +2 -0
  165. package/dist/modules/translator/language-codes/rtl-languages.js +9 -0
  166. package/dist/modules/translator/memory/memory-matcher.d.ts +8 -0
  167. package/dist/modules/translator/memory/memory-matcher.js +218 -0
  168. package/dist/modules/translator/memory/types.d.ts +22 -0
  169. package/dist/modules/translator/memory/types.js +2 -0
  170. package/dist/modules/translator/translate.d.ts +74 -0
  171. package/dist/modules/translator/translate.js +1047 -0
  172. package/dist/modules/vue/migrate.d.ts +13 -0
  173. package/dist/modules/vue/migrate.js +40 -0
  174. package/dist/modules/vue/script-inject.d.ts +16 -0
  175. package/dist/modules/vue/script-inject.js +115 -0
  176. package/dist/modules/vue/sfc.d.ts +11 -0
  177. package/dist/modules/vue/sfc.js +51 -0
  178. package/dist/modules/vue/template-rewrite.d.ts +25 -0
  179. package/dist/modules/vue/template-rewrite.js +76 -0
  180. package/dist/modules/vue/template-scan.d.ts +29 -0
  181. package/dist/modules/vue/template-scan.js +264 -0
  182. package/dist/modules/workspace/index.d.ts +1 -0
  183. package/dist/modules/workspace/index.js +5 -0
  184. package/dist/modules/workspace/validate.d.ts +13 -0
  185. package/dist/modules/workspace/validate.js +333 -0
  186. package/dist/orchestrator.d.ts +13 -0
  187. package/dist/orchestrator.js +727 -0
  188. package/dist/providers/default-translation-provider.d.ts +20 -0
  189. package/dist/providers/default-translation-provider.js +30 -0
  190. package/dist/providers/index.d.ts +2 -0
  191. package/dist/providers/index.js +21 -0
  192. package/dist/providers/types.d.ts +153 -0
  193. package/dist/providers/types.js +2 -0
  194. package/dist/qa/check-project.d.ts +50 -0
  195. package/dist/qa/check-project.js +193 -0
  196. package/dist/qa/gettext-files.d.ts +15 -0
  197. package/dist/qa/gettext-files.js +115 -0
  198. package/dist/qa/locale-files.d.ts +44 -0
  199. package/dist/qa/locale-files.js +159 -0
  200. package/dist/qa/locale-qa.d.ts +61 -0
  201. package/dist/qa/locale-qa.js +342 -0
  202. package/dist/qa/review.d.ts +43 -0
  203. package/dist/qa/review.js +175 -0
  204. package/dist/runtime/config.d.ts +8 -0
  205. package/dist/runtime/config.js +37 -0
  206. package/dist/runtime/index.d.ts +3 -0
  207. package/dist/runtime/index.js +8 -0
  208. package/dist/runtime/types.d.ts +427 -0
  209. package/dist/runtime/types.js +38 -0
  210. package/dist/types.d.ts +134 -0
  211. package/dist/types.js +12 -0
  212. package/dist/utils/ast-helpers.d.ts +57 -0
  213. package/dist/utils/ast-helpers.js +608 -0
  214. package/dist/utils/cache-keys.d.ts +8 -0
  215. package/dist/utils/cache-keys.js +18 -0
  216. package/dist/utils/circuit-breaker.d.ts +43 -0
  217. package/dist/utils/circuit-breaker.js +119 -0
  218. package/dist/utils/concurrent-map.d.ts +4 -0
  219. package/dist/utils/concurrent-map.js +21 -0
  220. package/dist/utils/detect-format.d.ts +5 -0
  221. package/dist/utils/detect-format.js +125 -0
  222. package/dist/utils/extraction-options.d.ts +29 -0
  223. package/dist/utils/extraction-options.js +33 -0
  224. package/dist/utils/file-utils.d.ts +33 -0
  225. package/dist/utils/file-utils.js +267 -0
  226. package/dist/utils/glossary-match.d.ts +9 -0
  227. package/dist/utils/glossary-match.js +25 -0
  228. package/dist/utils/html-extractor.d.ts +8 -0
  229. package/dist/utils/html-extractor.js +317 -0
  230. package/dist/utils/i18next-icu.d.ts +38 -0
  231. package/dist/utils/i18next-icu.js +159 -0
  232. package/dist/utils/icu-message.d.ts +65 -0
  233. package/dist/utils/icu-message.js +346 -0
  234. package/dist/utils/icu-spans.d.ts +10 -0
  235. package/dist/utils/icu-spans.js +126 -0
  236. package/dist/utils/jsx-message.d.ts +93 -0
  237. package/dist/utils/jsx-message.js +628 -0
  238. package/dist/utils/key-usage.d.ts +35 -0
  239. package/dist/utils/key-usage.js +247 -0
  240. package/dist/utils/logger.d.ts +24 -0
  241. package/dist/utils/logger.js +33 -0
  242. package/dist/utils/metadata.d.ts +82 -0
  243. package/dist/utils/metadata.js +261 -0
  244. package/dist/utils/module-constants.d.ts +30 -0
  245. package/dist/utils/module-constants.js +265 -0
  246. package/dist/utils/not-injected.d.ts +6 -0
  247. package/dist/utils/not-injected.js +38 -0
  248. package/dist/utils/placeholder-protection.d.ts +74 -0
  249. package/dist/utils/placeholder-protection.js +181 -0
  250. package/dist/utils/pseudo-markers.d.ts +9 -0
  251. package/dist/utils/pseudo-markers.js +16 -0
  252. package/dist/utils/string-utils.d.ts +52 -0
  253. package/dist/utils/string-utils.js +270 -0
  254. package/dist/utils/translation-quality.d.ts +34 -0
  255. package/dist/utils/translation-quality.js +156 -0
  256. package/dist/utils/translation-sanitizer.d.ts +11 -0
  257. package/dist/utils/translation-sanitizer.js +71 -0
  258. package/dist/utils/translator-caller.d.ts +32 -0
  259. package/dist/utils/translator-caller.js +217 -0
  260. package/dist/utils/translator-load-balancer.d.ts +73 -0
  261. package/dist/utils/translator-load-balancer.js +297 -0
  262. package/dist/utils/ui-copy.d.ts +32 -0
  263. package/dist/utils/ui-copy.js +126 -0
  264. package/dist/utils/untranslated.d.ts +15 -0
  265. package/dist/utils/untranslated.js +34 -0
  266. package/dist/utils/workspace.d.ts +14 -0
  267. package/dist/utils/workspace.js +89 -0
  268. package/package.json +64 -4
  269. package/schemas/translate-report.schema.json +297 -0
  270. package/README.md +0 -3
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /** Shared by the translate phase and locale QA so both judge glossary use the same way. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.containsGlossaryTranslation = containsGlossaryTranslation;
5
+ /**
6
+ * Whether the required glossary translation appears in the output, allowing
7
+ * for inflection: each of its words must match a word in the output on a
8
+ * shared stem (Polish "obszar roboczy" → "obszarem roboczym", German
9
+ * "Arbeitsbereich" → "Arbeitsbereichs"). Scripts without spaces fall back to a
10
+ * substring check.
11
+ */
12
+ function containsGlossaryTranslation(translated, required) {
13
+ const out = translated.toLowerCase();
14
+ const want = required.toLowerCase();
15
+ if (out.includes(want))
16
+ return true;
17
+ const outWords = out.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
18
+ const wantWords = want.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
19
+ if (wantWords.length === 0)
20
+ return true;
21
+ return wantWords.every((w) => {
22
+ const stem = w.slice(0, Math.max(3, w.length - 3));
23
+ return outWords.some((o) => o.startsWith(stem));
24
+ });
25
+ }
@@ -0,0 +1,8 @@
1
+ import type { ExtractedString, Warning } from '../runtime/types';
2
+ import type { Node } from 'node-html-parser';
3
+ export declare function extractStringsFromHtml(content: string, filePath: string, detectFormats?: boolean): Promise<{
4
+ strings: ExtractedString[];
5
+ warnings: Warning[];
6
+ }>;
7
+ export declare function isTextNode(node: Node): boolean;
8
+ export declare function isElementNode(node: Node): boolean;
@@ -0,0 +1,317 @@
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.extractStringsFromHtml = extractStringsFromHtml;
37
+ exports.isTextNode = isTextNode;
38
+ exports.isElementNode = isElementNode;
39
+ const string_utils_1 = require("./string-utils");
40
+ const detect_format_1 = require("./detect-format");
41
+ // Void elements that can't have text content
42
+ const VOID_ELEMENTS = new Set([
43
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
44
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
45
+ ]);
46
+ // Tags to skip entirely (already handled by SKIP_TAGS + void elements)
47
+ // Everything else is considered potentially translatable
48
+ const TRANSLATABLE_ATTRS = new Set([
49
+ 'alt', 'title', 'placeholder', 'aria-label', 'aria-description',
50
+ ]);
51
+ async function extractStringsFromHtml(content, filePath, detectFormats) {
52
+ const { parse } = await Promise.resolve().then(() => __importStar(require('node-html-parser')));
53
+ const ctx = {
54
+ filePath,
55
+ fileContent: content,
56
+ strings: [],
57
+ seenTexts: new Set(),
58
+ };
59
+ const warnings = [];
60
+ const root = parse(content, {
61
+ comment: false,
62
+ blockTextElements: { script: true, style: true, noscript: true, pre: true },
63
+ });
64
+ walkNode(root, ctx, 1, warnings, detectFormats);
65
+ extractHeadStrings(root, ctx, warnings);
66
+ return { strings: ctx.strings, warnings };
67
+ }
68
+ function isTextNode(node) {
69
+ return node.nodeType === 3;
70
+ }
71
+ function isElementNode(node) {
72
+ return node.nodeType === 1;
73
+ }
74
+ function asElement(node) {
75
+ return node.nodeType === 1 ? node : null;
76
+ }
77
+ /**
78
+ * Whether any element at or above `node` carries a `data-i18n` attribute.
79
+ * `node` is the parent of the text node being considered.
80
+ */
81
+ function hasDataI18nAncestor(node) {
82
+ let current = node;
83
+ while (current) {
84
+ if (isElementNode(current)) {
85
+ const el = asElement(current);
86
+ if (el && el.getAttribute('data-i18n'))
87
+ return true;
88
+ }
89
+ current = current.parentNode;
90
+ }
91
+ return false;
92
+ }
93
+ function walkNode(node, ctx, depth, warnings, detectFormats) {
94
+ if (depth > 100)
95
+ return;
96
+ if (isTextNode(node)) {
97
+ const text = node.text.trim();
98
+ if (!text || text.length < 2)
99
+ return;
100
+ const parent = node.parentNode;
101
+ if (!parent || !isElementNode(parent))
102
+ return;
103
+ // Honor data-i18n on ANY ancestor, not just the direct parent — an
104
+ // ancestor `<div data-i18n="k"><p>Text</p></div>` must not re-extract.
105
+ if (hasDataI18nAncestor(parent))
106
+ return;
107
+ const parentRaw = parent.rawTagName;
108
+ if (!parentRaw)
109
+ return;
110
+ const parentTagName = parentRaw.toLowerCase();
111
+ if (string_utils_1.SKIP_TAGS.has(parentTagName) || VOID_ELEMENTS.has(parentTagName))
112
+ return;
113
+ const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
114
+ const hasDate = (0, string_utils_1.matchesDatePattern)(text);
115
+ const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3;
116
+ const format = detectFormats ? (0, detect_format_1.detectFormat)(text) : null;
117
+ if (!(0, string_utils_1.isTranslatableText)(text) && !isDateOrCurrencyValue && !format)
118
+ return;
119
+ if ((0, string_utils_1.containsPII)(text))
120
+ return;
121
+ if (format) {
122
+ warnings.push({
123
+ type: 'ambiguous-format',
124
+ file: ctx.filePath,
125
+ line: findLineNumber(node, ctx.fileContent),
126
+ message: `Detected ${format.class} format: "${text}". Will be replaced with Intl.* API call.`,
127
+ severity: 'warning',
128
+ dismissible: true,
129
+ });
130
+ }
131
+ else {
132
+ if (hasCurrency) {
133
+ warnings.push({
134
+ type: 'hardcoded-currency',
135
+ file: ctx.filePath,
136
+ line: findLineNumber(node, ctx.fileContent),
137
+ message: 'Hardcoded currency/number format detected. Use Intl.NumberFormat.',
138
+ });
139
+ }
140
+ if (hasDate) {
141
+ warnings.push({
142
+ type: 'hardcoded-date',
143
+ file: ctx.filePath,
144
+ line: findLineNumber(node, ctx.fileContent),
145
+ message: 'Hardcoded date format detected. Use Intl.DateTimeFormat.',
146
+ });
147
+ }
148
+ }
149
+ const isDateOrCurrencyOnly = isDateOrCurrencyValue && !(0, string_utils_1.isTranslatableText)(text);
150
+ let context = 'jsx-text';
151
+ if (format) {
152
+ context = 'format-string';
153
+ }
154
+ else if (isDateOrCurrencyOnly) {
155
+ context = hasCurrency ? 'hardcoded-currency' : 'hardcoded-date';
156
+ }
157
+ const line = findLineNumber(node, ctx.fileContent);
158
+ const contentKey = `${ctx.filePath}:${(0, string_utils_1.hashString)(text)}`;
159
+ if (!ctx.seenTexts.has(contentKey)) {
160
+ const id = `${ctx.filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
161
+ ctx.seenTexts.add(contentKey);
162
+ ctx.strings.push({
163
+ text,
164
+ file: ctx.filePath,
165
+ line,
166
+ column: 0,
167
+ context,
168
+ id,
169
+ detectedFormat: format ?? undefined,
170
+ });
171
+ }
172
+ return;
173
+ }
174
+ const element = asElement(node);
175
+ if (element) {
176
+ const raw = element.rawTagName;
177
+ if (!raw) {
178
+ for (const child of element.childNodes) {
179
+ if (isTextNode(child) || asElement(child)) {
180
+ walkNode(child, ctx, depth + 1, warnings, detectFormats);
181
+ }
182
+ }
183
+ return;
184
+ }
185
+ const tagName = raw.toLowerCase();
186
+ for (const attr of TRANSLATABLE_ATTRS) {
187
+ const value = element.getAttribute(attr);
188
+ if (value && (0, string_utils_1.isTranslatableText)(value) && !(0, string_utils_1.containsPII)(value)) {
189
+ const line = findLineNumber(element, ctx.fileContent);
190
+ const contentKey = `${ctx.filePath}:${(0, string_utils_1.hashString)(value)}`;
191
+ if (!ctx.seenTexts.has(contentKey)) {
192
+ const id = `${ctx.filePath}:${line}:${(0, string_utils_1.hashString)(value)}`;
193
+ ctx.seenTexts.add(contentKey);
194
+ ctx.strings.push({
195
+ text: value,
196
+ file: ctx.filePath,
197
+ line,
198
+ column: 0,
199
+ context: 'jsx-attr',
200
+ id,
201
+ });
202
+ }
203
+ }
204
+ }
205
+ // Extract value attribute from <input type="button|submit|reset"> and <option>
206
+ if (tagName === 'input') {
207
+ const type = element.getAttribute('type')?.toLowerCase();
208
+ if (type === 'button' || type === 'submit' || type === 'reset') {
209
+ const value = element.getAttribute('value');
210
+ if (value && value.trim().length >= 2 && !(0, string_utils_1.containsPII)(value) && (0, string_utils_1.isTranslatableText)(value)) {
211
+ const line = findLineNumber(element, ctx.fileContent);
212
+ const contentKey = `${ctx.filePath}:${(0, string_utils_1.hashString)(value)}`;
213
+ if (!ctx.seenTexts.has(contentKey)) {
214
+ const id = `${ctx.filePath}:${line}:${(0, string_utils_1.hashString)(value)}`;
215
+ ctx.seenTexts.add(contentKey);
216
+ ctx.strings.push({
217
+ text: value,
218
+ file: ctx.filePath,
219
+ line,
220
+ column: 0,
221
+ context: 'jsx-attr',
222
+ id,
223
+ });
224
+ }
225
+ }
226
+ }
227
+ }
228
+ if (tagName === 'option') {
229
+ const value = element.getAttribute('value');
230
+ if (value && value.trim().length >= 2 && !(0, string_utils_1.containsPII)(value) && (0, string_utils_1.isTranslatableText)(value)) {
231
+ // Only extract if value differs from text content (avoid duplicates)
232
+ const textContent = element.textContent?.trim();
233
+ if (textContent && value !== textContent) {
234
+ const line = findLineNumber(element, ctx.fileContent);
235
+ const contentKey = `${ctx.filePath}:${(0, string_utils_1.hashString)(value)}`;
236
+ if (!ctx.seenTexts.has(contentKey)) {
237
+ const id = `${ctx.filePath}:${line}:${(0, string_utils_1.hashString)(value)}`;
238
+ ctx.seenTexts.add(contentKey);
239
+ ctx.strings.push({
240
+ text: value,
241
+ file: ctx.filePath,
242
+ line,
243
+ column: 0,
244
+ context: 'jsx-attr',
245
+ id,
246
+ });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ if (string_utils_1.SKIP_TAGS.has(tagName) || VOID_ELEMENTS.has(tagName))
252
+ return;
253
+ for (const child of element.childNodes) {
254
+ if (isTextNode(child) || asElement(child)) {
255
+ walkNode(child, ctx, depth + 1, warnings, detectFormats);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ function findLineNumber(node, fileContent) {
261
+ if (node.range) {
262
+ return (fileContent.slice(0, node.range[0]).match(/\n/g) || []).length + 1;
263
+ }
264
+ const raw = node.rawText || node.text || '';
265
+ if (!raw)
266
+ return 0;
267
+ const idx = fileContent.indexOf(raw);
268
+ if (idx === -1)
269
+ return 0;
270
+ return (fileContent.slice(0, idx).match(/\n/g) || []).length + 1;
271
+ }
272
+ function extractHeadStrings(root, ctx, _warnings) {
273
+ // <title>
274
+ const title = root.querySelector('title');
275
+ if (title) {
276
+ const text = title.textContent?.trim();
277
+ if (text && text.length >= 2 && !ctx.seenTexts.has(text)) {
278
+ const contentKey = `${ctx.filePath}:${(0, string_utils_1.hashString)(text)}`;
279
+ if (!ctx.seenTexts.has(contentKey)) {
280
+ const id = `${ctx.filePath}:title:${(0, string_utils_1.hashString)(text)}`;
281
+ ctx.seenTexts.add(contentKey);
282
+ ctx.strings.push({
283
+ text,
284
+ file: ctx.filePath,
285
+ line: 0,
286
+ column: 0,
287
+ context: 'jsx-attr',
288
+ id,
289
+ });
290
+ }
291
+ }
292
+ }
293
+ // Meta tags with content attribute
294
+ const metaTags = root.querySelectorAll('meta[name],meta[property]');
295
+ const seoAttrs = ['description', 'og:title', 'og:description', 'og:site_name', 'twitter:title', 'twitter:description'];
296
+ for (const meta of metaTags) {
297
+ const name = meta.getAttribute('name') || meta.getAttribute('property');
298
+ if (name && seoAttrs.includes(name)) {
299
+ const content = meta.getAttribute('content');
300
+ if (content && content.trim().length >= 2 && !ctx.seenTexts.has(content)) {
301
+ const contentKey = `${ctx.filePath}:${(0, string_utils_1.hashString)(content)}`;
302
+ if (!ctx.seenTexts.has(contentKey)) {
303
+ const id = `${ctx.filePath}:meta:${(0, string_utils_1.hashString)(content)}`;
304
+ ctx.seenTexts.add(contentKey);
305
+ ctx.strings.push({
306
+ text: content,
307
+ file: ctx.filePath,
308
+ line: 0,
309
+ column: 0,
310
+ context: 'jsx-attr',
311
+ id,
312
+ });
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
@@ -0,0 +1,38 @@
1
+ import type { WorkspaceInfo } from '../runtime/types';
2
+ /** Libraries that call i18next's init themselves (the config is inside the library). */
3
+ export declare const I18NEXT_WRAPPERS: string[];
4
+ /** First line of i18n configs Babelize writes; Babelize may update a file that has it. */
5
+ export declare const GENERATED_I18N_CONFIG_MARKER = "// @babelize-generated i18n config";
6
+ /**
7
+ * Whether an i18next config was written by Babelize: the marker, or the
8
+ * signature of the template from before the marker existed.
9
+ */
10
+ export declare function isGeneratedI18nConfig(content: string): boolean;
11
+ export interface I18nextIcuStatus {
12
+ /** ICU messages ("Hello {name}", plurals) will be formatted at runtime. */
13
+ available: boolean;
14
+ /** The project initializes i18next itself (its own config, or a wrapper like next-i18next): Babelize must not add a second config. */
15
+ userOwnsInit: boolean;
16
+ /** A config the user owns that doesn't load i18next-icu (relative path). */
17
+ userConfig?: string;
18
+ /** Wrapper library that owns i18next's init, when that's how the project is set up. */
19
+ wrapper?: string;
20
+ /** Babelize-generated configs that don't load i18next-icu yet (relative paths); finalization adds it. */
21
+ generatedWithoutIcu: string[];
22
+ }
23
+ /**
24
+ * Can a react-i18next project format ICU messages at runtime? Decided by the
25
+ * config that actually initializes i18next — the files calling
26
+ * `.use(initReactI18next)` / `i18n.init(…)`:
27
+ * - a config the user owns: only if it loads i18next-icu;
28
+ * - Babelize's own config: yes (finalization adds i18next-icu to older ones);
29
+ * - a wrapper that owns init (next-i18next, …): only if its config loads i18next-icu;
30
+ * - nothing yet: yes, Babelize will generate the config.
31
+ * Without ICU, placeholders would show as raw `{user}`, so sentences stay
32
+ * fragments until the user adds i18next-icu.
33
+ */
34
+ export declare function detectI18nextIcu(workspace: WorkspaceInfo): Promise<I18nextIcuStatus>;
35
+ /** Add i18next-icu to a Babelize-generated config (idempotent). */
36
+ export declare function addIcuToGeneratedConfig(content: string): string;
37
+ /** The note shown when sentences stay fragments because the runtime can't format ICU. */
38
+ export declare function missingIcuNote(status: I18nextIcuStatus): string | null;
@@ -0,0 +1,159 @@
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.GENERATED_I18N_CONFIG_MARKER = exports.I18NEXT_WRAPPERS = void 0;
37
+ exports.isGeneratedI18nConfig = isGeneratedI18nConfig;
38
+ exports.detectI18nextIcu = detectI18nextIcu;
39
+ exports.addIcuToGeneratedConfig = addIcuToGeneratedConfig;
40
+ exports.missingIcuNote = missingIcuNote;
41
+ const path = __importStar(require("path"));
42
+ const types_1 = require("../runtime/types");
43
+ const file_utils_1 = require("./file-utils");
44
+ /** Configs often live in src/lib, which extraction skips; only skip build output and dependencies here. */
45
+ const CONFIG_SCAN_EXCLUDES = ['node_modules', '.git', '.next', 'dist', 'build', 'out', 'coverage', '*.test.*', '*.spec.*', '*.d.ts', '*.min.js'];
46
+ /** Libraries that call i18next's init themselves (the config is inside the library). */
47
+ exports.I18NEXT_WRAPPERS = ['next-i18next', 'remix-i18next', 'gatsby-plugin-react-i18next'];
48
+ /** Config files those wrappers read, relative to the project root. */
49
+ const WRAPPER_CONFIG_FILES = [
50
+ 'next-i18next.config.js', 'next-i18next.config.mjs', 'next-i18next.config.cjs', 'next-i18next.config.ts',
51
+ 'gatsby-config.js', 'gatsby-config.ts', 'remix.config.js',
52
+ ];
53
+ /** First line of i18n configs Babelize writes; Babelize may update a file that has it. */
54
+ exports.GENERATED_I18N_CONFIG_MARKER = '// @babelize-generated i18n config';
55
+ /**
56
+ * Whether an i18next config was written by Babelize: the marker, or the
57
+ * signature of the template from before the marker existed.
58
+ */
59
+ function isGeneratedI18nConfig(content) {
60
+ if (content.includes(exports.GENERATED_I18N_CONFIG_MARKER))
61
+ return true;
62
+ return (content.includes('export const t = i18n.t.bind(i18n);') &&
63
+ content.includes("loadPath: '/locales/{{lng}}.json'") &&
64
+ content.includes("bindI18nStore: 'added removed'"));
65
+ }
66
+ const loadsIcu = (content) => /['"]i18next-icu['"]/.test(content);
67
+ /** The file initializes i18next for React: `.use(initReactI18next)` or `i18next.init(…)`. */
68
+ function isI18nextInit(content) {
69
+ if (/\.use\(\s*initReactI18next\s*\)/.test(content))
70
+ return true;
71
+ return /from\s+['"]i18next['"]|require\(\s*['"]i18next['"]\s*\)/.test(content) && /\b(?:i18n|i18next|instance)\s*(?:\.use\([^)]*\)\s*)*\.init\s*\(/.test(content);
72
+ }
73
+ async function readSafe(file) {
74
+ try {
75
+ return (await (0, file_utils_1.readFile)(file)) ?? '';
76
+ }
77
+ catch {
78
+ return '';
79
+ }
80
+ }
81
+ /**
82
+ * Can a react-i18next project format ICU messages at runtime? Decided by the
83
+ * config that actually initializes i18next — the files calling
84
+ * `.use(initReactI18next)` / `i18n.init(…)`:
85
+ * - a config the user owns: only if it loads i18next-icu;
86
+ * - Babelize's own config: yes (finalization adds i18next-icu to older ones);
87
+ * - a wrapper that owns init (next-i18next, …): only if its config loads i18next-icu;
88
+ * - nothing yet: yes, Babelize will generate the config.
89
+ * Without ICU, placeholders would show as raw `{user}`, so sentences stay
90
+ * fragments until the user adds i18next-icu.
91
+ */
92
+ async function detectI18nextIcu(workspace) {
93
+ const seen = new Set();
94
+ const userConfigs = [];
95
+ const generatedWithoutIcu = [];
96
+ for (const dir of new Set(['.', ...workspace.sourceDirs])) {
97
+ let files = [];
98
+ try {
99
+ files = (await (0, file_utils_1.findFiles)(path.join(workspace.path, dir), types_1.SUPPORTED_EXTENSIONS, CONFIG_SCAN_EXCLUDES)) ?? [];
100
+ }
101
+ catch {
102
+ files = [];
103
+ }
104
+ for (const file of files) {
105
+ if (seen.has(file) || /[\\/]node_modules[\\/]/.test(file))
106
+ continue;
107
+ seen.add(file);
108
+ const content = await readSafe(file);
109
+ if (!isI18nextInit(content))
110
+ continue;
111
+ const rel = path.relative(workspace.path, file).split(path.sep).join('/');
112
+ if (isGeneratedI18nConfig(content)) {
113
+ if (!loadsIcu(content))
114
+ generatedWithoutIcu.push(rel);
115
+ }
116
+ else {
117
+ userConfigs.push({ rel, icu: loadsIcu(content) });
118
+ }
119
+ }
120
+ }
121
+ if (userConfigs.length > 0) {
122
+ const without = userConfigs.find((c) => !c.icu);
123
+ return { available: !without, userOwnsInit: true, userConfig: without?.rel, generatedWithoutIcu };
124
+ }
125
+ const deps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
126
+ const wrapper = exports.I18NEXT_WRAPPERS.find((w) => deps[w]);
127
+ if (wrapper) {
128
+ let icu = false;
129
+ for (const name of WRAPPER_CONFIG_FILES) {
130
+ const file = path.join(workspace.path, name);
131
+ if ((await (0, file_utils_1.pathExists)(file).catch(() => false)) && loadsIcu(await readSafe(file)))
132
+ icu = true;
133
+ }
134
+ return { available: icu, userOwnsInit: true, wrapper, generatedWithoutIcu };
135
+ }
136
+ return { available: true, userOwnsInit: false, generatedWithoutIcu };
137
+ }
138
+ /** Add i18next-icu to a Babelize-generated config (idempotent). */
139
+ function addIcuToGeneratedConfig(content) {
140
+ if (loadsIcu(content))
141
+ return content;
142
+ const importLine = "import ICU from 'i18next-icu';";
143
+ let out = content;
144
+ const detector = /^import LanguageDetector from ['"]i18next-browser-languagedetector['"];?\r?\n/m;
145
+ if (detector.test(out))
146
+ out = out.replace(detector, (m) => `${m}${importLine}\n`);
147
+ else
148
+ out = out.replace(/^(import i18n from ['"]i18next['"];?\r?\n)/m, `$1${importLine}\n`);
149
+ return out.replace(/\.use\(initReactI18next\)/, '.use(ICU)\n .use(initReactI18next)');
150
+ }
151
+ /** The note shown when sentences stay fragments because the runtime can't format ICU. */
152
+ function missingIcuNote(status) {
153
+ if (status.available)
154
+ return null;
155
+ if (status.wrapper) {
156
+ return `${status.wrapper} initializes i18next without i18next-icu, so JSX sentences with variables were kept as separate fragments. Add i18next-icu to enable full-sentence translation (add it to the \`use\` option of your ${status.wrapper} config).`;
157
+ }
158
+ return `${status.userConfig} doesn't load i18next-icu, so JSX sentences with variables were kept as separate fragments. Add i18next-icu to enable full-sentence translation: \`import ICU from 'i18next-icu'\` and \`.use(ICU)\`.`;
159
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Minimal ICU MessageFormat parser and validator for translated messages.
3
+ *
4
+ * ICU messages can't be tokenized like simple placeholders: a translation
5
+ * legitimately changes their structure (Polish needs `few`/`many` plural
6
+ * branches English doesn't have). So instead of protecting them, we let the
7
+ * model rewrite the whole message and then check the result is still a valid
8
+ * message with the same arguments and selector branches.
9
+ *
10
+ * Covers what UI strings use: simple and formatted arguments, plural,
11
+ * selectordinal (with offset and =n keys), select, `#`, and apostrophe
12
+ * quoting. Not a full ICU implementation (no nested formatting skeletons).
13
+ */
14
+ export type IcuNode = {
15
+ type: 'text';
16
+ value: string;
17
+ } | {
18
+ type: 'pound';
19
+ } | {
20
+ type: 'argument';
21
+ name: string;
22
+ format?: string;
23
+ } | {
24
+ type: 'plural' | 'selectordinal' | 'select';
25
+ name: string;
26
+ options: Record<string, IcuNode[]>;
27
+ };
28
+ export declare class IcuParseError extends Error {
29
+ }
30
+ export declare function parseIcu(message: string): IcuNode[];
31
+ /** True when the message uses plural/select/selectordinal syntax. */
32
+ export declare function isIcuMessage(message: string): boolean;
33
+ export interface IcuIssue {
34
+ severity: 'error' | 'warning';
35
+ code: 'icu_invalid' | 'icu_arguments_changed' | 'icu_selector_removed' | 'icu_selector_added' | 'icu_branch_lost_argument' | 'icu_select_keys_changed' | 'icu_plural_category_invalid' | 'icu_missing_other' | 'icu_plural_category_missing';
36
+ message: string;
37
+ }
38
+ /**
39
+ * Compare a translated ICU message to its source. Errors mean the message is
40
+ * broken at runtime (unparseable, arguments renamed, select branches changed,
41
+ * no `other`). Missing target plural categories are only a warning: ICU
42
+ * falls back to `other` for them.
43
+ */
44
+ export declare function compareIcuMessages(source: string, translated: string, targetLocale: string, options?: {
45
+ /**
46
+ * Simple arguments of the source the translation may turn into a plural
47
+ * (the count-like ones the engine flagged). A plural over any other
48
+ * argument is an error: `{name, plural, …}` formats a name as NaN.
49
+ */
50
+ pluralizable?: string[];
51
+ }): IcuIssue[];
52
+ export declare function isCountLikeName(name: string): boolean;
53
+ /**
54
+ * Simple `{name}` arguments of a non-ICU message that look like counts. A
55
+ * translation may turn such an argument into a plural for languages that
56
+ * inflect words by number, without the source having to.
57
+ */
58
+ export declare function countArguments(message: string): string[];
59
+ /** Single-brace `{name}` arguments (not `{{name}}`). */
60
+ export declare function extractSimpleArguments(message: string): string[];
61
+ /** CLDR plural categories a translator should produce for this locale. */
62
+ export declare function requiredPluralCategories(locale: string): {
63
+ cardinal: string[];
64
+ ordinal: string[];
65
+ };