@babelize/engine 0.2.0 → 0.3.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 (33) hide show
  1. package/dist/analysis/analyze.js +1 -1
  2. package/dist/detection/i18n.d.ts +19 -0
  3. package/dist/detection/i18n.js +144 -0
  4. package/dist/detection/stack.d.ts +2 -0
  5. package/dist/index.d.ts +4 -1
  6. package/dist/index.js +9 -2
  7. package/dist/injection/html/html-injector.d.ts +2 -0
  8. package/dist/injection/html/html-injector.js +55 -6
  9. package/dist/local/hosted-provider.d.ts +30 -2
  10. package/dist/local/hosted-provider.js +50 -3
  11. package/dist/local/translate-core.d.ts +2 -2
  12. package/dist/local/translate-core.js +6 -2
  13. package/dist/modules/finalization/finalize.js +6 -4
  14. package/dist/modules/finalization/static-runtime.d.ts +2 -2
  15. package/dist/modules/finalization/static-runtime.js +10 -5
  16. package/dist/modules/lockfile/index.d.ts +1 -1
  17. package/dist/modules/lockfile/index.js +2 -1
  18. package/dist/modules/lockfile/lockfile-manager.d.ts +9 -0
  19. package/dist/modules/lockfile/lockfile-manager.js +43 -1
  20. package/dist/modules/lockfile/schema.d.ts +14 -0
  21. package/dist/modules/lockfile/schema.js +2 -0
  22. package/dist/modules/overrides/index.d.ts +41 -0
  23. package/dist/modules/overrides/index.js +50 -0
  24. package/dist/modules/translator/budget.d.ts +25 -0
  25. package/dist/modules/translator/budget.js +46 -0
  26. package/dist/modules/translator/translate.d.ts +11 -0
  27. package/dist/modules/translator/translate.js +50 -10
  28. package/dist/orchestrator.js +54 -6
  29. package/dist/providers/types.d.ts +6 -0
  30. package/dist/runtime/config.js +1 -0
  31. package/dist/runtime/types.d.ts +21 -0
  32. package/dist/utils/translator-caller.js +6 -3
  33. package/package.json +1 -1
@@ -52,6 +52,7 @@ const finalization_1 = require("./modules/finalization");
52
52
  const lockfile_1 = require("./modules/lockfile");
53
53
  const file_utils_1 = require("./utils/file-utils");
54
54
  const key_usage_1 = require("./utils/key-usage");
55
+ const overrides_1 = require("./modules/overrides");
55
56
  const not_injected_1 = require("./utils/not-injected");
56
57
  const language_codes_1 = require("./modules/translator/language-codes");
57
58
  const format_injection_1 = require("./modules/format-injection");
@@ -180,6 +181,8 @@ async function runEngineV2(config) {
180
181
  // (key, locale) translations the translator produced this run — the
181
182
  // billable work. Lockfile/TM/cache reuse and failures are excluded.
182
183
  let translatedThisRun = 0;
184
+ // (key, locale) pairs left for the next run: the run's maxTranslations ran out.
185
+ let skippedForBudget = 0;
183
186
  let keyUsage;
184
187
  let translationMetadata = {};
185
188
  if (config.projectId) {
@@ -239,6 +242,20 @@ async function runEngineV2(config) {
239
242
  // lack some locales (a newly added language): their existing locales are
240
243
  // still valid and must not drop out of the other locale files.
241
244
  const cachedTranslations = (0, lockfile_1.collectCachedTranslations)(lockfile, [...partition.unchanged, ...partition.missingLocales], config.targetLocales, lockfileRead);
245
+ // Dashboard edits (overrides), loaded before translating: a pair with a
246
+ // current edit is never sent to the translator (it would be thrown away).
247
+ let overrides = [];
248
+ if (config.providers?.overrides) {
249
+ try {
250
+ overrides = await config.providers.overrides.getOverrides(config.projectId);
251
+ }
252
+ catch (err) {
253
+ logger_1.engineLogger.warn('Could not load translation overrides; translating without them', {
254
+ error: err instanceof Error ? err.message : String(err),
255
+ });
256
+ }
257
+ }
258
+ const overridden = new Set(overrides.filter((o) => flatKeys[o.key] === o.sourceText).map((o) => `${o.locale}\u0000${o.key}`));
242
259
  config.onProgress?.('Translating to target locales...', 'translation', 'running');
243
260
  const translatorHealthy = await isTranslatorHealthy(config);
244
261
  if (!translatorHealthy) {
@@ -259,13 +276,19 @@ async function runEngineV2(config) {
259
276
  }
260
277
  else if (translatorHealthy) {
261
278
  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 });
279
+ if (overridden.size > 0) {
280
+ for (const [locale, keys] of Object.entries(keysToTranslateByLocale)) {
281
+ keysToTranslateByLocale[locale] = keys.filter((k) => !overridden.has(`${locale}\u0000${k}`));
282
+ }
283
+ }
284
+ 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, maxTranslations: config.maxTranslations });
263
285
  translation = result.translation;
264
286
  // A translation that wouldn't compile into the .po file is a failure, not output.
265
287
  gettext?.validate(translation, flatKeys);
266
288
  tmHits = result.stats.tmHits;
267
289
  translationMetadata = result.metadata;
268
290
  translatedThisRun = freshTranslations(translation, translationMetadata);
291
+ skippedForBudget = translation.skippedForBudget?.length ?? 0;
269
292
  // Strings reused from the lockfile still make a good run: only fail
270
293
  // when nothing at all is usable. Otherwise one string that keeps
271
294
  // failing (and is retried every run) failed the whole project.
@@ -326,6 +349,24 @@ async function runEngineV2(config) {
326
349
  locale.data._meta = { dir: (0, language_codes_1.isRTL)(locale.locale) ? 'rtl' : 'ltr', language: locale.locale };
327
350
  }
328
351
  }
352
+ // Dashboard edits win over the lockfile and the translator (while their
353
+ // source text is unchanged); the lockfile marks them as manual.
354
+ let overrideReport;
355
+ if (overrides.length > 0) {
356
+ overrideReport = (0, overrides_1.applyTranslationOverrides)(translation.locales, flatKeys, overrides);
357
+ for (const { key, locale } of overrideReport.applied) {
358
+ translationMetadata[locale] = { ...(translationMetadata[locale] ?? {}) };
359
+ // Keep what the lockfile knew (glossary fingerprint, inTm): losing it would re-translate the key every run.
360
+ // Not a fallback translation (that flag would drop it from the lockfile).
361
+ const kept = { ...(lockfile.keys[key]?.metadata?.[locale] ?? {}), ...(translationMetadata[locale][key] ?? {}) };
362
+ delete kept.fallback;
363
+ translationMetadata[locale][key] = { ...kept, manual: true };
364
+ }
365
+ logger_1.engineLogger.info('Applied translation overrides', {
366
+ applied: overrideReport.applied.length,
367
+ replaced: overrideReport.replaced.length,
368
+ });
369
+ }
329
370
  // The translator wrote only this run's strings to disk; write the merged
330
371
  // locale (lockfile + new) so the workspace — which the download ZIP is
331
372
  // built from — has every translation, including fully cached locales.
@@ -384,6 +425,9 @@ async function runEngineV2(config) {
384
425
  const previousLockfile = lockfile;
385
426
  lockfile = (0, lockfile_1.updateLockfile)(lockfile, flatKeys, allTranslatedPerLocale, config.projectId, lockfileMetadata);
386
427
  lockfile = (0, lockfile_1.dropFailedTranslations)(lockfile, translation.failures ?? []);
428
+ // Pairs the budget didn't cover are retried next run the same way
429
+ // (a changed key must not keep its old translation as current).
430
+ lockfile = (0, lockfile_1.dropFailedTranslations)(lockfile, translation.skippedForBudget ?? []);
387
431
  // Backup-translator (NLLB) output is shipped in the locale file but not
388
432
  // recorded as done, so the next run retries it with the AI translator.
389
433
  lockfile = (0, lockfile_1.dropFailedTranslations)(lockfile, fallbackTranslations(translationMetadata));
@@ -403,8 +447,8 @@ async function runEngineV2(config) {
403
447
  extraction, keyGeneration, localeHandling, translation,
404
448
  config, startTime, workspace, allTranslatedPerLocale,
405
449
  lockfileStats, tmHits, translationMetadata,
406
- translatedThisRun, keyUsage,
407
- formatStrings, frameworkConfig, gettext,
450
+ translatedThisRun, skippedForBudget, keyUsage,
451
+ formatStrings, frameworkConfig, gettext, overrideReport,
408
452
  });
409
453
  return result;
410
454
  }
@@ -429,7 +473,7 @@ async function runEngineV2(config) {
429
473
  config.onProgress?.(msg, 'translation', 'warning');
430
474
  }
431
475
  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 })
476
+ ? 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, maxTranslations: config.maxTranslations })
433
477
  : {
434
478
  translation: {
435
479
  locales: [],
@@ -443,6 +487,7 @@ async function runEngineV2(config) {
443
487
  tmHits = translationResult.stats.tmHits;
444
488
  translationMetadata = translationResult.metadata;
445
489
  translatedThisRun = freshTranslations(translation, translationMetadata);
490
+ skippedForBudget = translation.skippedForBudget?.length ?? 0;
446
491
  assertSomethingTranslated(translation);
447
492
  // Fail immediately when translation errors occurred — before injection/finalization
448
493
  if (translation.errors && translation.errors.length > 0) {
@@ -458,7 +503,7 @@ async function runEngineV2(config) {
458
503
  extraction, keyGeneration, localeHandling, translation,
459
504
  config, startTime, workspace, allTranslatedPerLocale,
460
505
  lockfileStats: undefined, tmHits, translationMetadata,
461
- translatedThisRun, keyUsage,
506
+ translatedThisRun, skippedForBudget, keyUsage,
462
507
  formatStrings, frameworkConfig, gettext,
463
508
  });
464
509
  return result;
@@ -621,7 +666,7 @@ function mergeChangedFile(list, changed) {
621
666
  existing.hookAdded = existing.hookAdded || changed.hookAdded;
622
667
  }
623
668
  async function finalizePipeline(params) {
624
- const { extraction, keyGeneration, localeHandling, translation, config, startTime, workspace, allTranslatedPerLocale, lockfileStats, tmHits, translationMetadata, formatStrings, frameworkConfig, translatedThisRun, keyUsage, gettext, } = params;
669
+ const { extraction, keyGeneration, localeHandling, translation, config, startTime, workspace, allTranslatedPerLocale, lockfileStats, tmHits, translationMetadata, formatStrings, frameworkConfig, translatedThisRun, skippedForBudget, keyUsage, gettext, overrideReport, } = params;
625
670
  const keyMapping = (0, keygen_1.createKeyMapping)(keyGeneration.keys);
626
671
  // Injection: AST-based i18n hook injection (skipped for frameworks like Django)
627
672
  let injection;
@@ -701,6 +746,8 @@ async function finalizePipeline(params) {
701
746
  config.onProgress?.('Generating summary...', 'summary', 'running');
702
747
  const { stats, warnings, diffs } = (0, summary_1.generateSummary)(extraction, keyGeneration, localeHandling, translation, injection, startTime, lockfileStats, tmHits);
703
748
  stats.translatedThisRun = Math.max(0, translatedThisRun);
749
+ if (skippedForBudget && skippedForBudget > 0)
750
+ stats.skippedForBudget = skippedForBudget;
704
751
  logger_1.engineLogger.info('Processing complete', stats);
705
752
  logger_1.engineLogger.debug('Readable summary', { summary: (0, summary_1.generateReadableSummary)(stats) });
706
753
  const allLocales = [localeHandling.sourceLocale, ...translation.locales];
@@ -723,5 +770,6 @@ async function finalizePipeline(params) {
723
770
  targets: allTranslatedPerLocale,
724
771
  metadata: translationMetadata,
725
772
  },
773
+ ...(overrideReport ? { overrides: overrideReport } : {}),
726
774
  };
727
775
  }
@@ -1,6 +1,7 @@
1
1
  import type { TranslationEntry } from '../runtime/types';
2
2
  import type { MemoryEntry } from '../modules/translator/memory/types';
3
3
  import type { GlossaryEntryType } from '../modules/glossary/types';
4
+ import type { TranslationOverride } from '../modules/overrides';
4
5
  /**
5
6
  * Everything the engine needs from the outside world, as interfaces. The
6
7
  * engine never imports a database or cache client: the cloud backend injects
@@ -129,6 +130,9 @@ export interface ProjectRulesStore {
129
130
  getGlossary(projectId: string): Promise<GlossaryRule[]>;
130
131
  getBrandVoice(projectId: string): Promise<string | null>;
131
132
  }
133
+ export interface TranslationOverrideStore {
134
+ getOverrides(projectId: string): Promise<TranslationOverride[]>;
135
+ }
132
136
  export interface TranslationCache {
133
137
  get(key: string): Promise<{
134
138
  hit: boolean;
@@ -150,4 +154,6 @@ export interface EngineProviders {
150
154
  memory?: TranslationMemoryStore;
151
155
  rules?: ProjectRulesStore;
152
156
  cache?: TranslationCache;
157
+ /** Translations edited in the dashboard; they win over every other source. */
158
+ overrides?: TranslationOverrideStore;
153
159
  }
@@ -24,6 +24,7 @@ function validateEngineConfigV2(config) {
24
24
  confidenceThreshold,
25
25
  dryRun: config.dryRun ?? false,
26
26
  forceTranslation: config.forceTranslation ?? false,
27
+ maxTranslations: config.maxTranslations,
27
28
  onProgress: config.onProgress,
28
29
  signal: config.signal,
29
30
  projectId: config.projectId,
@@ -25,6 +25,15 @@ export interface EngineConfigV2 {
25
25
  dryRun: boolean;
26
26
  /** Force translation even if locale files exist */
27
27
  forceTranslation?: boolean;
28
+ /**
29
+ * Most (key, locale) pairs this run may send to the translator: the
30
+ * caller's credit budget. Pairs past it are left untranslated (not in the
31
+ * locale files or lockfile, so the next run picks them up) and counted in
32
+ * `stats.skippedForBudget`. Lockfile, translation-memory, cache and
33
+ * passthrough reuse never count against it. Languages are filled in
34
+ * `targetLocales` order, keys in source order. Undefined or negative: no cap.
35
+ */
36
+ maxTranslations?: number;
28
37
  /** Project ID for scoping lockfile/memory */
29
38
  projectId?: string;
30
39
  /**
@@ -266,6 +275,11 @@ export interface TranslationResult {
266
275
  }>;
267
276
  /** Keys with no translation for a locale this run (not written, retried next run). */
268
277
  failures?: TranslationFailure[];
278
+ /** (key, locale) pairs not sent to the translator because the run's `maxTranslations` ran out (retried next run). */
279
+ skippedForBudget?: Array<{
280
+ key: string;
281
+ locale: string;
282
+ }>;
269
283
  }
270
284
  export interface InjectionLocation {
271
285
  file: string;
@@ -339,6 +353,11 @@ export interface ProcessingStats {
339
353
  * and failed keys.
340
354
  */
341
355
  translatedThisRun?: number;
356
+ /**
357
+ * (key, locale) translations left for the next run because the run's
358
+ * `maxTranslations` budget ran out. Set only when some were skipped.
359
+ */
360
+ skippedForBudget?: number;
342
361
  }
343
362
  export interface NotInjectedString {
344
363
  file: string;
@@ -398,6 +417,8 @@ export interface EngineResultV2 {
398
417
  targets: Record<string, Record<string, string>>;
399
418
  metadata?: Record<string, Record<string, any>>;
400
419
  };
420
+ /** Dashboard edits this run applied, and those whose source text changed (lockfile runs only). */
421
+ overrides?: import('../modules/overrides').OverrideReport;
401
422
  }
402
423
  export interface Phase0Result {
403
424
  workspace: WorkspaceInfo;
@@ -92,7 +92,7 @@ async function callTranslator(entries, sourceLang, targetLang, byokConfig, brand
92
92
  if (needsFallback) {
93
93
  logger_1.engineLogger.warn('Rust translator returned untargeted translations or unavailable, falling back to HF Inference API');
94
94
  try {
95
- const hfResults = await callHuggingFaceNLLB(entries, sourceLang, targetLang);
95
+ const hfResults = await callHuggingFaceNLLB(entries, sourceLang, targetLang, outerSignal);
96
96
  if (hfResults.translations.length > 0) {
97
97
  return { translations: hfResults.translations, metadata: { ...rustResult?.metadata, fallback: 'hf-inference-api' } };
98
98
  }
@@ -137,12 +137,15 @@ async function callByokTranslator(entries, sourceLang, targetLang, byokConfig, b
137
137
  }
138
138
  return { translations, metadata: result.metadata };
139
139
  }
140
- async function callHuggingFaceNLLB(entries, sourceLang, targetLang) {
140
+ async function callHuggingFaceNLLB(entries, sourceLang, targetLang,
141
+ /** The caller gave up (time budget, client gone): stop instead of finishing every entry. */
142
+ outerSignal) {
141
143
  const hfToken = process.env.HF_TOKEN || process.env.HF_API_TOKEN;
142
144
  const model = 'facebook/nllb-200-distilled-600M';
143
145
  const baseUrl = `https://api-inference.huggingface.co/models/${model}`;
144
146
  const translations = [];
145
147
  for (const entry of entries) {
148
+ outerSignal?.throwIfAborted();
146
149
  const response = await fetch(baseUrl, {
147
150
  method: 'POST',
148
151
  headers: {
@@ -153,7 +156,7 @@ async function callHuggingFaceNLLB(entries, sourceLang, targetLang) {
153
156
  inputs: entry.value,
154
157
  parameters: { src_lang: sourceLang, tgt_lang: targetLang },
155
158
  }),
156
- signal: AbortSignal.timeout(60000),
159
+ signal: outerSignal ? AbortSignal.any([AbortSignal.timeout(60000), outerSignal]) : AbortSignal.timeout(60000),
157
160
  });
158
161
  if (!response.ok) {
159
162
  const body = await response.text().catch(() => '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelize/engine",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Babelize translation pipeline (extract, keygen, translate, inject). Infrastructure is injected through provider/store interfaces.",
5
5
  "keywords": [
6
6
  "babelize",