@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,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
+ };
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal ICU MessageFormat parser and validator for translated messages.
4
+ *
5
+ * ICU messages can't be tokenized like simple placeholders: a translation
6
+ * legitimately changes their structure (Polish needs `few`/`many` plural
7
+ * branches English doesn't have). So instead of protecting them, we let the
8
+ * model rewrite the whole message and then check the result is still a valid
9
+ * message with the same arguments and selector branches.
10
+ *
11
+ * Covers what UI strings use: simple and formatted arguments, plural,
12
+ * selectordinal (with offset and =n keys), select, `#`, and apostrophe
13
+ * quoting. Not a full ICU implementation (no nested formatting skeletons).
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.IcuParseError = void 0;
17
+ exports.parseIcu = parseIcu;
18
+ exports.isIcuMessage = isIcuMessage;
19
+ exports.compareIcuMessages = compareIcuMessages;
20
+ exports.isCountLikeName = isCountLikeName;
21
+ exports.countArguments = countArguments;
22
+ exports.extractSimpleArguments = extractSimpleArguments;
23
+ exports.requiredPluralCategories = requiredPluralCategories;
24
+ class IcuParseError extends Error {
25
+ }
26
+ exports.IcuParseError = IcuParseError;
27
+ function parseIcu(message) {
28
+ let pos = 0;
29
+ const fail = (msg) => {
30
+ throw new IcuParseError(`${msg} at position ${pos}`);
31
+ };
32
+ const skipSpace = () => {
33
+ while (pos < message.length && /\s/.test(message[pos]))
34
+ pos++;
35
+ };
36
+ const readIdentifier = () => {
37
+ skipSpace();
38
+ const start = pos;
39
+ while (pos < message.length && /[^\s,{}#]/.test(message[pos]))
40
+ pos++;
41
+ if (pos === start)
42
+ fail('Expected identifier');
43
+ return message.slice(start, pos);
44
+ };
45
+ const parseMessage = (inPlural, depth) => {
46
+ const nodes = [];
47
+ let text = '';
48
+ const flush = () => {
49
+ if (text)
50
+ nodes.push({ type: 'text', value: text });
51
+ text = '';
52
+ };
53
+ while (pos < message.length) {
54
+ const ch = message[pos];
55
+ if (ch === "'") {
56
+ // '' is a literal apostrophe; '{…}' / '#…' quotes syntax characters.
57
+ if (message[pos + 1] === "'") {
58
+ text += "'";
59
+ pos += 2;
60
+ continue;
61
+ }
62
+ const next = message[pos + 1];
63
+ if (next === '{' || next === '}' || (inPlural && next === '#')) {
64
+ const end = message.indexOf("'", pos + 1);
65
+ if (end === -1)
66
+ fail('Unterminated quote');
67
+ text += message.slice(pos + 1, end);
68
+ pos = end + 1;
69
+ continue;
70
+ }
71
+ text += ch;
72
+ pos++;
73
+ continue;
74
+ }
75
+ if (ch === '#' && inPlural) {
76
+ flush();
77
+ nodes.push({ type: 'pound' });
78
+ pos++;
79
+ continue;
80
+ }
81
+ if (ch === '}') {
82
+ if (depth === 0)
83
+ fail('Unbalanced }');
84
+ break;
85
+ }
86
+ if (ch === '{') {
87
+ flush();
88
+ nodes.push(parseArgument(depth));
89
+ continue;
90
+ }
91
+ text += ch;
92
+ pos++;
93
+ }
94
+ flush();
95
+ return nodes;
96
+ };
97
+ const parseArgument = (depth) => {
98
+ pos++; // {
99
+ const name = readIdentifier();
100
+ skipSpace();
101
+ if (message[pos] === '}') {
102
+ pos++;
103
+ return { type: 'argument', name };
104
+ }
105
+ if (message[pos] !== ',')
106
+ fail('Expected , or }');
107
+ pos++;
108
+ const kind = readIdentifier();
109
+ skipSpace();
110
+ if (kind === 'plural' || kind === 'selectordinal' || kind === 'select') {
111
+ if (message[pos] !== ',')
112
+ fail(`Expected , after ${kind}`);
113
+ pos++;
114
+ const options = {};
115
+ skipSpace();
116
+ if (kind !== 'select' && message.startsWith('offset:', pos)) {
117
+ pos += 'offset:'.length;
118
+ readIdentifier();
119
+ }
120
+ while (true) {
121
+ skipSpace();
122
+ if (message[pos] === '}') {
123
+ pos++;
124
+ break;
125
+ }
126
+ if (pos >= message.length)
127
+ fail(`Unterminated ${kind}`);
128
+ const key = readIdentifier();
129
+ skipSpace();
130
+ if (message[pos] !== '{')
131
+ fail(`Expected { after option "${key}"`);
132
+ if (key in options)
133
+ fail(`Duplicate option "${key}"`);
134
+ pos++;
135
+ options[key] = parseMessage(kind !== 'select', depth + 1);
136
+ if (message[pos] !== '}')
137
+ fail(`Unterminated option "${key}"`);
138
+ pos++;
139
+ }
140
+ if (Object.keys(options).length === 0)
141
+ fail(`${kind} has no options`);
142
+ return { type: kind, name, options };
143
+ }
144
+ // Formatted argument: {price, number} / {d, date, short} — keep the rest verbatim.
145
+ let format = kind;
146
+ if (message[pos] === ',') {
147
+ const end = message.indexOf('}', pos);
148
+ if (end === -1)
149
+ fail('Unterminated argument');
150
+ format += message.slice(pos, end);
151
+ pos = end;
152
+ }
153
+ skipSpace();
154
+ if (message[pos] !== '}')
155
+ fail('Expected }');
156
+ pos++;
157
+ return { type: 'argument', name, format };
158
+ };
159
+ const nodes = parseMessage(false, 0);
160
+ if (pos < message.length)
161
+ fail('Unexpected }');
162
+ return nodes;
163
+ }
164
+ /** True when the message uses plural/select/selectordinal syntax. */
165
+ function isIcuMessage(message) {
166
+ return /\{\s*[\w.]+\s*,\s*(?:plural|select|selectordinal)\s*,/.test(message);
167
+ }
168
+ function collect(nodes, args, selectors, texts) {
169
+ for (const node of nodes) {
170
+ if (node.type === 'text')
171
+ texts.push(node.value);
172
+ else if (node.type === 'argument')
173
+ args.add(node.name);
174
+ else if (node.type !== 'pound') {
175
+ args.add(node.name);
176
+ selectors.push({ kind: node.type, name: node.name, keys: Object.keys(node.options), branches: Object.values(node.options) });
177
+ for (const branch of Object.values(node.options))
178
+ collect(branch, args, selectors, texts);
179
+ }
180
+ }
181
+ }
182
+ const CLDR_ORDER = ['zero', 'one', 'two', 'few', 'many', 'other'];
183
+ /** Plural categories for a locale, always in CLDR order (runtimes differ in ordering). */
184
+ function pluralCategories(locale, type) {
185
+ try {
186
+ const found = new Set(new Intl.PluralRules(locale, { type }).resolvedOptions().pluralCategories);
187
+ return CLDR_ORDER.filter((c) => found.has(c));
188
+ }
189
+ catch {
190
+ return [...CLDR_ORDER];
191
+ }
192
+ }
193
+ /**
194
+ * Compare a translated ICU message to its source. Errors mean the message is
195
+ * broken at runtime (unparseable, arguments renamed, select branches changed,
196
+ * no `other`). Missing target plural categories are only a warning: ICU
197
+ * falls back to `other` for them.
198
+ */
199
+ function compareIcuMessages(source, translated, targetLocale, options = {}) {
200
+ let sourceAst;
201
+ try {
202
+ sourceAst = parseIcu(source);
203
+ }
204
+ catch {
205
+ // Source isn't valid ICU itself; nothing meaningful to compare.
206
+ return [];
207
+ }
208
+ let targetAst;
209
+ try {
210
+ targetAst = parseIcu(translated);
211
+ }
212
+ catch (err) {
213
+ return [{ severity: 'error', code: 'icu_invalid', message: `Translation is not a valid ICU message: ${err.message}` }];
214
+ }
215
+ const issues = [];
216
+ const srcArgs = new Set();
217
+ const tgtArgs = new Set();
218
+ const srcSelectors = [];
219
+ const tgtSelectors = [];
220
+ collect(sourceAst, srcArgs, srcSelectors, []);
221
+ collect(targetAst, tgtArgs, tgtSelectors, []);
222
+ const missingArgs = [...srcArgs].filter((a) => !tgtArgs.has(a));
223
+ const extraArgs = [...tgtArgs].filter((a) => !srcArgs.has(a));
224
+ if (missingArgs.length || extraArgs.length) {
225
+ issues.push({
226
+ severity: 'error',
227
+ code: 'icu_arguments_changed',
228
+ message: [
229
+ missingArgs.length ? `missing {${missingArgs.join('}, {')}}` : '',
230
+ extraArgs.length ? `unknown {${extraArgs.join('}, {')}}` : '',
231
+ ].filter(Boolean).join('; '),
232
+ });
233
+ }
234
+ // A plural/select dropped from the translation (e.g. "{count} fichiers")
235
+ // still renders, but wrongly for every count but one.
236
+ for (const src of srcSelectors) {
237
+ if (!tgtSelectors.some((t) => t.name === src.name && t.kind === src.kind)) {
238
+ issues.push({
239
+ severity: 'error',
240
+ code: 'icu_selector_removed',
241
+ message: `{${src.name}, ${src.kind}} was removed from the translation`,
242
+ });
243
+ }
244
+ }
245
+ // A translation may add a plural over a simple argument of the source:
246
+ // "{files} files" → Polish "{files, plural, one {# plik} few {# pliki} …}".
247
+ // Only a cardinal plural, and every branch must still show the number.
248
+ for (const target of tgtSelectors) {
249
+ if (srcSelectors.some((s) => s.name === target.name && s.kind === target.kind))
250
+ continue;
251
+ if (target.kind !== 'plural') {
252
+ issues.push({ severity: 'error', code: 'icu_selector_added', message: `{${target.name}, ${target.kind}} is not in the source; only a plural may be added` });
253
+ continue;
254
+ }
255
+ if (!(options.pluralizable ?? []).includes(target.name)) {
256
+ issues.push({ severity: 'error', code: 'icu_selector_added', message: `{${target.name}} is not a number; don't add a plural over it` });
257
+ continue;
258
+ }
259
+ const lost = target.keys.filter((k, i) => !mentions(target.branches[i], target.name));
260
+ if (lost.length) {
261
+ issues.push({
262
+ severity: 'error',
263
+ code: 'icu_branch_lost_argument',
264
+ message: `{${target.name}, plural} branches [${lost.join(', ')}] don't show the number (use # or {${target.name}})`,
265
+ });
266
+ }
267
+ }
268
+ for (const target of tgtSelectors) {
269
+ if (!target.keys.includes('other')) {
270
+ issues.push({ severity: 'error', code: 'icu_missing_other', message: `{${target.name}, ${target.kind}} has no "other" branch` });
271
+ }
272
+ if (target.kind === 'select') {
273
+ const source = srcSelectors.find((s) => s.kind === 'select' && s.name === target.name);
274
+ if (source && [...source.keys].sort().join('|') !== [...target.keys].sort().join('|')) {
275
+ issues.push({
276
+ severity: 'error',
277
+ code: 'icu_select_keys_changed',
278
+ message: `{${target.name}, select} branches changed from [${source.keys.join(', ')}] to [${target.keys.join(', ')}]`,
279
+ });
280
+ }
281
+ continue;
282
+ }
283
+ const allowed = pluralCategories(targetLocale, target.kind === 'selectordinal' ? 'ordinal' : 'cardinal');
284
+ const invalid = target.keys.filter((k) => !/^=\d+$/.test(k) && !allowed.includes(k));
285
+ if (invalid.length) {
286
+ issues.push({
287
+ severity: 'error',
288
+ code: 'icu_plural_category_invalid',
289
+ message: `{${target.name}, ${target.kind}} uses categories [${invalid.join(', ')}] that ${targetLocale} doesn't have (valid: ${allowed.join(', ')})`,
290
+ });
291
+ }
292
+ const missing = allowed.filter((c) => !target.keys.includes(c));
293
+ if (missing.length) {
294
+ issues.push({
295
+ severity: 'warning',
296
+ code: 'icu_plural_category_missing',
297
+ message: `{${target.name}, ${target.kind}} has no [${missing.join(', ')}] branch for ${targetLocale}; "other" will be used`,
298
+ });
299
+ }
300
+ }
301
+ return issues;
302
+ }
303
+ /** Whether a plural branch shows its number: `#`, or `{name}` (also inside nested selectors). */
304
+ function mentions(nodes, name) {
305
+ return nodes.some((n) => n.type === 'pound' ||
306
+ (n.type === 'argument' && n.name === name) ||
307
+ ((n.type === 'plural' || n.type === 'select' || n.type === 'selectordinal') && Object.values(n.options).some((b) => mentions(b, name))));
308
+ }
309
+ /** Argument names that read like counts (`count`, `total`, `itemCount`, `items.length` → itemsLength). */
310
+ const COUNT_NAME = /^(?:count|total|num|n|length)$|(?:Count|Length)$/;
311
+ /** Argument names that are a plural noun, i.e. how many of them (`{files}`, `{unreadMessages}`). */
312
+ const COUNTED_NOUNS = new Set([
313
+ 'attempts', 'comments', 'credits', 'days', 'documents', 'entries', 'errors', 'events', 'files', 'followers',
314
+ 'hours', 'images', 'issues', 'items', 'keys', 'licenses', 'likes', 'members', 'messages', 'minutes',
315
+ 'notifications', 'orders', 'pages', 'photos', 'points', 'products', 'projects', 'records', 'results', 'rows',
316
+ 'seats', 'seconds', 'steps', 'tasks', 'users', 'views', 'warnings', 'weeks', 'months', 'years', 'people',
317
+ ]);
318
+ function isCountLikeName(name) {
319
+ if (COUNT_NAME.test(name))
320
+ return true;
321
+ const last = name.split(/(?=[A-Z])|_/).pop().toLowerCase();
322
+ return COUNTED_NOUNS.has(last);
323
+ }
324
+ /**
325
+ * Simple `{name}` arguments of a non-ICU message that look like counts. A
326
+ * translation may turn such an argument into a plural for languages that
327
+ * inflect words by number, without the source having to.
328
+ */
329
+ function countArguments(message) {
330
+ // Only ICU-style messages: i18next `{{count}}`, printf and template
331
+ // placeholders keep token protection and its checks.
332
+ if (isIcuMessage(message) || /\{\{|\}\}|%(?:\(\w+\))?[sd]|\$\{/.test(message))
333
+ return [];
334
+ return extractSimpleArguments(message).filter(isCountLikeName);
335
+ }
336
+ /** Single-brace `{name}` arguments (not `{{name}}`). */
337
+ function extractSimpleArguments(message) {
338
+ const names = new Set();
339
+ for (const m of message.matchAll(/(?<!\{)\{\s*([A-Za-z_]\w*)\s*\}(?!\})/g))
340
+ names.add(m[1]);
341
+ return [...names];
342
+ }
343
+ /** CLDR plural categories a translator should produce for this locale. */
344
+ function requiredPluralCategories(locale) {
345
+ return { cardinal: pluralCategories(locale, 'cardinal'), ordinal: pluralCategories(locale, 'ordinal') };
346
+ }
@@ -0,0 +1,10 @@
1
+ /** Span differences of `target` per branch combination (empty when fine). */
2
+ export declare function branchSpanProblems(source: string, target: string, neverTranslate?: string[]): BranchSpanProblem[];
3
+ export interface BranchSpanProblem {
4
+ /** Which branch combination, e.g. "count=other" ("message" when the target has no selectors). */
5
+ branch: string;
6
+ lost: string[];
7
+ added: string[];
8
+ }
9
+ /** "count=other: lost <b>, </b>" */
10
+ export declare function describeBranchSpanProblem(p: BranchSpanProblem): string;