@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,319 @@
1
+ "use strict";
2
+ /**
3
+ * BYOK (Bring Your Own Key) Translation Provider
4
+ *
5
+ * Direct integration with Anthropic, OpenAI, Gemini, Groq and OpenRouter for
6
+ * users who provide their own API keys. Zero Babelize AI credits consumed.
7
+ *
8
+ * Contract: returns a translation only for entries the model actually
9
+ * translated. An entry it skipped or garbled is simply absent from the
10
+ * response, and the engine reports it as a failed key — never as the source
11
+ * text.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.BYOKTranslatorClient = void 0;
15
+ exports.buildSystemPrompt = buildSystemPrompt;
16
+ exports.parseTranslations = parseTranslations;
17
+ const types_1 = require("@babelize/types");
18
+ const retry_1 = require("./retry");
19
+ const language_codes_1 = require("../language-codes");
20
+ const icu_message_1 = require("../../../utils/icu-message");
21
+ // Rough character to token ratio
22
+ const CHAR_TO_TOKEN_RATIO = 4;
23
+ const RETRY = { baseDelayMs: 1000, maxRetries: 3, timeoutMs: 120000 };
24
+ const TOOL_NAME = 'submit_translations';
25
+ class BYOKTranslatorClient {
26
+ constructor(config) {
27
+ this.config = config;
28
+ }
29
+ async health() {
30
+ try {
31
+ const def = types_1.PROVIDER_DEFINITIONS[this.config.provider];
32
+ if (!def)
33
+ return false;
34
+ // Lightweight call that validates the key is live: list models.
35
+ if (this.config.provider === 'anthropic') {
36
+ const res = await fetch(`${def.baseUrl}/models`, {
37
+ headers: { 'x-api-key': this.config.apiKey, 'anthropic-version': '2023-06-01' },
38
+ signal: AbortSignal.timeout(10000),
39
+ });
40
+ return res.ok;
41
+ }
42
+ const res = await fetch(`${def.baseUrl}/models`, {
43
+ headers: { Authorization: `Bearer ${this.config.apiKey}` },
44
+ signal: AbortSignal.timeout(10000),
45
+ });
46
+ return res.ok;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ async warmup() {
53
+ return true;
54
+ }
55
+ async translate(req, signal) {
56
+ const entries = req.entries.map((e) => ({
57
+ id: e.key,
58
+ text: e.value,
59
+ ...(e.context ? { context: e.context } : {}),
60
+ ...(e.extractionContext ? { element: e.extractionContext.replace(/^hardcoded-/, '') } : {}),
61
+ ...(req.hints?.[e.key] ? { note: req.hints[e.key] } : {}),
62
+ }));
63
+ const system = buildSystemPrompt(req, entries);
64
+ const user = JSON.stringify({ entries }, null, 1);
65
+ // Resolved before any request: a missing model is a configuration error,
66
+ // not something a retry can fix.
67
+ const model = this.getModel();
68
+ const retry = { ...RETRY, signal };
69
+ const { content, usage } = this.config.provider === 'anthropic'
70
+ ? await this.callAnthropic(model, system, user, retry)
71
+ : await this.callOpenAICompatible(model, system, user, retry);
72
+ const ids = new Set(entries.map((e) => e.id));
73
+ return {
74
+ translations: parseTranslations(content, ids),
75
+ model,
76
+ metadata: { provider: this.config.provider, usage },
77
+ };
78
+ }
79
+ getModel() {
80
+ if (this.config.model)
81
+ return this.config.model;
82
+ const def = types_1.PROVIDER_DEFINITIONS[this.config.provider];
83
+ if (!def?.defaultModel) {
84
+ throw new Error(`No model configured for ${this.getProviderDisplayName()}. Pass a model explicitly.`);
85
+ }
86
+ return def.defaultModel;
87
+ }
88
+ getBaseUrl() {
89
+ const def = types_1.PROVIDER_DEFINITIONS[this.config.provider];
90
+ if (!def)
91
+ throw new Error(`Unknown provider: ${this.config.provider}`);
92
+ if (this.config.provider === 'anthropic') {
93
+ return `${def.baseUrl}/messages`;
94
+ }
95
+ return `${def.baseUrl}/chat/completions`;
96
+ }
97
+ getProviderDisplayName() {
98
+ return types_1.PROVIDER_DEFINITIONS[this.config.provider]?.name || this.config.provider;
99
+ }
100
+ /** Anthropic: force a tool call so the output is schema-shaped JSON. */
101
+ async callAnthropic(model, system, user, retry = RETRY) {
102
+ const response = await (0, retry_1.withRetry)(async (signal) => {
103
+ const res = await fetch(this.getBaseUrl(), {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ 'x-api-key': this.config.apiKey,
108
+ 'anthropic-version': '2023-06-01',
109
+ },
110
+ body: JSON.stringify({
111
+ model,
112
+ max_tokens: 8192,
113
+ temperature: 0,
114
+ system,
115
+ tools: [{
116
+ name: TOOL_NAME,
117
+ description: 'Submit the translation of every entry.',
118
+ input_schema: {
119
+ type: 'object',
120
+ properties: {
121
+ translations: {
122
+ type: 'array',
123
+ items: {
124
+ type: 'object',
125
+ properties: { id: { type: 'string' }, value: { type: 'string' } },
126
+ required: ['id', 'value'],
127
+ },
128
+ },
129
+ },
130
+ required: ['translations'],
131
+ },
132
+ }],
133
+ tool_choice: { type: 'tool', name: TOOL_NAME },
134
+ messages: [{ role: 'user', content: user }],
135
+ }),
136
+ signal,
137
+ });
138
+ if (!res.ok)
139
+ throw await this.httpError(res);
140
+ return res.json();
141
+ }, retry);
142
+ const block = (response.content ?? []).find((b) => b.type === 'tool_use');
143
+ return { content: block?.input ?? response.content?.find((b) => b.type === 'text')?.text, usage: response.usage };
144
+ }
145
+ async callOpenAICompatible(model, system, user, retry = RETRY) {
146
+ const body = {
147
+ model,
148
+ messages: [
149
+ { role: 'system', content: system },
150
+ { role: 'user', content: user },
151
+ ],
152
+ };
153
+ // Reasoning models reject a temperature other than the default.
154
+ if (!/^(?:openai\/)?(?:o\d|gpt-5)/.test(model))
155
+ body.temperature = 0;
156
+ // JSON mode where the API guarantees it.
157
+ if (this.config.provider === 'openai' || this.config.provider === 'groq') {
158
+ body.response_format = { type: 'json_object' };
159
+ }
160
+ const response = await (0, retry_1.withRetry)(async (signal) => {
161
+ const res = await fetch(this.getBaseUrl(), {
162
+ method: 'POST',
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ Authorization: `Bearer ${this.config.apiKey}`,
166
+ },
167
+ body: JSON.stringify(body),
168
+ signal,
169
+ });
170
+ if (!res.ok)
171
+ throw await this.httpError(res);
172
+ return res.json();
173
+ }, retry);
174
+ return { content: response.choices?.[0]?.message?.content ?? '', usage: response.usage };
175
+ }
176
+ async httpError(res) {
177
+ const detail = this.redactApiKey((await res.text().catch(() => '')).slice(0, 300));
178
+ return new retry_1.ProviderHttpError(this.formatApiError(res.status, detail), res.status, (0, retry_1.parseRetryAfter)(res.headers.get('retry-after')));
179
+ }
180
+ formatApiError(status, detail) {
181
+ const name = this.getProviderDisplayName();
182
+ switch (status) {
183
+ case 401:
184
+ case 403:
185
+ return `${name} API error: Invalid or expired API key. Check your key in Settings.`;
186
+ case 402:
187
+ return `${name} API error: Insufficient credits on your account.`;
188
+ case 429:
189
+ return `${name} API error: Rate limit exceeded. Please wait and try again, or upgrade your plan.`;
190
+ case 404:
191
+ return `${name} API error: Model "${this.getModel()}" was not found. Check the selected model.`;
192
+ default:
193
+ if (status >= 500) {
194
+ return `${name} API error (${status}). Server issue — please try again later.`;
195
+ }
196
+ return `${name} API error (${status})${detail ? `: ${detail}` : '. Please try again.'}`;
197
+ }
198
+ }
199
+ redactApiKey(body) {
200
+ if (typeof body === 'string' && this.config.apiKey) {
201
+ return body.split(this.config.apiKey).join('[REDACTED]');
202
+ }
203
+ return body;
204
+ }
205
+ /**
206
+ * Estimate token count and cost for a translation request
207
+ */
208
+ static estimateCost(entries, provider) {
209
+ const totalChars = entries.reduce((sum, e) => sum + e.value.length, 0);
210
+ const estimatedTokens = Math.ceil(totalChars / CHAR_TO_TOKEN_RATIO);
211
+ const pricing = types_1.PROVIDER_PRICING[provider];
212
+ const estimatedCost = pricing
213
+ ? Math.round((estimatedTokens / 1000000) * pricing.input * 10000) / 10000
214
+ : null;
215
+ return {
216
+ estimatedTokens,
217
+ estimatedCost,
218
+ currency: 'USD',
219
+ };
220
+ }
221
+ }
222
+ exports.BYOKTranslatorClient = BYOKTranslatorClient;
223
+ function languageName(code) {
224
+ try {
225
+ const name = new Intl.DisplayNames(['en'], { type: 'language' }).of(code);
226
+ return name && name !== code ? `${name} (${code})` : code;
227
+ }
228
+ catch {
229
+ return code;
230
+ }
231
+ }
232
+ function safeFromFlores(flores) {
233
+ try {
234
+ return (0, language_codes_1.fromFlores)(flores);
235
+ }
236
+ catch {
237
+ return flores;
238
+ }
239
+ }
240
+ function buildSystemPrompt(req, entries) {
241
+ const targetCode = req.targetLocale || safeFromFlores(req.targetLang);
242
+ const source = languageName(safeFromFlores(req.sourceLang));
243
+ const target = languageName(targetCode);
244
+ const rules = [
245
+ `You are a professional software localizer. Translate the user-interface strings of a web application from ${source} into ${target}.`,
246
+ '',
247
+ 'Input: a JSON object with `entries`. Each entry has `id` (the string key in the codebase — a hint about meaning, never translate it), `text` (translate this), and optionally `context` (the full sentence this fragment belongs to — use it to pick the right words, but translate only `text`), `element` (where the string appears, e.g. button, label, placeholder, aria-label) and `note` (a correction from a previous attempt — follow it).',
248
+ '',
249
+ `Output: call ${TOOL_NAME} (or reply with only a JSON object) of the form {"translations":[{"id":"…","value":"…"}]}, with exactly one item per input id and nothing else.`,
250
+ '',
251
+ 'Rules:',
252
+ '1. Tokens such as [#VAR_0#] stand for variables, markup tags, links or brand names. Copy every token exactly once and unchanged; move it if the target grammar needs a different word order. Never translate, add, merge or drop tokens.',
253
+ '2. Write natural, idiomatic UI text as a native speaker would — not a word-for-word rendering. Keep it concise: buttons, labels and menu items should stay short.',
254
+ '3. Use one consistent formality level and terminology across all entries, matching what is conventional for modern software in the target language.',
255
+ "4. Follow the target language's punctuation, capitalization and spacing conventions. Keep any leading or trailing whitespace.",
256
+ '5. If a text is a proper name, a code identifier, or is already correct in the target language (e.g. "OK", "Email" where that is the norm), return it unchanged.',
257
+ '6. Never explain, never add quotes or notes, never leave text in the source language unless rule 5 applies.',
258
+ ];
259
+ if (entries.some((e) => (0, icu_message_1.isIcuMessage)(e.text))) {
260
+ const { cardinal, ordinal } = (0, icu_message_1.requiredPluralCategories)(targetCode);
261
+ rules.push(`7. Some texts are ICU MessageFormat messages ({name, plural, …}, {name, select, …}, {name, selectordinal, …}). Keep argument names, select keys, "=N" keys, "#", and any tags, links and code exactly; translate only the text inside the branches. For plural use exactly the ${target} categories [${cardinal.join(', ')}] and for selectordinal [${ordinal.join(', ')}], always including "other". The result must be a valid ICU message.`);
262
+ }
263
+ if (entries.some((e) => (0, icu_message_1.countArguments)(e.text).length > 0)) {
264
+ const { cardinal } = (0, icu_message_1.requiredPluralCategories)(targetCode);
265
+ rules.push(`8. Some texts contain {name} arguments in braces instead of tokens. Keep every argument name and tag exactly. When the note says an argument is a number and ${target} changes the surrounding words by number, return an ICU plural over that argument, e.g. {files, plural, one {# …} other {# …}}, with exactly the ${target} categories [${cardinal.join(', ')}], "other" included and "#" in every branch. If ${target} doesn't change words by number, return plain text with the {name} argument.`);
266
+ }
267
+ if (req.glossary?.length) {
268
+ rules.push('', 'Required terminology — use these translations whenever the term appears (inflect them if grammar requires):');
269
+ for (const g of req.glossary)
270
+ rules.push(`- "${g.term}" → "${g.translation}"`);
271
+ }
272
+ if (req.brandVoice) {
273
+ rules.push('', 'Brand voice — follow this tone and style; it takes priority over rule 3:', req.brandVoice);
274
+ }
275
+ return rules.join('\n');
276
+ }
277
+ /**
278
+ * Pull `{translations:[{id, value}]}` out of a tool input object or a text
279
+ * reply (tolerating code fences and surrounding prose). Returns only entries
280
+ * whose id was requested and whose value is a string; nothing is invented.
281
+ */
282
+ function parseTranslations(content, requestedIds) {
283
+ let data = content;
284
+ if (typeof content === 'string') {
285
+ data = parseJsonLoosely(content);
286
+ }
287
+ const list = data?.translations;
288
+ if (!Array.isArray(list))
289
+ return [];
290
+ const out = [];
291
+ const seen = new Set();
292
+ for (const item of list) {
293
+ const id = item && typeof item === 'object' ? String(item.id ?? '') : '';
294
+ const value = item?.value;
295
+ if (!requestedIds.has(id) || seen.has(id) || typeof value !== 'string')
296
+ continue;
297
+ seen.add(id);
298
+ out.push({ key: id, value });
299
+ }
300
+ return out;
301
+ }
302
+ function parseJsonLoosely(text) {
303
+ const trimmed = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
304
+ try {
305
+ return JSON.parse(trimmed);
306
+ }
307
+ catch {
308
+ const start = trimmed.indexOf('{');
309
+ const end = trimmed.lastIndexOf('}');
310
+ if (start === -1 || end <= start)
311
+ return null;
312
+ try {
313
+ return JSON.parse(trimmed.slice(start, end + 1));
314
+ }
315
+ catch {
316
+ return null;
317
+ }
318
+ }
319
+ }
@@ -0,0 +1,12 @@
1
+ import type { HFTranslateRequest, HFTranslateResponse } from './request.types';
2
+ import type { TranslatorClient } from './translator.interface';
3
+ /**
4
+ * HTTP client for the Babelize translator service.
5
+ */
6
+ export declare class DirectTranslatorClient implements TranslatorClient {
7
+ private readonly baseUrl;
8
+ constructor(baseUrl?: string);
9
+ health(): Promise<boolean>;
10
+ warmup(): Promise<boolean>;
11
+ translate(req: HFTranslateRequest): Promise<HFTranslateResponse>;
12
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DirectTranslatorClient = void 0;
4
+ const retry_1 = require("./retry");
5
+ /**
6
+ * HTTP client for the Babelize translator service.
7
+ */
8
+ class DirectTranslatorClient {
9
+ constructor(baseUrl) {
10
+ const url = baseUrl?.trim() || process.env.TRANSLATOR_URL?.trim();
11
+ if (!url) {
12
+ throw new Error('TRANSLATOR_URL environment variable is required');
13
+ }
14
+ this.baseUrl = url;
15
+ }
16
+ async health() {
17
+ return await (0, retry_1.withRetry503)(async (signal) => {
18
+ const res = await fetch(`${this.baseUrl}/health`, { method: 'GET', signal });
19
+ return res.ok;
20
+ }, { baseDelayMs: 5000, maxRetries: 3, timeoutMs: 30000 });
21
+ }
22
+ async warmup() {
23
+ // Warmup should be tolerant of cold starts; 30s per call, exponential backoff.
24
+ try {
25
+ return await (0, retry_1.withRetry503)(async (signal) => {
26
+ const res = await fetch(`${this.baseUrl}/warmup`, { method: 'POST', signal });
27
+ return res.ok;
28
+ }, { baseDelayMs: 5000, maxRetries: 3, timeoutMs: 30000 });
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ async translate(req) {
35
+ const payload = {
36
+ entries: req.entries,
37
+ sourceLang: req.sourceLang,
38
+ targetLang: req.targetLang,
39
+ };
40
+ const res = await (0, retry_1.withRetry503)(async (signal) => {
41
+ const r = await fetch(`${this.baseUrl}/translate`, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify(payload),
45
+ signal,
46
+ });
47
+ if (r.status === 503) {
48
+ throw new Error('Translator service warming (503)');
49
+ }
50
+ if (!r.ok) {
51
+ const body = await safeReadText(r);
52
+ throw new Error(`Translate failed: ${r.status} ${r.statusText}${body ? ` — ${body}` : ''}`);
53
+ }
54
+ const data = (await r.json());
55
+ if (!data || !Array.isArray(data.translations)) {
56
+ throw new Error('Translate returned invalid response');
57
+ }
58
+ return data;
59
+ }, { baseDelayMs: 5000, maxRetries: 3, timeoutMs: 30000 });
60
+ return res;
61
+ }
62
+ }
63
+ exports.DirectTranslatorClient = DirectTranslatorClient;
64
+ async function safeReadText(res) {
65
+ try {
66
+ return await res.text();
67
+ }
68
+ catch {
69
+ return '';
70
+ }
71
+ }
@@ -0,0 +1 @@
1
+ export declare function polishWithBrandVoice(translations: Record<string, string>, brandVoice: string, targetLocale: string): Promise<Record<string, string>>;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.polishWithBrandVoice = polishWithBrandVoice;
4
+ const env_1 = require("../../../config/env");
5
+ const logger_1 = require("../../../utils/logger");
6
+ const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai';
7
+ const DEFAULT_MODEL = 'models/gemini-3.1-flash-lite';
8
+ function getGeminiApiKey() {
9
+ return env_1.config.GEMINI_API_KEY?.trim() || null;
10
+ }
11
+ function buildSystemPrompt(brandVoice) {
12
+ return `You are a brand voice editor. Adjust the tone and style of the following translations to match this brand voice:
13
+
14
+ "${brandVoice}"
15
+
16
+ Rules:
17
+ - Never change the meaning, only the tone
18
+ - Preserve ALL keys exactly as provided
19
+ - Return ONLY valid JSON with the same keys
20
+ - Preserve all placeholders like {0}, {name}, [#VAR_0#], etc.
21
+ - Keep proper nouns and technical terms unchanged`;
22
+ }
23
+ function buildUserMessage(translations, targetLocale) {
24
+ return `Polish these ${targetLocale} translations to match the brand voice. Return JSON with the same keys:
25
+
26
+ ${JSON.stringify(translations, null, 2)}
27
+
28
+ Return ONLY the JSON response:`;
29
+ }
30
+ function parseResponse(content, originalKeys) {
31
+ try {
32
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
33
+ if (!jsonMatch)
34
+ return null;
35
+ const parsed = JSON.parse(jsonMatch[0]);
36
+ const translations = parsed.translations || parsed.polished || parsed;
37
+ if (typeof translations !== 'object' || translations === null)
38
+ return null;
39
+ const result = {};
40
+ for (const key of originalKeys) {
41
+ const val = translations[key];
42
+ result[key] = typeof val === 'string' ? val : '';
43
+ }
44
+ for (const key of originalKeys) {
45
+ if (!result[key]) {
46
+ return null;
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function polishWithBrandVoice(translations, brandVoice, targetLocale) {
56
+ const apiKey = getGeminiApiKey();
57
+ if (!apiKey) {
58
+ logger_1.engineLogger.warn('GEMINI_API_KEY not set, skipping brand voice polish');
59
+ return translations;
60
+ }
61
+ const originalKeys = Object.keys(translations);
62
+ if (originalKeys.length === 0)
63
+ return translations;
64
+ try {
65
+ const response = await fetch(`${GEMINI_BASE_URL}/chat/completions`, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ Authorization: `Bearer ${apiKey}`,
70
+ },
71
+ body: JSON.stringify({
72
+ model: DEFAULT_MODEL,
73
+ messages: [
74
+ { role: 'system', content: buildSystemPrompt(brandVoice) },
75
+ { role: 'user', content: buildUserMessage(translations, targetLocale) },
76
+ ],
77
+ temperature: 0.3,
78
+ max_tokens: Math.min(originalKeys.length * 200, 8192),
79
+ }),
80
+ signal: AbortSignal.timeout(30000),
81
+ });
82
+ if (!response.ok) {
83
+ const errorText = await response.text().catch(() => '');
84
+ logger_1.engineLogger.warn('Gemini brand voice API error', {
85
+ status: response.status,
86
+ error: errorText,
87
+ });
88
+ return translations;
89
+ }
90
+ const data = await response.json();
91
+ const content = data.choices?.[0]?.message?.content || '';
92
+ const polished = parseResponse(content, originalKeys);
93
+ if (!polished) {
94
+ logger_1.engineLogger.warn('Gemini brand voice response parse failed, falling back to original');
95
+ return translations;
96
+ }
97
+ logger_1.engineLogger.info('Brand voice polish applied via Gemini', {
98
+ model: data.model || DEFAULT_MODEL,
99
+ keys: originalKeys.length,
100
+ usage: data.usage,
101
+ });
102
+ return polished;
103
+ }
104
+ catch (err) {
105
+ logger_1.engineLogger.warn('Gemini brand voice request failed, falling back to original', {
106
+ error: err instanceof Error ? err.message : String(err),
107
+ });
108
+ return translations;
109
+ }
110
+ }
@@ -0,0 +1 @@
1
+ export declare function polishWithBrandVoice(translations: Record<string, string>, brandVoice: string, targetLocale: string): Promise<Record<string, string>>;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.polishWithBrandVoice = polishWithBrandVoice;
4
+ const env_1 = require("../../../config/env");
5
+ const logger_1 = require("../../../utils/logger");
6
+ const GROQ_BASE_URL = 'https://api.groq.com/openai/v1';
7
+ function getGroqApiKey() {
8
+ return env_1.config.GROQ_API_KEY?.trim() || null;
9
+ }
10
+ function buildSystemPrompt(brandVoice) {
11
+ return `You are a brand voice editor. Adjust the tone and style of the following translations to match this brand voice:
12
+
13
+ "${brandVoice}"
14
+
15
+ Rules:
16
+ - Never change the meaning, only the tone
17
+ - Preserve ALL keys exactly as provided
18
+ - Return ONLY valid JSON with the same keys
19
+ - Preserve all placeholders like {0}, {name}, [#VAR_0#], etc.
20
+ - Keep proper nouns and technical terms unchanged`;
21
+ }
22
+ function buildUserMessage(translations, targetLocale) {
23
+ return `Polish these ${targetLocale} translations to match the brand voice. Return JSON with the same keys:
24
+
25
+ ${JSON.stringify(translations, null, 2)}
26
+
27
+ Return ONLY the JSON response:`;
28
+ }
29
+ function parseResponse(content, originalKeys) {
30
+ try {
31
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
32
+ if (!jsonMatch)
33
+ return null;
34
+ const parsed = JSON.parse(jsonMatch[0]);
35
+ // Try to find the translations object - could be the top-level or nested
36
+ const translations = parsed.translations || parsed.polished || parsed;
37
+ if (typeof translations !== 'object' || translations === null)
38
+ return null;
39
+ const result = {};
40
+ for (const key of originalKeys) {
41
+ const val = translations[key];
42
+ result[key] = typeof val === 'string' ? val : '';
43
+ }
44
+ // Ensure all keys are present (fallback to original if missing)
45
+ for (const key of originalKeys) {
46
+ if (!result[key]) {
47
+ return null; // Missing keys, return null to trigger fallback
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ async function polishWithBrandVoice(translations, brandVoice, targetLocale) {
57
+ const apiKey = getGroqApiKey();
58
+ if (!apiKey) {
59
+ logger_1.engineLogger.warn('GROQ_API_KEY not set, skipping brand voice polish');
60
+ return translations;
61
+ }
62
+ const originalKeys = Object.keys(translations);
63
+ if (originalKeys.length === 0)
64
+ return translations;
65
+ try {
66
+ const response = await fetch(`${GROQ_BASE_URL}/chat/completions`, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ Authorization: `Bearer ${apiKey}`,
71
+ },
72
+ body: JSON.stringify({
73
+ model: 'llama-3.3-70b-versatile',
74
+ messages: [
75
+ { role: 'system', content: buildSystemPrompt(brandVoice) },
76
+ { role: 'user', content: buildUserMessage(translations, targetLocale) },
77
+ ],
78
+ temperature: 0.3,
79
+ max_tokens: Math.min(originalKeys.length * 200, 4096),
80
+ }),
81
+ signal: AbortSignal.timeout(30000),
82
+ });
83
+ if (!response.ok) {
84
+ const errorText = await response.text().catch(() => '');
85
+ logger_1.engineLogger.warn('Groq brand voice API error', {
86
+ status: response.status,
87
+ error: errorText,
88
+ });
89
+ return translations;
90
+ }
91
+ const data = await response.json();
92
+ const content = data.choices?.[0]?.message?.content || '';
93
+ const polished = parseResponse(content, originalKeys);
94
+ if (!polished) {
95
+ logger_1.engineLogger.warn('Groq brand voice response parse failed, falling back to original');
96
+ return translations;
97
+ }
98
+ logger_1.engineLogger.info('Brand voice polish applied via Groq', {
99
+ model: data.model || 'llama-3.3-70b-versatile',
100
+ keys: originalKeys.length,
101
+ usage: data.usage,
102
+ });
103
+ return polished;
104
+ }
105
+ catch (err) {
106
+ logger_1.engineLogger.warn('Groq brand voice request failed, falling back to original', {
107
+ error: err instanceof Error ? err.message : String(err),
108
+ });
109
+ return translations;
110
+ }
111
+ }
@@ -0,0 +1 @@
1
+ export type { HFTranslateRequest, HFTranslateResponse } from '../../../types';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,29 @@
1
+ export interface RetryOptions {
2
+ baseDelayMs: number;
3
+ maxRetries: number;
4
+ timeoutMs: number;
5
+ /** Upper bound for a single wait, including server-requested Retry-After. */
6
+ maxDelayMs?: number;
7
+ /** Aborts the attempt in flight and stops any further retries. */
8
+ signal?: AbortSignal;
9
+ }
10
+ /** An HTTP error from a translation API, carrying what retry decisions need. */
11
+ export declare class ProviderHttpError extends Error {
12
+ readonly status: number;
13
+ /** Server-requested wait from a Retry-After header, in ms. */
14
+ readonly retryAfterMs?: number | undefined;
15
+ constructor(message: string, status: number,
16
+ /** Server-requested wait from a Retry-After header, in ms. */
17
+ retryAfterMs?: number | undefined);
18
+ }
19
+ /** Parse a Retry-After header (seconds or HTTP date) into milliseconds. */
20
+ export declare function parseRetryAfter(header: string | null): number | undefined;
21
+ /**
22
+ * Retry only what can succeed on a second try: timeouts, network errors,
23
+ * 408/409/425/429 and 5xx. Auth, bad request and unknown model (other 4xx)
24
+ * fail immediately — retrying a bad key just delays the error.
25
+ */
26
+ export declare function isRetryable(err: unknown): boolean;
27
+ export declare function withRetry<T>(fn: (signal: AbortSignal) => Promise<T>, opts: RetryOptions): Promise<T>;
28
+ /** @deprecated Use withRetry — kept for existing callers. */
29
+ export declare const withRetry503: typeof withRetry;