@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.
- package/dist/analysis/analyze.js +1 -1
- package/dist/detection/i18n.d.ts +19 -0
- package/dist/detection/i18n.js +144 -0
- package/dist/detection/stack.d.ts +2 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +9 -2
- package/dist/injection/html/html-injector.d.ts +2 -0
- package/dist/injection/html/html-injector.js +55 -6
- package/dist/local/hosted-provider.d.ts +30 -2
- package/dist/local/hosted-provider.js +50 -3
- package/dist/local/translate-core.d.ts +2 -2
- package/dist/local/translate-core.js +6 -2
- package/dist/modules/finalization/finalize.js +6 -4
- package/dist/modules/finalization/static-runtime.d.ts +2 -2
- package/dist/modules/finalization/static-runtime.js +10 -5
- package/dist/modules/lockfile/index.d.ts +1 -1
- package/dist/modules/lockfile/index.js +2 -1
- package/dist/modules/lockfile/lockfile-manager.d.ts +9 -0
- package/dist/modules/lockfile/lockfile-manager.js +43 -1
- package/dist/modules/lockfile/schema.d.ts +14 -0
- package/dist/modules/lockfile/schema.js +2 -0
- package/dist/modules/overrides/index.d.ts +41 -0
- package/dist/modules/overrides/index.js +50 -0
- package/dist/modules/translator/budget.d.ts +25 -0
- package/dist/modules/translator/budget.js +46 -0
- package/dist/modules/translator/translate.d.ts +11 -0
- package/dist/modules/translator/translate.js +50 -10
- package/dist/orchestrator.js +54 -6
- package/dist/providers/types.d.ts +6 -0
- package/dist/runtime/config.js +1 -0
- package/dist/runtime/types.d.ts +21 -0
- package/dist/utils/translator-caller.js +6 -3
- package/package.json +1 -1
package/dist/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/runtime/config.js
CHANGED
|
@@ -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,
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -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