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