@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,727 @@
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.RunAbortedError = void 0;
37
+ exports.runEngineV2 = runEngineV2;
38
+ const fs = __importStar(require("fs-extra"));
39
+ const logger_1 = require("./utils/logger");
40
+ const types_1 = require("@babelize/types");
41
+ const workspace_1 = require("./modules/workspace");
42
+ const workspace_2 = require("./utils/workspace");
43
+ const extract_1 = require("./modules/extractor/extract");
44
+ const keygen_1 = require("./modules/keygen");
45
+ const locale_builder_1 = require("./modules/locale-builder");
46
+ const translate_1 = require("./modules/translator/translate");
47
+ const glossary_1 = require("./modules/glossary");
48
+ const translator_caller_1 = require("./utils/translator-caller");
49
+ const injection_1 = require("./modules/injection");
50
+ const summary_1 = require("./modules/summary");
51
+ const finalization_1 = require("./modules/finalization");
52
+ const lockfile_1 = require("./modules/lockfile");
53
+ const file_utils_1 = require("./utils/file-utils");
54
+ const key_usage_1 = require("./utils/key-usage");
55
+ const not_injected_1 = require("./utils/not-injected");
56
+ const language_codes_1 = require("./modules/translator/language-codes");
57
+ const format_injection_1 = require("./modules/format-injection");
58
+ const extraction_options_1 = require("./utils/extraction-options");
59
+ const catalog_1 = require("./django/catalog");
60
+ /**
61
+ * Whether the run's translator is an LLM (BYOK or an injected provider that
62
+ * isn't plain-text MT). Only then do JSX sentences get ICU plurals in the
63
+ * source; NLLB translates existing ICU messages branch by branch.
64
+ * `capabilities.icu` overrides the guess.
65
+ */
66
+ function llmTranslator(config) {
67
+ return config.capabilities?.icu ?? (!!config.byok || (!!config.providers?.translator && !config.providers.translator.plainText));
68
+ }
69
+ /**
70
+ * Engine v2 Orchestrator (backend runtime)
71
+ *
72
+ * This is the single, clean wiring layer for the pipeline.
73
+ * Workers/controllers call `runEngineV2()`.
74
+ */
75
+ /** Thrown at a progress checkpoint once the run's AbortSignal has fired. */
76
+ class RunAbortedError extends Error {
77
+ constructor(reason) {
78
+ super(reason === 'timeout' ? 'Run timed out' : reason === 'cancelled' ? 'Run cancelled' : `Run aborted: ${reason}`);
79
+ this.reason = reason;
80
+ this.name = 'RunAbortedError';
81
+ }
82
+ }
83
+ exports.RunAbortedError = RunAbortedError;
84
+ function abortReason(signal) {
85
+ const reason = signal.reason;
86
+ if (typeof reason === 'string')
87
+ return reason;
88
+ if (reason instanceof Error)
89
+ return reason.message;
90
+ return 'aborted';
91
+ }
92
+ async function runEngineV2(config) {
93
+ if (config.logger) {
94
+ const { logger, ...rest } = config;
95
+ return (0, logger_1.runWithEngineLogger)(logger, () => runEngineV2(rest));
96
+ }
97
+ const startTime = Date.now();
98
+ // Fresh translations so far, for billing a run that is stopped mid-way.
99
+ const translationProgress = { freshTranslations: 0 };
100
+ try {
101
+ logger_1.engineLogger.info('Starting Babelize engine v2 pipeline', {
102
+ workspacePath: config.workspacePath,
103
+ targetLocales: config.targetLocales,
104
+ dryRun: config.dryRun,
105
+ });
106
+ // Cooperative cancellation: every phase (and every translation batch)
107
+ // reports progress, so the run stops at the next report after the signal
108
+ // fires. The engine has no other way to be stopped mid-run.
109
+ if (config.signal) {
110
+ const signal = config.signal;
111
+ const report = config.onProgress;
112
+ if (signal.aborted)
113
+ throw new RunAbortedError(abortReason(signal));
114
+ config = {
115
+ ...config,
116
+ onProgress: (message, step, status) => {
117
+ if (signal.aborted)
118
+ throw new RunAbortedError(abortReason(signal));
119
+ report?.(message, step, status);
120
+ },
121
+ };
122
+ }
123
+ // The file-list cache is per run: a long-lived worker (or a CLI rerun in
124
+ // the same process) must see files added since the previous run.
125
+ (0, file_utils_1.clearFindFilesCache)();
126
+ config.onProgress?.('Validating workspace...', 'validation', 'running');
127
+ const { workspace } = await (0, workspace_1.validateWorkspace)(config);
128
+ const frameworkConfig = types_1.FRAMEWORK_CONFIGS[workspace.framework] || types_1.FRAMEWORK_CONFIGS['unknown'];
129
+ config.targetLocales = config.targetLocales.filter((l) => l !== config.sourceLocale);
130
+ // Remove stale locale files from previous runs that are no longer in targetLocales.
131
+ // git reset --hard only resets tracked files; generated locale files are untracked
132
+ // and persist across runs, causing stale languages to appear in outputs.
133
+ await (0, workspace_2.removeStaleLocales)(config.workspacePath, config.targetLocales, config.sourceLocale, workspace.localeFormat, workspace.publicDir);
134
+ config.onProgress?.('Extracting strings from codebase...', 'extraction', 'running');
135
+ // One resolved ICU decision for extraction, translation and injection (#248).
136
+ const icuOptions = await (0, extraction_options_1.resolveIcuOptions)(workspace, llmTranslator(config));
137
+ if (!icuOptions.sentences && workspace.framework !== 'django') {
138
+ logger_1.engineLogger.warn('i18next-icu not loaded by the project i18n config; keeping JSX sentences as fragments');
139
+ }
140
+ const { extraction } = await (0, extract_1.extractStrings)(workspace, config.filesToProcess, icuOptions);
141
+ if (extraction.warnings && extraction.warnings.length > 0) {
142
+ logger_1.engineLogger.warn('Extraction warnings', { count: extraction.warnings.length, warnings: extraction.warnings });
143
+ }
144
+ if (extraction.skippedFiles && extraction.skippedFiles.length > 0) {
145
+ logger_1.engineLogger.warn('Files skipped during extraction', {
146
+ count: extraction.skippedFiles.length,
147
+ files: extraction.skippedFiles.map((f) => `${f.file}: ${f.reason}${f.details ? ' — ' + f.details : ''}`),
148
+ });
149
+ }
150
+ // Partition format strings (currency/date/number) from translatable strings.
151
+ // Gate on format-inject phase being in the framework's phases AND formatConfig being enabled (D5).
152
+ // This means static HTML gets format strings as translatable (month names, currency as text)
153
+ // instead of silently dropping them.
154
+ const formatPartitionEnabled = frameworkConfig.phases.includes('format-inject') && config.formatConfig?.enabled;
155
+ const [translatableStrings, formatStrings] = formatPartitionEnabled
156
+ ? (0, format_injection_1.partitionFormatStrings)(extraction.strings)
157
+ : [extraction.strings, []];
158
+ const existingLocale = await (0, locale_builder_1.readLocaleFile)(workspace, config.sourceLocale);
159
+ const existingData = existingLocale || {};
160
+ // Strings injected by earlier runs are no longer extracted, but their keys
161
+ // are still in the source locale file. With a lockfile those keys must
162
+ // still be carried into every target locale (and translated for newly
163
+ // added locales), so "nothing new to extract" is not "nothing to do".
164
+ const hasExistingKeys = !!config.projectId && Object.keys(sourceLocaleKeys(existingData)).length > 0;
165
+ if (translatableStrings.length === 0 && formatStrings.length === 0 && !hasExistingKeys) {
166
+ logger_1.engineLogger.info('No strings found, finishing early');
167
+ return createEmptyResult(startTime, extraction.warnings, extraction.skippedFiles, extraction.notInjected);
168
+ }
169
+ // Use only translatable strings for the rest of the pipeline
170
+ config.onProgress?.('Generating translation keys...', 'keygen', 'running');
171
+ const { keyGeneration } = (0, keygen_1.generateKeys)(translatableStrings, existingData);
172
+ // gettext projects (Django): the catalogue of messages and the project's
173
+ // existing .po translations, which are kept and never re-translated.
174
+ const gettext = workspace.localeFormat === 'po'
175
+ ? await catalog_1.GettextCatalog.create(workspace, translatableStrings, keyGeneration.keys, config.targetLocales)
176
+ : null;
177
+ // ── Lockfile: partition keys ─────────────────────────────
178
+ let lockfileStats = { reused: 0, translated: 0, changed: 0 };
179
+ let tmHits = 0;
180
+ // (key, locale) translations the translator produced this run — the
181
+ // billable work. Lockfile/TM/cache reuse and failures are excluded.
182
+ let translatedThisRun = 0;
183
+ let keyUsage;
184
+ let translationMetadata = {};
185
+ if (config.projectId) {
186
+ config.onProgress?.('Checking lockfile for cached translations...', 'lockfile', 'running');
187
+ // Force re-translate: clear lockfile first
188
+ if (config.forceTranslation) {
189
+ await (0, lockfile_1.clearLockfile)(config.projectId, config.dataDir);
190
+ logger_1.engineLogger.info('Force translation: cleared lockfile', { projectId: config.projectId });
191
+ }
192
+ let lockfile = await (0, lockfile_1.loadLockfile)(config.projectId, config.dataDir);
193
+ config.onProgress?.('Processing source locale file...', 'locale', 'running');
194
+ const { localeHandling } = await (0, locale_builder_1.handleBaseLocale)(workspace, keyGeneration.keys, config.sourceLocale, config.dryRun);
195
+ // Build flat key→value map for partitioning: every key in the source
196
+ // locale, not just the ones extracted this run. Keys injected by earlier
197
+ // runs aren't re-extracted, and partitioning only this run's keys used to
198
+ // drop their translations from every target file (and left newly added
199
+ // locales with nothing to translate).
200
+ const flatKeys = sourceLocaleKeys(localeHandling.sourceLocale.data);
201
+ const contextMap = {};
202
+ const extractionContextMap = {};
203
+ for (const k of keyGeneration.keys) {
204
+ flatKeys[k.key] = k.value;
205
+ if (k.parentContext) {
206
+ contextMap[k.key] = k.parentContext;
207
+ }
208
+ if (k.extractionContext) {
209
+ extractionContextMap[k.key] = k.extractionContext;
210
+ }
211
+ }
212
+ // Advisory only: which source keys no code seems to reference. The scan
213
+ // is a heuristic (renamed translators, other file types, templates), so
214
+ // it never changes what is translated, kept or written.
215
+ keyUsage = await (0, key_usage_1.reportKeyUsage)(workspace, flatKeys, new Set(keyGeneration.keys.map((k) => k.key)));
216
+ // Translations already in the project's .po files win over the lockfile.
217
+ if (gettext)
218
+ lockfile = gettext.seedLockfile(lockfile, flatKeys);
219
+ // Pseudo-localization (--provider pseudo) is not a translation: a real
220
+ // run treats pseudo values in the lockfile as missing and translates them.
221
+ // The glossary is loaded once, here: keys whose glossary rules changed
222
+ // since their stored translation are re-translated (and only those).
223
+ // If the rules failed to load, stored translations are kept as they are
224
+ // rather than redone without the glossary.
225
+ const rules = await (0, translate_1.loadProjectTranslationRules)(config.projectId, config.providers?.rules);
226
+ const lockfileRead = {
227
+ pseudo: config.providers?.translator?.pseudo === true,
228
+ glossaryHash: rules.complete ? (source, locale) => (0, glossary_1.glossaryFingerprint)(source, locale, rules.glossary) : undefined,
229
+ };
230
+ const partition = (0, lockfile_1.partitionKeys)(lockfile, flatKeys, config.targetLocales, lockfileRead);
231
+ lockfileStats = (0, lockfile_1.computeLockfileStats)(partition);
232
+ logger_1.engineLogger.info('Lockfile partition result', {
233
+ unchanged: partition.unchanged.length,
234
+ newKeys: partition.newKeys.length,
235
+ changed: partition.changed.length,
236
+ missingLocales: partition.missingLocales.length,
237
+ });
238
+ // Collect cached translations for unchanged keys, and for keys that only
239
+ // lack some locales (a newly added language): their existing locales are
240
+ // still valid and must not drop out of the other locale files.
241
+ const cachedTranslations = (0, lockfile_1.collectCachedTranslations)(lockfile, [...partition.unchanged, ...partition.missingLocales], config.targetLocales, lockfileRead);
242
+ config.onProgress?.('Translating to target locales...', 'translation', 'running');
243
+ const translatorHealthy = await isTranslatorHealthy(config);
244
+ if (!translatorHealthy) {
245
+ const msg = 'Translator service is not responding — only source locale (en.json) was generated. Target locale files will be missing. Please ensure the translator service is running and try again.';
246
+ logger_1.engineLogger.warn(msg);
247
+ config.onProgress?.(msg, 'translation', 'warning');
248
+ }
249
+ // Only translate keys that need it (new + changed + missing locales)
250
+ const keysNeedingTranslation = [
251
+ ...partition.newKeys,
252
+ ...partition.changed,
253
+ ...partition.missingLocales,
254
+ ];
255
+ let translation;
256
+ if (keysNeedingTranslation.length === 0) {
257
+ logger_1.engineLogger.info('All keys reused from lockfile, skipping translation');
258
+ translation = { locales: [], errors: [], failures: [] };
259
+ }
260
+ else if (translatorHealthy) {
261
+ const keysToTranslateByLocale = (0, lockfile_1.buildKeysToTranslateByTargetLocale)(partition, config.targetLocales);
262
+ const result = await (0, translate_1.translateLocales)(workspace, localeHandling.sourceLocale, config.targetLocales, config.forceTranslation, config.dryRun, config.projectId, (msg) => config.onProgress?.(msg, 'translation', 'running'), keysToTranslateByLocale, config.providers?.cache, config.byok, contextMap, extractionContextMap, config.providers, { targetPlurals: icuOptions.targetPlurals, pluralValuePlaceholders: gettext?.pluralValuePlaceholders(), rules, progress: translationProgress });
263
+ translation = result.translation;
264
+ // A translation that wouldn't compile into the .po file is a failure, not output.
265
+ gettext?.validate(translation, flatKeys);
266
+ tmHits = result.stats.tmHits;
267
+ translationMetadata = result.metadata;
268
+ translatedThisRun = freshTranslations(translation, translationMetadata);
269
+ // Strings reused from the lockfile still make a good run: only fail
270
+ // when nothing at all is usable. Otherwise one string that keeps
271
+ // failing (and is retried every run) failed the whole project.
272
+ if (!hasCachedTranslations(cachedTranslations))
273
+ assertSomethingTranslated(translation);
274
+ }
275
+ else {
276
+ const errorMsg = 'Translator service unavailable — target locale files (hi.json, fr.json, etc.) were NOT generated. Only source locale (en.json) was created. Check that TRANSLATOR_URL or BYOK provider is configured and the service is running.';
277
+ logger_1.engineLogger.error(errorMsg);
278
+ config.onProgress?.(errorMsg, 'translation', 'error');
279
+ throw new Error(errorMsg);
280
+ }
281
+ // Merge cached translations into translation output locale data
282
+ for (const locale of translation.locales) {
283
+ const cached = cachedTranslations[locale.locale];
284
+ if (cached) {
285
+ const existingFlat = (0, keygen_1.flattenLocaleData)(locale.data);
286
+ const merged = { ...cached, ...existingFlat };
287
+ // Rebuild the locale data with cached + new
288
+ for (const [k, v] of Object.entries(merged)) {
289
+ setNestedKey(locale.data, k, v);
290
+ }
291
+ }
292
+ }
293
+ // Also create locale files for locales that were fully cached (no translator call)
294
+ for (const targetLocale of config.targetLocales) {
295
+ const existsInTranslation = translation.locales.some((l) => l.locale === targetLocale);
296
+ if (!existsInTranslation && cachedTranslations[targetLocale] && Object.keys(cachedTranslations[targetLocale]).length > 0) {
297
+ // Filter out cached translations that are identical to source text
298
+ // (identity translations mean the previous run's translator was unavailable)
299
+ // A locale where *every* cached value equals the source is what an
300
+ // unavailable translator used to leave behind: skip it. Otherwise
301
+ // keep all values — names and codes are legitimately identical, and
302
+ // dropping them would delete real translations.
303
+ const realTranslations = { ...cachedTranslations[targetLocale] };
304
+ const hasRealTranslations = Object.entries(realTranslations).some(([k, v]) => flatKeys[k] !== v);
305
+ if (!hasRealTranslations) {
306
+ logger_1.engineLogger.warn(`Skipping cached locale "${targetLocale}" — all cached entries are identity translations (same as source). Translator may be unavailable.`);
307
+ continue;
308
+ }
309
+ const cachedData = {};
310
+ for (const [k, v] of Object.entries(realTranslations)) {
311
+ setNestedKey(cachedData, k, v);
312
+ }
313
+ const localePath = (0, file_utils_1.localeFilePath)(workspace.path, workspace.publicDir, workspace.localeFormat, targetLocale);
314
+ translation.locales.push({
315
+ locale: targetLocale,
316
+ path: localePath.relativeFile,
317
+ data: cachedData,
318
+ isNew: false,
319
+ });
320
+ }
321
+ }
322
+ // Locales rebuilt from the lockfile carry no _meta block; give them the
323
+ // same one a freshly translated locale gets.
324
+ for (const locale of translation.locales) {
325
+ if (!locale.data._meta) {
326
+ locale.data._meta = { dir: (0, language_codes_1.isRTL)(locale.locale) ? 'rtl' : 'ltr', language: locale.locale };
327
+ }
328
+ }
329
+ // The translator wrote only this run's strings to disk; write the merged
330
+ // locale (lockfile + new) so the workspace — which the download ZIP is
331
+ // built from — has every translation, including fully cached locales.
332
+ if (!config.dryRun && workspace.localeFormat !== 'po') {
333
+ for (const locale of translation.locales) {
334
+ const { file } = (0, file_utils_1.localeFilePath)(workspace.path, workspace.publicDir, workspace.localeFormat, locale.locale);
335
+ await (0, file_utils_1.writeJsonFile)(file, locale.data, true);
336
+ }
337
+ }
338
+ // ── Lockfile: update after translation ─────────────────
339
+ config.onProgress?.('Updating lockfile...', 'lockfile', 'running');
340
+ const allTranslatedPerLocale = {};
341
+ for (const locale of translation.locales) {
342
+ allTranslatedPerLocale[locale.locale] = (0, keygen_1.flattenLocaleData)(locale.data);
343
+ }
344
+ // Only persist lockfile if we actually generated translation data,
345
+ // to prevent corrupting the lockfile when the translator was unavailable.
346
+ const hasRealTranslations = translation.locales.length > 0;
347
+ if (hasRealTranslations) {
348
+ // Lockfile-reused keys skip the translator, so they would otherwise
349
+ // never reach TM. Backfill them without overwriting existing entries —
350
+ // once: keys already marked inTm in the lockfile are skipped, so an
351
+ // unchanged project doesn't re-upsert every key on every run.
352
+ const lockfileMetadata = { ...translationMetadata };
353
+ if (lockfileRead.pseudo) {
354
+ // Record what this run produced as pseudo, so a later real run replaces it.
355
+ for (const [locale, keys] of Object.entries(translationMetadata)) {
356
+ lockfileMetadata[locale] = Object.fromEntries(Object.entries(keys).map(([k, m]) => [k, { ...(m ?? {}), pseudo: true }]));
357
+ }
358
+ }
359
+ if (!config.dryRun) {
360
+ const pending = {};
361
+ for (const [locale, keys] of Object.entries(cachedTranslations)) {
362
+ for (const [key, value] of Object.entries(keys)) {
363
+ if (lockfile.keys[key]?.metadata?.[locale]?.inTm)
364
+ continue;
365
+ (pending[locale] ?? (pending[locale] = {}))[key] = value;
366
+ }
367
+ }
368
+ try {
369
+ const settled = await (0, translate_1.backfillTranslationMemory)(config.projectId, config.sourceLocale, flatKeys, pending, new Set(Object.keys(contextMap)), config.providers);
370
+ for (const [locale, keys] of Object.entries(settled)) {
371
+ const forLocale = (lockfileMetadata[locale] = { ...(lockfileMetadata[locale] ?? {}) });
372
+ for (const key of keys) {
373
+ forLocale[key] = { ...(lockfile.keys[key]?.metadata?.[locale] ?? {}), ...(forLocale[key] ?? {}), inTm: true };
374
+ }
375
+ }
376
+ }
377
+ catch (err) {
378
+ logger_1.engineLogger.error('TM backfill from lockfile failed', {
379
+ projectId: config.projectId,
380
+ error: err instanceof Error ? err.message : String(err),
381
+ });
382
+ }
383
+ }
384
+ const previousLockfile = lockfile;
385
+ lockfile = (0, lockfile_1.updateLockfile)(lockfile, flatKeys, allTranslatedPerLocale, config.projectId, lockfileMetadata);
386
+ lockfile = (0, lockfile_1.dropFailedTranslations)(lockfile, translation.failures ?? []);
387
+ // Backup-translator (NLLB) output is shipped in the locale file but not
388
+ // recorded as done, so the next run retries it with the AI translator.
389
+ lockfile = (0, lockfile_1.dropFailedTranslations)(lockfile, fallbackTranslations(translationMetadata));
390
+ // Users commit the lockfile: an unchanged one is not rewritten (no
391
+ // new timestamp, no diff).
392
+ const lockfileOnDisk = await fs.pathExists((0, lockfile_1.getLockfilePath)(config.projectId, config.dataDir));
393
+ if (!config.dryRun && (!lockfileOnDisk || (0, lockfile_1.lockfileChanged)(previousLockfile, lockfile))) {
394
+ await (0, lockfile_1.saveLockfile)(config.projectId, lockfile, config.dataDir);
395
+ }
396
+ }
397
+ else {
398
+ logger_1.engineLogger.warn('Skipping lockfile update: no translation data generated (translator may be unavailable)', {
399
+ projectId: config.projectId,
400
+ });
401
+ }
402
+ const result = await finalizePipeline({
403
+ extraction, keyGeneration, localeHandling, translation,
404
+ config, startTime, workspace, allTranslatedPerLocale,
405
+ lockfileStats, tmHits, translationMetadata,
406
+ translatedThisRun, keyUsage,
407
+ formatStrings, frameworkConfig, gettext,
408
+ });
409
+ return result;
410
+ }
411
+ // ── No projectId: run without lockfile (legacy path) ─────
412
+ const contextMap = {};
413
+ const extractionContextMap = {};
414
+ for (const k of keyGeneration.keys) {
415
+ if (k.parentContext) {
416
+ contextMap[k.key] = k.parentContext;
417
+ }
418
+ if (k.extractionContext) {
419
+ extractionContextMap[k.key] = k.extractionContext;
420
+ }
421
+ }
422
+ config.onProgress?.('Processing source locale file...', 'locale', 'running');
423
+ const { localeHandling } = await (0, locale_builder_1.handleBaseLocale)(workspace, keyGeneration.keys, config.sourceLocale, config.dryRun);
424
+ config.onProgress?.('Translating to target locales...', 'translation', 'running');
425
+ const translatorHealthy = await isTranslatorHealthy(config);
426
+ if (!translatorHealthy) {
427
+ const msg = 'Translator service is not responding — only source locale (en.json) was generated. Target locale files will be missing. Please ensure the translator service is running and try again.';
428
+ logger_1.engineLogger.warn(msg);
429
+ config.onProgress?.(msg, 'translation', 'warning');
430
+ }
431
+ const translationResult = translatorHealthy
432
+ ? await (0, translate_1.translateLocales)(workspace, localeHandling.sourceLocale, config.targetLocales, config.forceTranslation, config.dryRun, config.projectId, (msg) => config.onProgress?.(msg, 'translation', 'running'), undefined, config.providers?.cache, config.byok, contextMap, extractionContextMap, config.providers, { targetPlurals: icuOptions.targetPlurals, pluralValuePlaceholders: gettext?.pluralValuePlaceholders(), progress: translationProgress })
433
+ : {
434
+ translation: {
435
+ locales: [],
436
+ errors: config.targetLocales.map((l) => ({ locale: l, error: 'Translator service unavailable — target locale files were not generated. Only the source locale file was created.' })),
437
+ },
438
+ stats: { tmHits: 0 },
439
+ metadata: {}
440
+ };
441
+ const translation = translationResult.translation;
442
+ gettext?.validate(translation, (0, keygen_1.flattenLocaleData)(localeHandling.sourceLocale.data));
443
+ tmHits = translationResult.stats.tmHits;
444
+ translationMetadata = translationResult.metadata;
445
+ translatedThisRun = freshTranslations(translation, translationMetadata);
446
+ assertSomethingTranslated(translation);
447
+ // Fail immediately when translation errors occurred — before injection/finalization
448
+ if (translation.errors && translation.errors.length > 0) {
449
+ const errorMsg = translation.errors.map((e) => `${e.locale}: ${e.error}`).join('; ');
450
+ logger_1.engineLogger.error('Translation failed', { errors: errorMsg });
451
+ throw new Error(`Translation failed: ${errorMsg}`);
452
+ }
453
+ const allTranslatedPerLocale = {};
454
+ for (const locale of translation.locales) {
455
+ allTranslatedPerLocale[locale.locale] = (0, keygen_1.flattenLocaleData)(locale.data);
456
+ }
457
+ const result = await finalizePipeline({
458
+ extraction, keyGeneration, localeHandling, translation,
459
+ config, startTime, workspace, allTranslatedPerLocale,
460
+ lockfileStats: undefined, tmHits, translationMetadata,
461
+ translatedThisRun, keyUsage,
462
+ formatStrings, frameworkConfig, gettext,
463
+ });
464
+ return result;
465
+ }
466
+ catch (error) {
467
+ logger_1.engineLogger.error('Engine v2 error', { error: error?.message ?? String(error) });
468
+ return {
469
+ success: false,
470
+ error: error?.message ?? String(error),
471
+ ...(error instanceof RunAbortedError
472
+ ? { aborted: error.reason, partialTranslations: translationProgress.freshTranslations }
473
+ : {}),
474
+ stats: {
475
+ filesScanned: 0,
476
+ filesWithStrings: 0,
477
+ stringsExtracted: 0,
478
+ keysGenerated: 0,
479
+ filesModified: 0,
480
+ injectionsApplied: 0,
481
+ localeFilesCreated: 0,
482
+ processingTimeMs: Date.now() - startTime,
483
+ },
484
+ locales: [],
485
+ injections: { modifiedFiles: [], skippedFiles: [], totalInjections: 0 },
486
+ warnings: [],
487
+ skipped: [],
488
+ diffs: [],
489
+ };
490
+ }
491
+ }
492
+ /**
493
+ * (key, locale) translations the translator produced this run, in locales
494
+ * that made it into the output: the billable work. Values reused from
495
+ * translation memory or the cache, and source text copied through untouched
496
+ * (placeholders, codes), are not counted; failed keys are not in the output.
497
+ */
498
+ function fallbackTranslations(metadata) {
499
+ const out = [];
500
+ for (const [locale, keys] of Object.entries(metadata ?? {})) {
501
+ for (const [key, m] of Object.entries(keys ?? {}))
502
+ if (m?.fallback)
503
+ out.push({ key, locale });
504
+ }
505
+ return out;
506
+ }
507
+ function freshTranslations(translation, metadata) {
508
+ let n = 0;
509
+ for (const locale of translation.locales) {
510
+ const meta = metadata[locale.locale] ?? {};
511
+ for (const [key, value] of Object.entries((0, keygen_1.flattenLocaleData)(locale.data))) {
512
+ if (key === '_meta' || key.startsWith('_meta.') || !value)
513
+ continue;
514
+ const m = meta[key];
515
+ // Backup-translator output isn't billed: the next run redoes it with the AI translator.
516
+ if (m?.fromMemory || m?.fromCache || m?.passthrough || m?.fallback)
517
+ continue;
518
+ n++;
519
+ }
520
+ }
521
+ return n;
522
+ }
523
+ /** Flat key → source text for a source locale file, without the `_meta` block. */
524
+ function sourceLocaleKeys(data) {
525
+ const flat = {};
526
+ for (const [key, value] of Object.entries((0, keygen_1.flattenLocaleData)(data))) {
527
+ if (key === '_meta' || key.startsWith('_meta.'))
528
+ continue;
529
+ flat[key] = value;
530
+ }
531
+ return flat;
532
+ }
533
+ /**
534
+ * An injected TranslationProvider is the host's responsibility (it reports
535
+ * per-key failures itself), so only probe the built-in translator/BYOK
536
+ * endpoint when the engine is going to use it.
537
+ */
538
+ async function isTranslatorHealthy(config) {
539
+ if (config.providers?.translator)
540
+ return true;
541
+ return (0, translator_caller_1.checkTranslatorHealth)(config.byok);
542
+ }
543
+ function createEmptyResult(startTime, warnings = [], skipped = [], notInjected = []) {
544
+ return {
545
+ success: true,
546
+ notInjected,
547
+ stats: {
548
+ filesScanned: 0,
549
+ filesWithStrings: 0,
550
+ stringsExtracted: 0,
551
+ keysGenerated: 0,
552
+ filesModified: 0,
553
+ injectionsApplied: 0,
554
+ localeFilesCreated: 0,
555
+ processingTimeMs: Date.now() - startTime,
556
+ },
557
+ locales: [],
558
+ injections: { modifiedFiles: [], skippedFiles: [], totalInjections: 0 },
559
+ warnings,
560
+ skipped,
561
+ diffs: [],
562
+ };
563
+ }
564
+ /**
565
+ * Set a value in a nested object using a dot-separated key path.
566
+ * e.g. setNestedKey(obj, 'homepage.hero.title', 'Hello') →
567
+ * { homepage: { hero: { title: 'Hello' } } }
568
+ */
569
+ function setNestedKey(obj, dottedKey, value) {
570
+ const parts = dottedKey.split('.');
571
+ let current = obj;
572
+ for (let i = 0; i < parts.length - 1; i++) {
573
+ const part = parts[i];
574
+ if (!(part in current)) {
575
+ current[part] = {};
576
+ }
577
+ else if (typeof current[part] === 'string') {
578
+ logger_1.engineLogger.warn('Overwriting string value with object for nested key', {
579
+ key: dottedKey,
580
+ part,
581
+ });
582
+ current[part] = {};
583
+ }
584
+ current = current[part];
585
+ }
586
+ current[parts[parts.length - 1]] = value;
587
+ }
588
+ function hasCachedTranslations(cached) {
589
+ return Object.values(cached).some((locale) => Object.keys(locale).length > 0);
590
+ }
591
+ /**
592
+ * A run where translation was needed but no string in any locale could be
593
+ * translated is a failed run, not an empty success: fail it with the reasons
594
+ * so the job errors (and is not billed) instead of reporting success.
595
+ */
596
+ function assertSomethingTranslated(translation) {
597
+ const failures = translation.failures ?? [];
598
+ if (failures.length === 0 || translation.locales.length > 0)
599
+ return;
600
+ const byReason = new Map();
601
+ for (const f of failures)
602
+ byReason.set(f.reason, (byReason.get(f.reason) ?? 0) + 1);
603
+ const summary = [...byReason].map(([reason, n]) => `${n} ${reason}`).join(', ');
604
+ const sample = failures.find((f) => f.message)?.message;
605
+ throw new Error(`No strings could be translated (${summary})${sample ? `: ${sample}` : ''}`);
606
+ }
607
+ /**
608
+ * Add a changed file to the run's list, folding a later write to a file that is
609
+ * already listed (an entry point that was injected, then wired up) into one
610
+ * entry: the original from the first write, the content from the last.
611
+ */
612
+ function mergeChangedFile(list, changed) {
613
+ const existing = list.find((f) => f.file === changed.file);
614
+ if (!existing) {
615
+ list.push(changed);
616
+ return;
617
+ }
618
+ existing.modifiedContent = changed.modifiedContent;
619
+ existing.injections.push(...changed.injections);
620
+ existing.importAdded = existing.importAdded || changed.importAdded;
621
+ existing.hookAdded = existing.hookAdded || changed.hookAdded;
622
+ }
623
+ async function finalizePipeline(params) {
624
+ const { extraction, keyGeneration, localeHandling, translation, config, startTime, workspace, allTranslatedPerLocale, lockfileStats, tmHits, translationMetadata, formatStrings, frameworkConfig, translatedThisRun, keyUsage, gettext, } = params;
625
+ const keyMapping = (0, keygen_1.createKeyMapping)(keyGeneration.keys);
626
+ // Injection: AST-based i18n hook injection (skipped for frameworks like Django)
627
+ let injection;
628
+ if (frameworkConfig.phases.includes('inject')) {
629
+ config.onProgress?.('Injecting translations into code...', 'injection', 'running');
630
+ // Inject exactly the nodes extraction selected (by source offset).
631
+ const positions = new Map();
632
+ for (const str of extraction.strings)
633
+ if (str.offsets)
634
+ positions.set(str.id, new Set(str.offsets));
635
+ injection = await (0, injection_1.injectTranslations)(workspace, keyMapping, config.confidenceThreshold, config.dryRun, positions);
636
+ }
637
+ else {
638
+ injection = { modifiedFiles: [], skippedFiles: [], totalInjections: 0 };
639
+ }
640
+ const notInjected = frameworkConfig.phases.includes('inject')
641
+ ? [...(0, not_injected_1.findNotInjected)(keyGeneration, injection), ...(extraction.notInjected ?? [])]
642
+ : [];
643
+ if (notInjected.length > 0) {
644
+ logger_1.engineLogger.warn('Extracted strings not wired into code', { count: notInjected.length });
645
+ }
646
+ // Format injection: replace hardcoded dates/currencies with Intl.* API calls
647
+ if (frameworkConfig.phases.includes('format-inject') && config.formatConfig?.enabled && formatStrings && formatStrings.length > 0) {
648
+ config.onProgress?.('Injecting Intl.* format replacements...', 'injection', 'running');
649
+ const formatInjection = await (0, format_injection_1.injectFormats)(workspace, formatStrings, config.formatConfig, config.dryRun);
650
+ if (formatInjection.totalInjections > 0) {
651
+ for (const changed of formatInjection.modifiedFiles)
652
+ mergeChangedFile(injection.modifiedFiles, changed);
653
+ injection.totalInjections += formatInjection.totalInjections;
654
+ injection.skippedFiles.push(...formatInjection.skippedFiles);
655
+ }
656
+ logger_1.engineLogger.info('Format injection complete', {
657
+ totalInjections: formatInjection.totalInjections,
658
+ });
659
+ }
660
+ // gettext: merge this run's translations into the project's .po files.
661
+ const finalizationNotes = [];
662
+ if (gettext) {
663
+ gettext.refreshReferences(injection);
664
+ const written = await gettext.write(config.targetLocales, allTranslatedPerLocale, {
665
+ dryRun: config.dryRun,
666
+ pseudo: config.providers?.translator?.pseudo === true,
667
+ });
668
+ translation.locales = written.locales;
669
+ finalizationNotes.push(...written.notes);
670
+ }
671
+ // Finalization: install i18n deps, create config files, modify entry points
672
+ if (frameworkConfig.phases.includes('finalize')) {
673
+ config.onProgress?.('Finalizing project...', 'finalization', 'running');
674
+ const { finalization } = await (0, finalization_1.finalizeProject)(workspace, config.dryRun, {
675
+ targetLocales: config.targetLocales,
676
+ sourceLocale: config.sourceLocale,
677
+ installDependencies: config.installDependencies,
678
+ });
679
+ finalizationNotes.push(...(finalization.warnings ?? []));
680
+ // Every file finalization wrote (package.json, i18n config, entry point,
681
+ // next.config, layout, i18n.js, settings.py, a lockfile it updated) ships
682
+ // with the run like injected files — hosts stage modifiedFiles for the PR.
683
+ for (const changed of finalization.changedFiles ?? []) {
684
+ mergeChangedFile(injection.modifiedFiles, { ...changed, injections: [], importAdded: false, hookAdded: false });
685
+ }
686
+ if (finalization.dependenciesInvalid) {
687
+ if (finalization.packagesInstalled) {
688
+ logger_1.engineLogger.info('New dependencies installed successfully');
689
+ }
690
+ else {
691
+ logger_1.engineLogger.warn(finalization.warnings?.find((w) => w.includes('auto-install')) || 'New dependencies added. Please run your package manager install.');
692
+ }
693
+ }
694
+ if (finalization.configFileCreated) {
695
+ logger_1.engineLogger.info('Created i18n configuration file');
696
+ }
697
+ if (finalization.entryPointModified) {
698
+ logger_1.engineLogger.info('Injected i18n import into entry point');
699
+ }
700
+ }
701
+ config.onProgress?.('Generating summary...', 'summary', 'running');
702
+ const { stats, warnings, diffs } = (0, summary_1.generateSummary)(extraction, keyGeneration, localeHandling, translation, injection, startTime, lockfileStats, tmHits);
703
+ stats.translatedThisRun = Math.max(0, translatedThisRun);
704
+ logger_1.engineLogger.info('Processing complete', stats);
705
+ logger_1.engineLogger.debug('Readable summary', { summary: (0, summary_1.generateReadableSummary)(stats) });
706
+ const allLocales = [localeHandling.sourceLocale, ...translation.locales];
707
+ config.onProgress?.('Complete', 'complete', 'success');
708
+ const allSkipped = [...extraction.skippedFiles, ...injection.skippedFiles];
709
+ return {
710
+ success: true,
711
+ translationFailures: translation.failures ?? [],
712
+ stats,
713
+ locales: allLocales,
714
+ injections: injection,
715
+ warnings: [...warnings],
716
+ skipped: allSkipped,
717
+ diffs,
718
+ notes: finalizationNotes,
719
+ keyUsage,
720
+ notInjected,
721
+ results: {
722
+ source: (0, keygen_1.flattenLocaleData)(localeHandling.sourceLocale.data),
723
+ targets: allTranslatedPerLocale,
724
+ metadata: translationMetadata,
725
+ },
726
+ };
727
+ }