@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.
- package/LICENSE +202 -0
- package/dist/BabelizeEngine.d.ts +12 -0
- package/dist/BabelizeEngine.js +47 -0
- package/dist/analysis/analyze.d.ts +106 -0
- package/dist/analysis/analyze.js +323 -0
- package/dist/config/env.d.ts +9 -0
- package/dist/config/env.js +16 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.js +47 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +126 -0
- package/dist/injection/InjectionStrategy.d.ts +62 -0
- package/dist/injection/InjectionStrategy.js +2 -0
- package/dist/injection/StrategyFactory.d.ts +4 -0
- package/dist/injection/StrategyFactory.js +29 -0
- package/dist/injection/django/django-template-injector.d.ts +29 -0
- package/dist/injection/django/django-template-injector.js +216 -0
- package/dist/injection/html/html-injector.d.ts +11 -0
- package/dist/injection/html/html-injector.js +173 -0
- package/dist/injection/strategies/NextIntlStrategy.d.ts +25 -0
- package/dist/injection/strategies/NextIntlStrategy.js +141 -0
- package/dist/injection/strategies/ReactI18nextStrategy.d.ts +19 -0
- package/dist/injection/strategies/ReactI18nextStrategy.js +116 -0
- package/dist/injection/strategies/VanillaStrategy.d.ts +11 -0
- package/dist/injection/strategies/VanillaStrategy.js +73 -0
- package/dist/injection/strategies/VueI18nStrategy.d.ts +11 -0
- package/dist/injection/strategies/VueI18nStrategy.js +84 -0
- package/dist/local/formatter.d.ts +26 -0
- package/dist/local/formatter.js +105 -0
- package/dist/local/git.d.ts +23 -0
- package/dist/local/git.js +136 -0
- package/dist/local/hosted-provider.d.ts +68 -0
- package/dist/local/hosted-provider.js +215 -0
- package/dist/local/index.d.ts +23 -0
- package/dist/local/index.js +75 -0
- package/dist/local/provider.d.ts +56 -0
- package/dist/local/provider.js +119 -0
- package/dist/local/pseudo-provider.d.ts +18 -0
- package/dist/local/pseudo-provider.js +207 -0
- package/dist/local/report.d.ts +177 -0
- package/dist/local/report.js +186 -0
- package/dist/local/translate-core.d.ts +215 -0
- package/dist/local/translate-core.js +461 -0
- package/dist/local/translate-messages.d.ts +49 -0
- package/dist/local/translate-messages.js +91 -0
- package/dist/local/workspace-copy.d.ts +91 -0
- package/dist/local/workspace-copy.js +355 -0
- package/dist/modules/extractor/extract.d.ts +20 -0
- package/dist/modules/extractor/extract.js +701 -0
- package/dist/modules/extractor/index.d.ts +2 -0
- package/dist/modules/extractor/index.js +63 -0
- package/dist/modules/finalization/django-finalize.d.ts +13 -0
- package/dist/modules/finalization/django-finalize.js +275 -0
- package/dist/modules/finalization/finalize.d.ts +36 -0
- package/dist/modules/finalization/finalize.js +717 -0
- package/dist/modules/finalization/index.d.ts +1 -0
- package/dist/modules/finalization/index.js +5 -0
- package/dist/modules/finalization/nextjs-finalize.d.ts +37 -0
- package/dist/modules/finalization/nextjs-finalize.js +329 -0
- package/dist/modules/finalization/static-runtime.d.ts +9 -0
- package/dist/modules/finalization/static-runtime.js +261 -0
- package/dist/modules/format-injection/index.d.ts +19 -0
- package/dist/modules/format-injection/index.js +272 -0
- package/dist/modules/format-injection/inject-formats.d.ts +31 -0
- package/dist/modules/format-injection/inject-formats.js +208 -0
- package/dist/modules/glossary/glossary.d.ts +9 -0
- package/dist/modules/glossary/glossary.js +27 -0
- package/dist/modules/glossary/index.d.ts +3 -0
- package/dist/modules/glossary/index.js +8 -0
- package/dist/modules/glossary/masker.d.ts +23 -0
- package/dist/modules/glossary/masker.js +56 -0
- package/dist/modules/glossary/types.d.ts +22 -0
- package/dist/modules/glossary/types.js +2 -0
- package/dist/modules/injection/index.d.ts +1 -0
- package/dist/modules/injection/index.js +6 -0
- package/dist/modules/injection/inject.d.ts +29 -0
- package/dist/modules/injection/inject.js +1066 -0
- package/dist/modules/keygen/index.d.ts +1 -0
- package/dist/modules/keygen/index.js +11 -0
- package/dist/modules/keygen/keygen.d.ts +4 -0
- package/dist/modules/keygen/keygen.js +116 -0
- package/dist/modules/keygen/locale-data.d.ts +7 -0
- package/dist/modules/keygen/locale-data.js +73 -0
- package/dist/modules/keygen/public.d.ts +2 -0
- package/dist/modules/keygen/public.js +12 -0
- package/dist/modules/locale-builder/index.d.ts +1 -0
- package/dist/modules/locale-builder/index.js +7 -0
- package/dist/modules/locale-builder/locales.d.ts +13 -0
- package/dist/modules/locale-builder/locales.js +107 -0
- package/dist/modules/locale-builder/po.d.ts +33 -0
- package/dist/modules/locale-builder/po.js +131 -0
- package/dist/modules/lockfile/hasher.d.ts +7 -0
- package/dist/modules/lockfile/hasher.js +13 -0
- package/dist/modules/lockfile/index.d.ts +3 -0
- package/dist/modules/lockfile/index.js +21 -0
- package/dist/modules/lockfile/lockfile-manager.d.ts +90 -0
- package/dist/modules/lockfile/lockfile-manager.js +482 -0
- package/dist/modules/lockfile/schema.d.ts +154 -0
- package/dist/modules/lockfile/schema.js +45 -0
- package/dist/modules/parser/index.d.ts +2 -0
- package/dist/modules/parser/index.js +9 -0
- package/dist/modules/summary/index.d.ts +1 -0
- package/dist/modules/summary/index.js +7 -0
- package/dist/modules/summary/summary.d.ts +9 -0
- package/dist/modules/summary/summary.js +195 -0
- package/dist/modules/sync-detector/index.d.ts +2 -0
- package/dist/modules/sync-detector/index.js +7 -0
- package/dist/modules/sync-detector/reporter.d.ts +3 -0
- package/dist/modules/sync-detector/reporter.js +16 -0
- package/dist/modules/sync-detector/sync-detector.d.ts +39 -0
- package/dist/modules/sync-detector/sync-detector.js +374 -0
- package/dist/modules/translator/batch-optimizer.d.ts +12 -0
- package/dist/modules/translator/batch-optimizer.js +30 -0
- package/dist/modules/translator/client/byok.provider.d.ts +64 -0
- package/dist/modules/translator/client/byok.provider.js +319 -0
- package/dist/modules/translator/client/direct-translator.client.d.ts +12 -0
- package/dist/modules/translator/client/direct-translator.client.js +71 -0
- package/dist/modules/translator/client/gemini.provider.d.ts +1 -0
- package/dist/modules/translator/client/gemini.provider.js +110 -0
- package/dist/modules/translator/client/groq.provider.d.ts +1 -0
- package/dist/modules/translator/client/groq.provider.js +111 -0
- package/dist/modules/translator/client/request.types.d.ts +1 -0
- package/dist/modules/translator/client/request.types.js +2 -0
- package/dist/modules/translator/client/retry.d.ts +29 -0
- package/dist/modules/translator/client/retry.js +94 -0
- package/dist/modules/translator/client/test-utils/mock.client.d.ts +31 -0
- package/dist/modules/translator/client/test-utils/mock.client.js +59 -0
- package/dist/modules/translator/client/translator.interface.d.ts +1 -0
- package/dist/modules/translator/client/translator.interface.js +2 -0
- package/dist/modules/translator/language-codes/errors.d.ts +10 -0
- package/dist/modules/translator/language-codes/errors.js +18 -0
- package/dist/modules/translator/language-codes/flores-map.d.ts +1 -0
- package/dist/modules/translator/language-codes/flores-map.js +107 -0
- package/dist/modules/translator/language-codes/index.d.ts +11 -0
- package/dist/modules/translator/language-codes/index.js +24 -0
- package/dist/modules/translator/language-codes/language-mapper.d.ts +25 -0
- package/dist/modules/translator/language-codes/language-mapper.js +132 -0
- package/dist/modules/translator/language-codes/rtl-languages.d.ts +2 -0
- package/dist/modules/translator/language-codes/rtl-languages.js +9 -0
- package/dist/modules/translator/memory/memory-matcher.d.ts +8 -0
- package/dist/modules/translator/memory/memory-matcher.js +218 -0
- package/dist/modules/translator/memory/types.d.ts +22 -0
- package/dist/modules/translator/memory/types.js +2 -0
- package/dist/modules/translator/translate.d.ts +38 -0
- package/dist/modules/translator/translate.js +932 -0
- package/dist/modules/translator/translator.d.ts +5 -0
- package/dist/modules/translator/translator.js +46 -0
- package/dist/modules/vue/migrate.d.ts +13 -0
- package/dist/modules/vue/migrate.js +40 -0
- package/dist/modules/vue/script-inject.d.ts +16 -0
- package/dist/modules/vue/script-inject.js +115 -0
- package/dist/modules/vue/sfc.d.ts +11 -0
- package/dist/modules/vue/sfc.js +51 -0
- package/dist/modules/vue/template-rewrite.d.ts +25 -0
- package/dist/modules/vue/template-rewrite.js +76 -0
- package/dist/modules/vue/template-scan.d.ts +29 -0
- package/dist/modules/vue/template-scan.js +264 -0
- package/dist/modules/workspace/index.d.ts +1 -0
- package/dist/modules/workspace/index.js +5 -0
- package/dist/modules/workspace/validate.d.ts +13 -0
- package/dist/modules/workspace/validate.js +298 -0
- package/dist/orchestrator.d.ts +8 -0
- package/dist/orchestrator.js +616 -0
- package/dist/providers/default-translation-provider.d.ts +20 -0
- package/dist/providers/default-translation-provider.js +30 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +21 -0
- package/dist/providers/types.d.ts +145 -0
- package/dist/providers/types.js +2 -0
- package/dist/qa/check-project.d.ts +49 -0
- package/dist/qa/check-project.js +150 -0
- package/dist/qa/locale-files.d.ts +44 -0
- package/dist/qa/locale-files.js +159 -0
- package/dist/qa/locale-qa.d.ts +61 -0
- package/dist/qa/locale-qa.js +325 -0
- package/dist/qa/review.d.ts +43 -0
- package/dist/qa/review.js +175 -0
- package/dist/runtime/config.d.ts +8 -0
- package/dist/runtime/config.js +36 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/types.d.ts +363 -0
- package/dist/runtime/types.js +38 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +12 -0
- package/dist/utils/ast-helpers.d.ts +57 -0
- package/dist/utils/ast-helpers.js +608 -0
- package/dist/utils/cache-keys.d.ts +8 -0
- package/dist/utils/cache-keys.js +18 -0
- package/dist/utils/circuit-breaker.d.ts +43 -0
- package/dist/utils/circuit-breaker.js +119 -0
- package/dist/utils/concurrent-map.d.ts +4 -0
- package/dist/utils/concurrent-map.js +21 -0
- package/dist/utils/detect-format.d.ts +5 -0
- package/dist/utils/detect-format.js +125 -0
- package/dist/utils/dtl-mask.d.ts +25 -0
- package/dist/utils/dtl-mask.js +58 -0
- package/dist/utils/extraction-options.d.ts +23 -0
- package/dist/utils/extraction-options.js +23 -0
- package/dist/utils/file-utils.d.ts +33 -0
- package/dist/utils/file-utils.js +262 -0
- package/dist/utils/glossary-match.d.ts +9 -0
- package/dist/utils/glossary-match.js +25 -0
- package/dist/utils/html-extractor.d.ts +8 -0
- package/dist/utils/html-extractor.js +317 -0
- package/dist/utils/i18next-icu.d.ts +38 -0
- package/dist/utils/i18next-icu.js +159 -0
- package/dist/utils/icu-message.d.ts +65 -0
- package/dist/utils/icu-message.js +346 -0
- package/dist/utils/icu-spans.d.ts +10 -0
- package/dist/utils/icu-spans.js +126 -0
- package/dist/utils/jsx-message.d.ts +93 -0
- package/dist/utils/jsx-message.js +621 -0
- package/dist/utils/key-usage.d.ts +35 -0
- package/dist/utils/key-usage.js +247 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/metadata.d.ts +80 -0
- package/dist/utils/metadata.js +255 -0
- package/dist/utils/module-constants.d.ts +30 -0
- package/dist/utils/module-constants.js +265 -0
- package/dist/utils/not-injected.d.ts +6 -0
- package/dist/utils/not-injected.js +36 -0
- package/dist/utils/placeholder-protection.d.ts +57 -0
- package/dist/utils/placeholder-protection.js +136 -0
- package/dist/utils/pseudo-markers.d.ts +9 -0
- package/dist/utils/pseudo-markers.js +16 -0
- package/dist/utils/py-extractor.d.ts +14 -0
- package/dist/utils/py-extractor.js +285 -0
- package/dist/utils/string-utils.d.ts +52 -0
- package/dist/utils/string-utils.js +270 -0
- package/dist/utils/translation-quality.d.ts +34 -0
- package/dist/utils/translation-quality.js +156 -0
- package/dist/utils/translation-sanitizer.d.ts +10 -0
- package/dist/utils/translation-sanitizer.js +57 -0
- package/dist/utils/translator-caller.d.ts +32 -0
- package/dist/utils/translator-caller.js +217 -0
- package/dist/utils/translator-load-balancer.d.ts +73 -0
- package/dist/utils/translator-load-balancer.js +293 -0
- package/dist/utils/ui-copy.d.ts +32 -0
- package/dist/utils/ui-copy.js +126 -0
- package/dist/utils/workspace.d.ts +14 -0
- package/dist/utils/workspace.js +89 -0
- package/package.json +64 -4
- package/schemas/translate-report.schema.json +297 -0
- package/README.md +0 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Translation Quality Validation
|
|
4
|
+
*
|
|
5
|
+
* Validates translation quality and detects potential issues
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.validateTranslationQuality = validateTranslationQuality;
|
|
9
|
+
exports.validateBatchQuality = validateBatchQuality;
|
|
10
|
+
/**
|
|
11
|
+
* Validate translation quality
|
|
12
|
+
*/
|
|
13
|
+
function validateTranslationQuality(original, translated) {
|
|
14
|
+
const issues = [];
|
|
15
|
+
let score = 100;
|
|
16
|
+
const trimmedOriginal = original.trim();
|
|
17
|
+
const trimmedTranslated = (translated || '').trim();
|
|
18
|
+
// Check for empty translation
|
|
19
|
+
if (trimmedTranslated.length === 0) {
|
|
20
|
+
issues.push({
|
|
21
|
+
type: 'empty_translation',
|
|
22
|
+
severity: 'error',
|
|
23
|
+
message: 'Translation is empty',
|
|
24
|
+
original,
|
|
25
|
+
translated,
|
|
26
|
+
});
|
|
27
|
+
score -= 100; // Complete failure
|
|
28
|
+
}
|
|
29
|
+
// Check for untranslated text (identical to source)
|
|
30
|
+
// Skip for very short strings (e.g. "OK", "Save") where source might equal translation
|
|
31
|
+
if (trimmedTranslated.length > 4 && trimmedOriginal.toLowerCase() === trimmedTranslated.toLowerCase()) {
|
|
32
|
+
issues.push({
|
|
33
|
+
type: 'untranslated_text',
|
|
34
|
+
severity: 'warning',
|
|
35
|
+
message: 'Translation is identical to source text',
|
|
36
|
+
original,
|
|
37
|
+
translated,
|
|
38
|
+
});
|
|
39
|
+
score -= 20;
|
|
40
|
+
}
|
|
41
|
+
// Check placeholder preservation
|
|
42
|
+
const originalPlaceholders = original.match(/\{[^}]+\}/g) || [];
|
|
43
|
+
const translatedPlaceholders = translated.match(/\{[^}]+\}/g) || [];
|
|
44
|
+
// Missing placeholders
|
|
45
|
+
const sortedOriginal = [...originalPlaceholders].sort();
|
|
46
|
+
const sortedTranslated = [...translatedPlaceholders].sort();
|
|
47
|
+
const missingPlaceholders = sortedOriginal.filter((p) => !sortedTranslated.includes(p));
|
|
48
|
+
if (missingPlaceholders.length > 0) {
|
|
49
|
+
issues.push({
|
|
50
|
+
type: 'missing_placeholder',
|
|
51
|
+
severity: 'error',
|
|
52
|
+
message: `Missing placeholders: ${missingPlaceholders.join(', ')}`,
|
|
53
|
+
original,
|
|
54
|
+
translated,
|
|
55
|
+
});
|
|
56
|
+
score -= 30 * missingPlaceholders.length;
|
|
57
|
+
}
|
|
58
|
+
// Extra placeholders
|
|
59
|
+
const extraPlaceholders = sortedTranslated.filter((p) => !sortedOriginal.includes(p));
|
|
60
|
+
if (extraPlaceholders.length > 0) {
|
|
61
|
+
issues.push({
|
|
62
|
+
type: 'extra_placeholder',
|
|
63
|
+
severity: 'warning',
|
|
64
|
+
message: `Extra placeholders found: ${extraPlaceholders.join(', ')}`,
|
|
65
|
+
original,
|
|
66
|
+
translated,
|
|
67
|
+
});
|
|
68
|
+
score -= 10 * extraPlaceholders.length;
|
|
69
|
+
}
|
|
70
|
+
// Check placeholder ordering
|
|
71
|
+
// If the order of placeholders is fundamentally different, it might indicate grammar corruption
|
|
72
|
+
if (originalPlaceholders.length > 1 && originalPlaceholders.length === translatedPlaceholders.length) {
|
|
73
|
+
if (originalPlaceholders.join(',') !== translatedPlaceholders.join(',')) {
|
|
74
|
+
// Check if they are just reordered (which might be valid for some languages)
|
|
75
|
+
// vs. duplicated or mangled. If sorted they match, it's just a reorder.
|
|
76
|
+
if (sortedOriginal.join(',') === sortedTranslated.join(',')) {
|
|
77
|
+
// Reordering is often valid (Subject-Object-Verb vs Subject-Verb-Object)
|
|
78
|
+
// but drastic jumps might be worth a warning in some engines.
|
|
79
|
+
// For now, we allow reordering but log it if we wanted to.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Check HTML tag parity
|
|
84
|
+
const originalTags = original.match(/<[^>]+>/g) || [];
|
|
85
|
+
const translatedTags = translated.match(/<[^>]+>/g) || [];
|
|
86
|
+
if (originalTags.length !== translatedTags.length) {
|
|
87
|
+
issues.push({
|
|
88
|
+
type: 'html_tag_mismatch',
|
|
89
|
+
severity: 'error',
|
|
90
|
+
message: `HTML tag count mismatch: source has ${originalTags.length}, translation has ${translatedTags.length}`,
|
|
91
|
+
original,
|
|
92
|
+
translated,
|
|
93
|
+
});
|
|
94
|
+
score -= 25;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Even if count matches, check if tags themselves were mangled (e.g. <strong> becomes <fort>)
|
|
98
|
+
const sortedOrigTags = [...originalTags].map(t => t.toLowerCase().split(' ')[0]).sort();
|
|
99
|
+
const sortedTransTags = [...translatedTags].map(t => t.toLowerCase().split(' ')[0]).sort();
|
|
100
|
+
if (sortedOrigTags.join(',') !== sortedTransTags.join(',')) {
|
|
101
|
+
issues.push({
|
|
102
|
+
type: 'html_tag_mismatch',
|
|
103
|
+
severity: 'error',
|
|
104
|
+
message: 'HTML tags were altered or translated (e.g. <strong> became something else)',
|
|
105
|
+
original,
|
|
106
|
+
translated,
|
|
107
|
+
});
|
|
108
|
+
score -= 25;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Check for suspicious length differences
|
|
112
|
+
const lengthRatio = trimmedTranslated.length / (trimmedOriginal.length || 1);
|
|
113
|
+
if (lengthRatio < 0.2 || lengthRatio > 5) {
|
|
114
|
+
issues.push({
|
|
115
|
+
type: 'suspicious_length',
|
|
116
|
+
severity: 'warning',
|
|
117
|
+
message: `Translation length is ${(lengthRatio * 100).toFixed(0)}% of original (suspicious)`,
|
|
118
|
+
original,
|
|
119
|
+
translated,
|
|
120
|
+
});
|
|
121
|
+
score -= 15;
|
|
122
|
+
}
|
|
123
|
+
// Ensure score doesn't go below 0
|
|
124
|
+
score = Math.max(0, score);
|
|
125
|
+
return {
|
|
126
|
+
isValid: issues.filter((i) => i.severity === 'error').length === 0,
|
|
127
|
+
score,
|
|
128
|
+
issues,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validate batch of translations
|
|
133
|
+
*/
|
|
134
|
+
function validateBatchQuality(entries) {
|
|
135
|
+
let valid = 0;
|
|
136
|
+
let invalid = 0;
|
|
137
|
+
let totalScore = 0;
|
|
138
|
+
const allIssues = [];
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const result = validateTranslationQuality(entry.original, entry.translated);
|
|
141
|
+
totalScore += result.score;
|
|
142
|
+
if (result.isValid) {
|
|
143
|
+
valid++;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
invalid++;
|
|
147
|
+
}
|
|
148
|
+
allIssues.push(...result.issues);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
valid,
|
|
152
|
+
invalid,
|
|
153
|
+
averageScore: entries.length > 0 ? totalScore / entries.length : 0,
|
|
154
|
+
issues: allIssues,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean up translator output. Script injection is stripped for every
|
|
3
|
+
* provider; the NLLB-specific repairs (language-code echoes, unknown-token
|
|
4
|
+
* marks, whitespace and stutter collapsing) only run for NLLB output, since on
|
|
5
|
+
* LLM output they would rewrite legitimate text.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeTranslation(text: string, options?: {
|
|
8
|
+
nllb?: boolean;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function validateRawTokens(aiOutput: string, expectedCount: number): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeTranslation = sanitizeTranslation;
|
|
4
|
+
exports.validateRawTokens = validateRawTokens;
|
|
5
|
+
const NLLB_ARTIFACT_PATTERN = new RegExp('\\b(?:__)?[a-z]{3}_[A-Z][a-z]{3,4}(?:__)?\\b', 'g');
|
|
6
|
+
/**
|
|
7
|
+
* Collapse phrase-level stutter ("Il a Il a" -> "Il a"). NLLB occasionally
|
|
8
|
+
* repeats a short fragment twice after language tokens are stripped. Only
|
|
9
|
+
* collapse when BOTH halves contain >=2 words and match case-insensitively —
|
|
10
|
+
* this never touches legitimate one-word doublings like "no no" or "la la".
|
|
11
|
+
*/
|
|
12
|
+
function collapsePhraseStutter(text) {
|
|
13
|
+
const words = text.split(/\s+/);
|
|
14
|
+
if (words.length >= 4 && words.length % 2 === 0) {
|
|
15
|
+
const half = words.length / 2;
|
|
16
|
+
const first = words.slice(0, half);
|
|
17
|
+
const second = words.slice(half);
|
|
18
|
+
if (first.length >= 2 &&
|
|
19
|
+
first.every((w, i) => w.toLowerCase() === second[i].toLowerCase())) {
|
|
20
|
+
return first.join(' ');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Clean up translator output. Script injection is stripped for every
|
|
27
|
+
* provider; the NLLB-specific repairs (language-code echoes, unknown-token
|
|
28
|
+
* marks, whitespace and stutter collapsing) only run for NLLB output, since on
|
|
29
|
+
* LLM output they would rewrite legitimate text.
|
|
30
|
+
*/
|
|
31
|
+
function sanitizeTranslation(text, options = {}) {
|
|
32
|
+
if (!text)
|
|
33
|
+
return text;
|
|
34
|
+
let clean = text.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gim, '');
|
|
35
|
+
clean = clean.replace(/javascript:/gim, 'java_script:');
|
|
36
|
+
clean = clean.replace(/\s+on[a-z]+\s*=\s*['"][^'"]*['"]/gim, '');
|
|
37
|
+
if (options.nllb === false)
|
|
38
|
+
return clean;
|
|
39
|
+
// Strip FLORES-200 language-code artifacts (eng_Latn, __fra_Latn__, ...)
|
|
40
|
+
// anywhere in the output — NLLB echoes these at the end/middle of short
|
|
41
|
+
// fragments. Mirrors the fix in apps/translator/src/translator.rs.
|
|
42
|
+
clean = clean.replace(NLLB_ARTIFACT_PATTERN, '');
|
|
43
|
+
// Strip U+2047 (DOUBLE QUESTION MARK) — NLLB's unknown-token marker, emitted
|
|
44
|
+
// for tokens it can't translate (e.g. punctuation-only fragments). Never
|
|
45
|
+
// legitimate translated text.
|
|
46
|
+
clean = clean.replace(/\u2047/g, '');
|
|
47
|
+
clean = clean.replace(/ +/g, ' ').trim();
|
|
48
|
+
return collapsePhraseStutter(clean);
|
|
49
|
+
}
|
|
50
|
+
function validateRawTokens(aiOutput, expectedCount) {
|
|
51
|
+
for (let i = 0; i < expectedCount; i++) {
|
|
52
|
+
const tokenRegex = new RegExp(`\\[\\s*#\\s*VAR_${i}\\s*#\\s*\\]`, 'gi');
|
|
53
|
+
if (!tokenRegex.test(aiOutput))
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TranslationEntry } from '../runtime/types';
|
|
2
|
+
import type { ByokProvider } from '@babelize/types';
|
|
3
|
+
/** Options only LLM (BYOK) providers use; the NLLB service ignores them. */
|
|
4
|
+
export interface LlmRequestOptions {
|
|
5
|
+
targetLocale?: string;
|
|
6
|
+
glossary?: Array<{
|
|
7
|
+
term: string;
|
|
8
|
+
translation: string;
|
|
9
|
+
}>;
|
|
10
|
+
hints?: Record<string, string>;
|
|
11
|
+
/** Aborts the request and any retries. */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Translate a batch. Returns translations only for entries the translator
|
|
16
|
+
* actually produced — never the source text in their place. Entries missing
|
|
17
|
+
* from the result are reported as failed keys by the caller.
|
|
18
|
+
*/
|
|
19
|
+
export declare function callTranslator(entries: TranslationEntry[], sourceLang: string, targetLang: string, byokConfig?: {
|
|
20
|
+
provider: ByokProvider;
|
|
21
|
+
apiKey: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
}, brandVoice?: string, llmOptions?: LlmRequestOptions): Promise<{
|
|
24
|
+
translations: TranslationEntry[];
|
|
25
|
+
metadata?: any;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function checkTranslatorHealth(byokConfig?: {
|
|
28
|
+
provider: ByokProvider;
|
|
29
|
+
apiKey: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
}): Promise<boolean>;
|
|
32
|
+
export declare function warmupTranslator(): Promise<boolean>;
|
|
@@ -0,0 +1,217 @@
|
|
|
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.callTranslator = callTranslator;
|
|
37
|
+
exports.checkTranslatorHealth = checkTranslatorHealth;
|
|
38
|
+
exports.warmupTranslator = warmupTranslator;
|
|
39
|
+
const logger_1 = require("./logger");
|
|
40
|
+
const translation_quality_1 = require("./translation-quality");
|
|
41
|
+
const byok_provider_1 = require("../modules/translator/client/byok.provider");
|
|
42
|
+
/**
|
|
43
|
+
* Translate a batch. Returns translations only for entries the translator
|
|
44
|
+
* actually produced — never the source text in their place. Entries missing
|
|
45
|
+
* from the result are reported as failed keys by the caller.
|
|
46
|
+
*/
|
|
47
|
+
async function callTranslator(entries, sourceLang, targetLang, byokConfig, brandVoice, llmOptions = {}) {
|
|
48
|
+
if (byokConfig) {
|
|
49
|
+
return callByokTranslator(entries, sourceLang, targetLang, byokConfig, brandVoice, llmOptions);
|
|
50
|
+
}
|
|
51
|
+
const outerSignal = llmOptions.signal;
|
|
52
|
+
const { translatorLoadBalancer } = await Promise.resolve().then(() => __importStar(require('./translator-load-balancer')));
|
|
53
|
+
let rustResult = null;
|
|
54
|
+
try {
|
|
55
|
+
rustResult = await translatorLoadBalancer.executeWithFailover(async (translatorUrl) => {
|
|
56
|
+
const response = await fetch(`${translatorUrl}/translate`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify({ sourceLang, targetLang, entries }),
|
|
60
|
+
signal: outerSignal ? AbortSignal.any([AbortSignal.timeout(300000), outerSignal]) : AbortSignal.timeout(300000),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Translation API error: ${response.status} ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
if (data.metadata) {
|
|
67
|
+
logger_1.engineLogger.info('Translation metadata', {
|
|
68
|
+
dictVersion: data.metadata.dictVersion,
|
|
69
|
+
processingTimeMs: data.metadata.processingTimeMs,
|
|
70
|
+
dictionaryHits: data.metadata.dictionaryHits,
|
|
71
|
+
modelTranslations: data.metadata.modelTranslations,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { translations: data.translations || [], metadata: data.metadata };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
logger_1.engineLogger.warn('Rust translator unavailable, will try HF Inference API fallback', {
|
|
79
|
+
error: err instanceof Error ? err.message : String(err),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const needsFallback = rustResult === null || (rustResult.translations.length > 0 &&
|
|
83
|
+
rustResult.translations.every((t) => {
|
|
84
|
+
const orig = entries.find((e) => e.key === t.key)?.value || '';
|
|
85
|
+
if (orig.length <= 4)
|
|
86
|
+
return false;
|
|
87
|
+
const val = t.value || '';
|
|
88
|
+
if (orig.toLowerCase() === val.toLowerCase())
|
|
89
|
+
return true;
|
|
90
|
+
return /^\s*(?:[a-z]{3}_[A-Za-z]{4})\s+/.test(val);
|
|
91
|
+
}));
|
|
92
|
+
if (needsFallback) {
|
|
93
|
+
logger_1.engineLogger.warn('Rust translator returned untargeted translations or unavailable, falling back to HF Inference API');
|
|
94
|
+
try {
|
|
95
|
+
const hfResults = await callHuggingFaceNLLB(entries, sourceLang, targetLang);
|
|
96
|
+
if (hfResults.translations.length > 0) {
|
|
97
|
+
return { translations: hfResults.translations, metadata: { ...rustResult?.metadata, fallback: 'hf-inference-api' } };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (hfErr) {
|
|
101
|
+
logger_1.engineLogger.error('HF Inference API fallback also failed', { error: hfErr });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (rustResult !== null && rustResult.translations.length > 0) {
|
|
105
|
+
return rustResult;
|
|
106
|
+
}
|
|
107
|
+
// Never pass the source text off as a translation: an empty result makes
|
|
108
|
+
// every key a reported failure, and the next run retries them.
|
|
109
|
+
logger_1.engineLogger.error(`Translator unavailable for ${sourceLang} → ${targetLang}; no translations produced`);
|
|
110
|
+
return { translations: [], metadata: { translatorUnavailable: true } };
|
|
111
|
+
}
|
|
112
|
+
async function callByokTranslator(entries, sourceLang, targetLang, byokConfig, brandVoice, llmOptions = {}) {
|
|
113
|
+
const client = new byok_provider_1.BYOKTranslatorClient(byokConfig);
|
|
114
|
+
const { signal, ...promptOptions } = llmOptions;
|
|
115
|
+
const hfRequest = {
|
|
116
|
+
entries: entries.map((e) => ({ key: e.key, value: e.value, context: e.parentContext, extractionContext: e.extractionContext })),
|
|
117
|
+
sourceLang,
|
|
118
|
+
targetLang,
|
|
119
|
+
brandVoice,
|
|
120
|
+
...promptOptions,
|
|
121
|
+
};
|
|
122
|
+
const result = await client.translate(hfRequest, signal);
|
|
123
|
+
const byKey = new Map(result.translations.map((t) => [t.key, t]));
|
|
124
|
+
const translations = [];
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const t = byKey.get(entry.key);
|
|
127
|
+
if (!t)
|
|
128
|
+
continue;
|
|
129
|
+
const quality = (0, translation_quality_1.validateTranslationQuality)(entry.value, t.value);
|
|
130
|
+
translations.push({
|
|
131
|
+
key: entry.key,
|
|
132
|
+
value: t.value,
|
|
133
|
+
confidence: quality.isValid ? Math.max(0.5, quality.score / 100) : 0.3,
|
|
134
|
+
engine: byokConfig.provider,
|
|
135
|
+
warnings: quality.issues?.map((i) => `${i.type}:${i.severity}`) || [],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return { translations, metadata: result.metadata };
|
|
139
|
+
}
|
|
140
|
+
async function callHuggingFaceNLLB(entries, sourceLang, targetLang) {
|
|
141
|
+
const hfToken = process.env.HF_TOKEN || process.env.HF_API_TOKEN;
|
|
142
|
+
const model = 'facebook/nllb-200-distilled-600M';
|
|
143
|
+
const baseUrl = `https://api-inference.huggingface.co/models/${model}`;
|
|
144
|
+
const translations = [];
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const response = await fetch(baseUrl, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': 'application/json',
|
|
150
|
+
...(hfToken ? { Authorization: `Bearer ${hfToken}` } : {}),
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
inputs: entry.value,
|
|
154
|
+
parameters: { src_lang: sourceLang, tgt_lang: targetLang },
|
|
155
|
+
}),
|
|
156
|
+
signal: AbortSignal.timeout(60000),
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const body = await response.text().catch(() => '');
|
|
160
|
+
throw new Error(`HF Inference API error: ${response.status} — ${body.slice(0, 200)}`);
|
|
161
|
+
}
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
const translatedText = data?.[0]?.translation_text;
|
|
164
|
+
if (!translatedText)
|
|
165
|
+
continue;
|
|
166
|
+
translations.push({
|
|
167
|
+
key: entry.key,
|
|
168
|
+
value: translatedText,
|
|
169
|
+
confidence: 0.9,
|
|
170
|
+
engine: 'hf-nllb-200',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return { translations };
|
|
174
|
+
}
|
|
175
|
+
async function checkTranslatorHealth(byokConfig) {
|
|
176
|
+
try {
|
|
177
|
+
if (byokConfig) {
|
|
178
|
+
const client = new byok_provider_1.BYOKTranslatorClient(byokConfig);
|
|
179
|
+
return await client.health();
|
|
180
|
+
}
|
|
181
|
+
const { DirectTranslatorClient } = await Promise.resolve().then(() => __importStar(require('../modules/translator/client/direct-translator.client')));
|
|
182
|
+
const client = new DirectTranslatorClient();
|
|
183
|
+
return await client.health();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function warmupTranslator() {
|
|
190
|
+
logger_1.engineLogger.info('[Translator] Starting warmup...');
|
|
191
|
+
const MAX_ATTEMPTS = 3;
|
|
192
|
+
const RETRY_DELAY_MS = 15000;
|
|
193
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
194
|
+
try {
|
|
195
|
+
logger_1.engineLogger.info(`[Translator] Warmup attempt ${attempt}/${MAX_ATTEMPTS}...`);
|
|
196
|
+
const { DirectTranslatorClient } = await Promise.resolve().then(() => __importStar(require('../modules/translator/client/direct-translator.client')));
|
|
197
|
+
const client = new DirectTranslatorClient();
|
|
198
|
+
const ok = await client.warmup();
|
|
199
|
+
if (ok) {
|
|
200
|
+
logger_1.engineLogger.info('[Translator] Warmup successful');
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
logger_1.engineLogger.warn(`[Translator] Warmup attempt ${attempt} failed`);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
logger_1.engineLogger.warn(`[Translator] Warmup attempt ${attempt} failed:`, {
|
|
207
|
+
error: err instanceof Error ? err.message : String(err),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
211
|
+
logger_1.engineLogger.info(`[Translator] Retrying warmup in ${RETRY_DELAY_MS / 1000}s...`);
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
logger_1.engineLogger.warn('[Translator] All warmup attempts failed — running cold');
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translator Load Balancer
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple translator endpoints with:
|
|
5
|
+
* - Health checking
|
|
6
|
+
* - Round-robin load balancing
|
|
7
|
+
* - Automatic failover on errors
|
|
8
|
+
* - Circuit breaker per endpoint
|
|
9
|
+
*/
|
|
10
|
+
export interface TranslatorEndpoint {
|
|
11
|
+
url: string;
|
|
12
|
+
name: string;
|
|
13
|
+
priority: number;
|
|
14
|
+
isHealthy: boolean;
|
|
15
|
+
lastHealthCheck: Date | null;
|
|
16
|
+
consecutiveFailures: number;
|
|
17
|
+
}
|
|
18
|
+
declare class TranslatorLoadBalancer {
|
|
19
|
+
private config;
|
|
20
|
+
private circuitBreakers;
|
|
21
|
+
private healthCheckInterval;
|
|
22
|
+
private currentEndpointIndex;
|
|
23
|
+
constructor();
|
|
24
|
+
/**
|
|
25
|
+
* Initialize endpoints from environment variable
|
|
26
|
+
* Format: url1|name1,url2|name2 or just url1,url2
|
|
27
|
+
*/
|
|
28
|
+
private initializeEndpoints;
|
|
29
|
+
/**
|
|
30
|
+
* Get the next healthy endpoint using weighted round-robin
|
|
31
|
+
*/
|
|
32
|
+
getNextEndpoint(): TranslatorEndpoint | null;
|
|
33
|
+
/**
|
|
34
|
+
* Mark an endpoint as failed
|
|
35
|
+
*/
|
|
36
|
+
markEndpointFailed(endpoint: TranslatorEndpoint): void;
|
|
37
|
+
/**
|
|
38
|
+
* Mark an endpoint as successful
|
|
39
|
+
*/
|
|
40
|
+
markEndpointSuccess(endpoint: TranslatorEndpoint): void;
|
|
41
|
+
/**
|
|
42
|
+
* Execute a translation request with automatic failover and exponential backoff
|
|
43
|
+
*/
|
|
44
|
+
executeWithFailover<T>(requestFn: (url: string) => Promise<T>): Promise<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Sleep utility
|
|
47
|
+
*/
|
|
48
|
+
private sleep;
|
|
49
|
+
/**
|
|
50
|
+
* Perform health check on all endpoints
|
|
51
|
+
*/
|
|
52
|
+
healthCheck(): Promise<Record<string, boolean>>;
|
|
53
|
+
/**
|
|
54
|
+
* Start periodic health checks
|
|
55
|
+
*/
|
|
56
|
+
startHealthChecks(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Stop periodic health checks
|
|
59
|
+
*/
|
|
60
|
+
stopHealthChecks(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Get current status of all endpoints
|
|
63
|
+
*/
|
|
64
|
+
getStatus(): Array<{
|
|
65
|
+
name: string;
|
|
66
|
+
url: string;
|
|
67
|
+
isHealthy: boolean;
|
|
68
|
+
failures: number;
|
|
69
|
+
}>;
|
|
70
|
+
}
|
|
71
|
+
export declare function getTranslatorLoadBalancer(): TranslatorLoadBalancer;
|
|
72
|
+
export declare const translatorLoadBalancer: TranslatorLoadBalancer;
|
|
73
|
+
export {};
|