@babelize/engine 0.1.0 → 0.1.3
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/stack.d.ts +42 -0
- package/dist/detection/stack.js +277 -0
- package/dist/django/catalog.d.ts +92 -0
- package/dist/django/catalog.js +677 -0
- package/dist/django/extract.d.ts +20 -0
- package/dist/django/extract.js +79 -0
- package/dist/django/inject-file.d.ts +6 -0
- package/dist/django/inject-file.js +52 -0
- package/dist/django/inject.d.ts +11 -0
- package/dist/django/inject.js +205 -0
- package/dist/django/keys.d.ts +21 -0
- package/dist/django/keys.js +63 -0
- package/dist/django/locale-names.d.ts +13 -0
- package/dist/django/locale-names.js +60 -0
- package/dist/django/mo-file.d.ts +6 -0
- package/dist/django/mo-file.js +60 -0
- package/dist/django/plural-forms.d.ts +34 -0
- package/dist/django/plural-forms.js +216 -0
- package/dist/django/po-file.d.ts +38 -0
- package/dist/django/po-file.js +273 -0
- package/dist/django/python-extract.d.ts +34 -0
- package/dist/django/python-extract.js +619 -0
- package/dist/django/python-lexer.d.ts +65 -0
- package/dist/django/python-lexer.js +398 -0
- package/dist/django/template-extract.d.ts +32 -0
- package/dist/django/template-extract.js +680 -0
- package/dist/django/types.d.ts +49 -0
- package/dist/django/types.js +15 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +16 -2
- package/dist/local/pseudo-provider.js +5 -4
- package/dist/local/translate-core.d.ts +5 -2
- package/dist/local/translate-core.js +43 -5
- package/dist/local/translate-messages.js +7 -9
- package/dist/modules/extractor/extract.js +22 -14
- package/dist/modules/finalization/django-finalize.d.ts +15 -0
- package/dist/modules/finalization/django-finalize.js +156 -19
- package/dist/modules/finalization/finalize.d.ts +2 -0
- package/dist/modules/finalization/finalize.js +58 -30
- package/dist/modules/finalization/nextjs-finalize.d.ts +4 -1
- package/dist/modules/finalization/nextjs-finalize.js +8 -9
- package/dist/modules/finalization/writes.d.ts +29 -0
- package/dist/modules/finalization/writes.js +78 -0
- package/dist/modules/format-injection/index.js +3 -2
- package/dist/modules/glossary/index.d.ts +1 -2
- package/dist/modules/glossary/index.js +14 -6
- package/dist/modules/glossary/rules.d.ts +81 -0
- package/dist/modules/glossary/rules.js +244 -0
- package/dist/modules/injection/inject.js +14 -30
- package/dist/modules/keygen/keygen.js +7 -0
- package/dist/modules/locale-builder/locales.js +23 -0
- 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 +16 -1
- package/dist/modules/lockfile/schema.d.ts +42 -0
- package/dist/modules/lockfile/schema.js +11 -0
- package/dist/modules/translator/icu-branches.d.ts +138 -0
- package/dist/modules/translator/icu-branches.js +393 -0
- package/dist/modules/translator/translate.d.ts +40 -4
- package/dist/modules/translator/translate.js +297 -173
- package/dist/modules/workspace/validate.js +67 -32
- package/dist/orchestrator.d.ts +5 -0
- package/dist/orchestrator.js +125 -14
- package/dist/providers/types.d.ts +10 -2
- package/dist/qa/check-project.d.ts +2 -1
- package/dist/qa/check-project.js +50 -7
- package/dist/qa/gettext-files.d.ts +15 -0
- package/dist/qa/gettext-files.js +115 -0
- package/dist/qa/locale-qa.js +20 -3
- package/dist/runtime/config.js +1 -0
- package/dist/runtime/types.d.ts +64 -0
- package/dist/utils/extraction-options.d.ts +9 -3
- package/dist/utils/extraction-options.js +13 -3
- package/dist/utils/file-utils.js +7 -2
- package/dist/utils/icu-message.d.ts +2 -2
- package/dist/utils/icu-message.js +43 -7
- package/dist/utils/jsx-message.js +7 -0
- package/dist/utils/metadata.d.ts +4 -2
- package/dist/utils/metadata.js +16 -10
- package/dist/utils/not-injected.js +2 -0
- package/dist/utils/placeholder-protection.d.ts +18 -1
- package/dist/utils/placeholder-protection.js +58 -13
- package/dist/utils/translation-sanitizer.d.ts +1 -0
- package/dist/utils/translation-sanitizer.js +23 -9
- package/dist/utils/translator-load-balancer.js +5 -1
- package/dist/utils/untranslated.d.ts +15 -0
- package/dist/utils/untranslated.js +34 -0
- package/package.json +2 -6
- package/dist/injection/django/django-template-injector.d.ts +0 -29
- package/dist/injection/django/django-template-injector.js +0 -216
- package/dist/modules/glossary/glossary.d.ts +0 -9
- package/dist/modules/glossary/glossary.js +0 -27
- package/dist/modules/glossary/masker.d.ts +0 -23
- package/dist/modules/glossary/masker.js +0 -56
- package/dist/modules/locale-builder/po.d.ts +0 -33
- package/dist/modules/locale-builder/po.js +0 -131
- package/dist/modules/translator/translator.d.ts +0 -5
- package/dist/modules/translator/translator.js +0 -46
- package/dist/utils/dtl-mask.d.ts +0 -25
- package/dist/utils/dtl-mask.js +0 -58
- package/dist/utils/py-extractor.d.ts +0 -14
- package/dist/utils/py-extractor.js +0 -285
|
@@ -37,51 +37,93 @@ const PROTECTED_PATTERNS = [
|
|
|
37
37
|
{ kind: 'template', source: String.raw `\$\{[^{}]*\}` },
|
|
38
38
|
// i18next nesting: $t(key) / $t(key, {...})
|
|
39
39
|
{ kind: 'nesting', source: String.raw `\$t\([^()]*\)` },
|
|
40
|
-
// Python named printf: %(name)s
|
|
41
|
-
{ kind: 'printf', source: String.raw `%\(\w+\)[sdifr]` },
|
|
40
|
+
// Python named printf: %(name)s, %(count)d, %(price).2f
|
|
41
|
+
{ kind: 'printf', source: String.raw `%\(\w+\)[-#0 +]*\d*(?:\.\d+)?[sdifr]` },
|
|
42
|
+
// Escaped percent in printf-style (gettext) strings: "50%% off"
|
|
43
|
+
{ kind: 'printf', source: '%%' },
|
|
42
44
|
// printf: %s %d %1$s %.2f %@ — not "%" followed by a word ("50%done")
|
|
43
45
|
{ kind: 'printf', source: String.raw `%(?:\d+\$)?[-+0#]*\d*(?:\.\d+)?[sdifu@xXc](?![A-Za-z])` },
|
|
44
46
|
// ICU simple / formatted args and positional args: {name} {0} {price, number} {d, date, short}
|
|
45
47
|
{ kind: 'argument', source: String.raw `\{\s*[\w.]+\s*(?:,\s*(?:number|date|time)\s*(?:,\s*[^{}]+)?)?\}` },
|
|
48
|
+
// Python str.format positional field: "Only {} left"
|
|
49
|
+
{ kind: 'argument', source: String.raw `\{\}` },
|
|
50
|
+
// HTML character references in markup-bearing text: — ’
|
|
51
|
+
{ kind: 'entity', source: String.raw `&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#\d{1,7}|#x[0-9a-fA-F]{1,6});` },
|
|
46
52
|
// Markup / react-i18next Trans tags: <0>, </0>, <1/>, <b>, </strong>, <br />, <Link href="...">
|
|
47
53
|
{ kind: 'tag', source: String.raw `<\/?\d+\s*\/?>|<\/?[A-Za-z][\w.:-]*(?:\s+[^<>]*?)?\s*\/?>` },
|
|
48
54
|
];
|
|
49
55
|
function escapeRegex(s) {
|
|
50
56
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
51
57
|
}
|
|
58
|
+
/** A term as a whole term in any script (same rule as modules/glossary termPattern). */
|
|
59
|
+
function wholeTerm(term) {
|
|
60
|
+
return `(?<![\\p{L}\\p{N}_])${escapeRegex(term)}(?![\\p{L}\\p{N}_])`;
|
|
61
|
+
}
|
|
52
62
|
/** In ICU messages braces are structure, so only these kinds are tokenized. */
|
|
53
|
-
const ICU_SAFE_KINDS = new Set(['url', 'email', 'code', 'tag']);
|
|
54
|
-
|
|
63
|
+
const ICU_SAFE_KINDS = new Set(['url', 'email', 'code', 'tag', 'entity']);
|
|
64
|
+
/**
|
|
65
|
+
* Placeholders, tags, URLs etc. come first, so a term never matches inside
|
|
66
|
+
* one ("{dashboard}", "https://\u2026/settings"). Terms match longest first; a
|
|
67
|
+
* never-translate term wins over a fixed term spelled the same (ignoring
|
|
68
|
+
* case). Fixed terms are the named groups `f<i>` (index into `fixed`).
|
|
69
|
+
*/
|
|
70
|
+
function buildPattern(neverTranslateTerms, icu, fixedTerms) {
|
|
55
71
|
const sources = PROTECTED_PATTERNS.filter((p) => !icu || ICU_SAFE_KINDS.has(p.kind)).map((p) => p.source);
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
const never = [...new Set(neverTranslateTerms.filter((t) => t.trim().length > 0))];
|
|
73
|
+
const neverLower = new Set(never.map((t) => t.toLowerCase()));
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const fixed = fixedTerms.filter((f) => {
|
|
76
|
+
const k = f.term.toLowerCase();
|
|
77
|
+
if (!f.term.trim() || !f.translation || neverLower.has(k) || seen.has(k))
|
|
78
|
+
return false;
|
|
79
|
+
seen.add(k);
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
const terms = [
|
|
83
|
+
...never.map((t) => ({ term: t, source: wholeTerm(t) })),
|
|
84
|
+
...fixed.map((f, i) => ({ term: f.term, source: `(?<f${i}>${wholeTerm(f.term)})` })),
|
|
85
|
+
].sort((a, b) => b.term.length - a.term.length);
|
|
59
86
|
if (terms.length > 0)
|
|
60
|
-
sources.push(terms.join('|'));
|
|
61
|
-
return new RegExp(sources.map((s) => `(?:${s})`).join('|'), '
|
|
87
|
+
sources.push(terms.map((t) => t.source).join('|'));
|
|
88
|
+
return { pattern: new RegExp(sources.map((s) => `(?:${s})`).join('|'), 'giu'), fixed };
|
|
62
89
|
}
|
|
63
90
|
/**
|
|
64
91
|
* Replace every protected span (and every never-translate term) with a token.
|
|
65
92
|
* With `icu: true`, braces are left alone (they are ICU structure) and only
|
|
66
93
|
* URLs, emails, code, tags and terms are tokenized.
|
|
94
|
+
* `fixedTerms` ("always translate as") are tokenized too, and `restore` maps
|
|
95
|
+
* their tokens to the fixed translation: a plain-text translator never sees
|
|
96
|
+
* the term, so it can't ignore the rule.
|
|
67
97
|
* Literal `[#VAR_n#]` text already present in the source is protected too, so
|
|
68
98
|
* it can never be confused with an issued token.
|
|
69
99
|
*/
|
|
70
100
|
function protectText(text, neverTranslateTerms = [], options = {}) {
|
|
71
101
|
const tokens = [];
|
|
72
|
-
const
|
|
102
|
+
const restore = [];
|
|
103
|
+
const { pattern, fixed } = buildPattern(neverTranslateTerms, options.icu ?? false, options.fixedTerms ?? []);
|
|
73
104
|
// Guard pre-existing token-shaped text first.
|
|
74
105
|
const guarded = text.replace(TOKEN_PATTERN, (m) => {
|
|
75
106
|
tokens.push(m);
|
|
107
|
+
restore.push(m);
|
|
76
108
|
return `\uE000${tokens.length - 1}\uE000`;
|
|
77
109
|
});
|
|
78
110
|
const protectedText = guarded
|
|
79
|
-
.replace(pattern, (
|
|
111
|
+
.replace(pattern, (...args) => {
|
|
112
|
+
const match = args[0];
|
|
113
|
+
const groups = args[args.length - 1];
|
|
114
|
+
let replacement = match;
|
|
115
|
+
if (fixed.length > 0 && groups && typeof groups === 'object') {
|
|
116
|
+
const g = groups;
|
|
117
|
+
const i = fixed.findIndex((_, idx) => g[`f${idx}`] !== undefined);
|
|
118
|
+
if (i >= 0)
|
|
119
|
+
replacement = fixed[i].translation;
|
|
120
|
+
}
|
|
80
121
|
tokens.push(match);
|
|
122
|
+
restore.push(replacement);
|
|
81
123
|
return tokenFor(tokens.length - 1);
|
|
82
124
|
})
|
|
83
125
|
.replace(/\uE000(\d+)\uE000/g, (_, i) => tokenFor(Number(i)));
|
|
84
|
-
return { text: protectedText, tokens };
|
|
126
|
+
return { text: protectedText, tokens, restore };
|
|
85
127
|
}
|
|
86
128
|
/** Every issued token must appear exactly once, and no other token may appear. */
|
|
87
129
|
function checkTokens(translated, tokenCount) {
|
|
@@ -102,7 +144,10 @@ function checkTokens(translated, tokenCount) {
|
|
|
102
144
|
const unexpected = [...counts.keys()].filter((i) => i >= tokenCount);
|
|
103
145
|
return { ok: missing.length === 0 && duplicated.length === 0 && unexpected.length === 0, missing, duplicated, unexpected };
|
|
104
146
|
}
|
|
105
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Swap tokens back. Pass protectText's `restore` (fixed glossary terms become
|
|
149
|
+
* their translation) or `tokens` (original text). Call only after checkTokens passed.
|
|
150
|
+
*/
|
|
106
151
|
function restoreText(translated, tokens) {
|
|
107
152
|
return translated.replace(TOKEN_PATTERN, (m, i) => tokens[Number(i)] ?? m);
|
|
108
153
|
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sanitizeTranslation = sanitizeTranslation;
|
|
4
4
|
exports.validateRawTokens = validateRawTokens;
|
|
5
|
-
const NLLB_ARTIFACT_PATTERN = new RegExp(
|
|
5
|
+
const NLLB_ARTIFACT_PATTERN = new RegExp(
|
|
6
|
+
// Sentence-initial echoes come back capitalized ("Eng_Latn Produktkatalog").
|
|
7
|
+
'\\b(?:__)?[A-Za-z][a-z]{2}_[A-Z][a-z]{3,4}(?:__)?\\b', 'g');
|
|
6
8
|
/**
|
|
7
9
|
* Collapse phrase-level stutter ("Il a Il a" -> "Il a"). NLLB occasionally
|
|
8
10
|
* repeats a short fragment twice after language tokens are stripped. Only
|
|
@@ -11,14 +13,19 @@ const NLLB_ARTIFACT_PATTERN = new RegExp('\\b(?:__)?[a-z]{3}_[A-Z][a-z]{3,4}(?:_
|
|
|
11
13
|
*/
|
|
12
14
|
function collapsePhraseStutter(text) {
|
|
13
15
|
const words = text.split(/\s+/);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// The whole output is one phrase of >=2 words repeated 2+ times ("Il a Il a",
|
|
17
|
+
// "Product catalog Product catalog Product catalog").
|
|
18
|
+
for (let size = 2; size <= words.length / 2; size++) {
|
|
19
|
+
if (words.length % size !== 0)
|
|
20
|
+
continue;
|
|
21
|
+
const first = words.slice(0, size).map((w) => w.toLowerCase());
|
|
22
|
+
let repeated = true;
|
|
23
|
+
for (let i = size; i < words.length && repeated; i++) {
|
|
24
|
+
if (words[i].toLowerCase() !== first[i % size])
|
|
25
|
+
repeated = false;
|
|
21
26
|
}
|
|
27
|
+
if (repeated)
|
|
28
|
+
return words.slice(0, size).join(' ');
|
|
22
29
|
}
|
|
23
30
|
return text;
|
|
24
31
|
}
|
|
@@ -45,7 +52,14 @@ function sanitizeTranslation(text, options = {}) {
|
|
|
45
52
|
// legitimate translated text.
|
|
46
53
|
clean = clean.replace(/\u2047/g, '');
|
|
47
54
|
clean = clean.replace(/ +/g, ' ').trim();
|
|
48
|
-
|
|
55
|
+
clean = collapsePhraseStutter(clean);
|
|
56
|
+
// A one-word source doubled in the output ("Product" → "Produkt Produkt").
|
|
57
|
+
if (options.source && !/\s/.test(options.source.trim())) {
|
|
58
|
+
const words = clean.split(/\s+/);
|
|
59
|
+
if (words.length > 1 && words.every((w) => w.toLowerCase() === words[0].toLowerCase()))
|
|
60
|
+
clean = words[0];
|
|
61
|
+
}
|
|
62
|
+
return clean;
|
|
49
63
|
}
|
|
50
64
|
function validateRawTokens(aiOutput, expectedCount) {
|
|
51
65
|
for (let i = 0; i < expectedCount; i++) {
|
|
@@ -27,7 +27,7 @@ class TranslatorLoadBalancer {
|
|
|
27
27
|
this.config = { ...DEFAULT_CONFIG };
|
|
28
28
|
this.initializeEndpoints();
|
|
29
29
|
if (this.config.endpoints.length > 0) {
|
|
30
|
-
setTimeout(() => this.startHealthChecks(), 0);
|
|
30
|
+
setTimeout(() => this.startHealthChecks(), 0).unref?.();
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
@@ -214,6 +214,7 @@ class TranslatorLoadBalancer {
|
|
|
214
214
|
try {
|
|
215
215
|
const controller = new AbortController();
|
|
216
216
|
timeout = setTimeout(() => controller.abort(), this.config.healthCheckTimeout);
|
|
217
|
+
timeout.unref?.();
|
|
217
218
|
const response = await fetch(`${endpoint.url}/health`, {
|
|
218
219
|
method: 'GET',
|
|
219
220
|
signal: controller.signal,
|
|
@@ -250,6 +251,9 @@ class TranslatorLoadBalancer {
|
|
|
250
251
|
if (this.healthCheckInterval)
|
|
251
252
|
return;
|
|
252
253
|
this.healthCheckInterval = setInterval(() => this.healthCheck(), this.config.healthCheckInterval);
|
|
254
|
+
// Background monitoring must not keep the process alive: short-lived
|
|
255
|
+
// hosts (scripts, the local runner) would otherwise never exit.
|
|
256
|
+
this.healthCheckInterval.unref?.();
|
|
253
257
|
// Initial health check
|
|
254
258
|
this.healthCheck();
|
|
255
259
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects source text that came back from translation unchanged.
|
|
3
|
+
*
|
|
4
|
+
* Into a Latin-script language an unchanged short string can be a real
|
|
5
|
+
* translation ("Hotel", "Email"). Into a non-Latin-script language, English
|
|
6
|
+
* letters returned verbatim were not translated. Such values must be rejected
|
|
7
|
+
* when the translator returns them AND when a cache or TM lookup does: once
|
|
8
|
+
* stored, a bad value would otherwise be replayed on every run and never retried.
|
|
9
|
+
*/
|
|
10
|
+
/** Is the locale written in Latin script (where an unchanged word can be a real translation)? */
|
|
11
|
+
export declare function isLatinScript(locale: string): boolean;
|
|
12
|
+
/** Letters left once placeholders, tags and entities are removed. */
|
|
13
|
+
export declare function hasWords(text: string | undefined): boolean;
|
|
14
|
+
/** `translated` is the untouched source, into a language where that cannot be a translation. */
|
|
15
|
+
export declare function isUntranslatedInto(source: string, translated: string, targetLocale: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Detects source text that came back from translation unchanged.
|
|
4
|
+
*
|
|
5
|
+
* Into a Latin-script language an unchanged short string can be a real
|
|
6
|
+
* translation ("Hotel", "Email"). Into a non-Latin-script language, English
|
|
7
|
+
* letters returned verbatim were not translated. Such values must be rejected
|
|
8
|
+
* when the translator returns them AND when a cache or TM lookup does: once
|
|
9
|
+
* stored, a bad value would otherwise be replayed on every run and never retried.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.isLatinScript = isLatinScript;
|
|
13
|
+
exports.hasWords = hasWords;
|
|
14
|
+
exports.isUntranslatedInto = isUntranslatedInto;
|
|
15
|
+
/** Is the locale written in Latin script (where an unchanged word can be a real translation)? */
|
|
16
|
+
function isLatinScript(locale) {
|
|
17
|
+
try {
|
|
18
|
+
return new Intl.Locale(locale).maximize().script === 'Latn';
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Letters left once placeholders, tags and entities are removed. */
|
|
25
|
+
function hasWords(text) {
|
|
26
|
+
if (!text)
|
|
27
|
+
return false;
|
|
28
|
+
const stripped = text.replace(/%\(\w+\)[-#0 +]*\d*(?:\.\d+)?[a-zA-Z]|%[sdif%]|\{[^{}]*\}|<[^>]*>|&[#\w]+;|\[\s*#\s*VAR_\d+\s*#\s*\]/gi, ' ');
|
|
29
|
+
return /\p{L}{2,}/u.test(stripped);
|
|
30
|
+
}
|
|
31
|
+
/** `translated` is the untouched source, into a language where that cannot be a translation. */
|
|
32
|
+
function isUntranslatedInto(source, translated, targetLocale) {
|
|
33
|
+
return !isLatinScript(targetLocale) && translated.trim() === source.trim() && hasWords(source);
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@babelize/engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Babelize translation pipeline (extract, keygen, translate, inject). Infrastructure is injected through provider/store interfaces.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"babelize",
|
|
@@ -11,10 +11,6 @@
|
|
|
11
11
|
],
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"homepage": "https://babelize.co",
|
|
14
|
-
"repository": {
|
|
15
|
-
"type": "git",
|
|
16
|
-
"url": "git+https://github.com/babelize/babelize-dashboard.git"
|
|
17
|
-
},
|
|
18
14
|
"main": "./dist/index.js",
|
|
19
15
|
"types": "./dist/index.d.ts",
|
|
20
16
|
"files": [
|
|
@@ -36,7 +32,7 @@
|
|
|
36
32
|
"@babel/parser": "^7.28.5",
|
|
37
33
|
"@babel/traverse": "^7.28.5",
|
|
38
34
|
"@babel/types": "^7.29.8",
|
|
39
|
-
"@babelize/types": "^0.1.
|
|
35
|
+
"@babelize/types": "^0.1.2",
|
|
40
36
|
"@vue/compiler-dom": "^3.5",
|
|
41
37
|
"@vue/compiler-sfc": "^3.5",
|
|
42
38
|
"diff": "^8.0.4",
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { FileInjection } from '../../runtime/types';
|
|
2
|
-
export interface DjangoInjectorOptions {
|
|
3
|
-
/** The masked HTML content after standard HTML injection has run */
|
|
4
|
-
maskedContent: string;
|
|
5
|
-
/** The restore function from dtl-mask to undo masking */
|
|
6
|
-
restore: (masked: string) => string;
|
|
7
|
-
/** Path of the file being processed */
|
|
8
|
-
filePath: string;
|
|
9
|
-
/** Mapping from extraction ID to translation key */
|
|
10
|
-
keyMapping: Map<string, string>;
|
|
11
|
-
/** Original unmodified source content for comparison */
|
|
12
|
-
sourceContent: string;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Convert data-i18n attributes to Django {% trans %} tags.
|
|
16
|
-
*
|
|
17
|
-
* For pure text elements:
|
|
18
|
-
* <p data-i18n="hello">Hello</p> → <p>{% trans "hello" %}</p>
|
|
19
|
-
*
|
|
20
|
-
* For mixed content (elements with both text and child elements):
|
|
21
|
-
* <p data-i18n="hello">Hello <em>world</em></p> →
|
|
22
|
-
* <p>{% blocktrans %}Hello <em>world</em>{% endblocktrans %}</p>
|
|
23
|
-
* (blocktrans wraps the raw inner HTML)
|
|
24
|
-
*
|
|
25
|
-
* For attribute translations:
|
|
26
|
-
* <input data-i18n-placeholder="ph_key" placeholder="Search..."> →
|
|
27
|
-
* <input placeholder="{% trans "ph_key" %}">
|
|
28
|
-
*/
|
|
29
|
-
export declare function injectDjangoTemplates(options: DjangoInjectorOptions): Promise<FileInjection>;
|
|
@@ -1,216 +0,0 @@
|
|
|
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.injectDjangoTemplates = injectDjangoTemplates;
|
|
37
|
-
function asElement(node) {
|
|
38
|
-
return node.nodeType === 1 ? node : null;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Convert data-i18n attributes to Django {% trans %} tags.
|
|
42
|
-
*
|
|
43
|
-
* For pure text elements:
|
|
44
|
-
* <p data-i18n="hello">Hello</p> → <p>{% trans "hello" %}</p>
|
|
45
|
-
*
|
|
46
|
-
* For mixed content (elements with both text and child elements):
|
|
47
|
-
* <p data-i18n="hello">Hello <em>world</em></p> →
|
|
48
|
-
* <p>{% blocktrans %}Hello <em>world</em>{% endblocktrans %}</p>
|
|
49
|
-
* (blocktrans wraps the raw inner HTML)
|
|
50
|
-
*
|
|
51
|
-
* For attribute translations:
|
|
52
|
-
* <input data-i18n-placeholder="ph_key" placeholder="Search..."> →
|
|
53
|
-
* <input placeholder="{% trans "ph_key" %}">
|
|
54
|
-
*/
|
|
55
|
-
async function injectDjangoTemplates(options) {
|
|
56
|
-
const { maskedContent, restore, filePath, keyMapping } = options;
|
|
57
|
-
const { parse } = await Promise.resolve().then(() => __importStar(require('node-html-parser')));
|
|
58
|
-
const injections = [];
|
|
59
|
-
const importAdded = false;
|
|
60
|
-
const hookAdded = false;
|
|
61
|
-
// Parse the masked + injected HTML
|
|
62
|
-
const root = parse(maskedContent, {
|
|
63
|
-
comment: false,
|
|
64
|
-
blockTextElements: { script: true, style: true, noscript: true, pre: true },
|
|
65
|
-
});
|
|
66
|
-
const htmlEl = root.querySelector('html');
|
|
67
|
-
if (!htmlEl) {
|
|
68
|
-
return {
|
|
69
|
-
file: filePath,
|
|
70
|
-
originalContent: options.sourceContent,
|
|
71
|
-
modifiedContent: restore(maskedContent),
|
|
72
|
-
injections,
|
|
73
|
-
importAdded,
|
|
74
|
-
hookAdded,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
walkAndInject(htmlEl, filePath, options.sourceContent, keyMapping, injections);
|
|
78
|
-
let modifiedContent = root.toString();
|
|
79
|
-
// Remove the i18n.js script tag injected by HTML pipeline — Django uses {% trans %} not client JS
|
|
80
|
-
modifiedContent = modifiedContent.replace(/<script[^>]*src\s*=\s*["']i18n\.js["'][^>]*>\s*<\/script>\s*\n?/g, '');
|
|
81
|
-
// Add {% load i18n %} after any {% extends %} tag (DTL requires extends first)
|
|
82
|
-
modifiedContent = ensureI18nLoad(modifiedContent);
|
|
83
|
-
// Restore template tags
|
|
84
|
-
modifiedContent = restore(modifiedContent);
|
|
85
|
-
return {
|
|
86
|
-
file: filePath,
|
|
87
|
-
originalContent: options.sourceContent,
|
|
88
|
-
modifiedContent,
|
|
89
|
-
injections,
|
|
90
|
-
importAdded,
|
|
91
|
-
hookAdded,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function walkAndInject(node, filePath, sourceContent, keyMapping, injections) {
|
|
95
|
-
const element = asElement(node);
|
|
96
|
-
if (!element)
|
|
97
|
-
return;
|
|
98
|
-
const rawTagName = element.rawTagName?.toLowerCase();
|
|
99
|
-
if (!rawTagName)
|
|
100
|
-
return;
|
|
101
|
-
if (['script', 'style', 'noscript', 'code', 'pre', 'svg'].includes(rawTagName))
|
|
102
|
-
return;
|
|
103
|
-
// Handle data-i18n on this element
|
|
104
|
-
const i18nKey = element.getAttribute('data-i18n');
|
|
105
|
-
if (i18nKey) {
|
|
106
|
-
convertDataI18nToTransTag(element, i18nKey, filePath, sourceContent, injections);
|
|
107
|
-
}
|
|
108
|
-
// Handle data-i18n-{attr} for attribute translations
|
|
109
|
-
convertDataI18nAttrTags(element, filePath, sourceContent, injections);
|
|
110
|
-
// Recurse into children
|
|
111
|
-
for (const child of node.childNodes || []) {
|
|
112
|
-
if (child.nodeType === 1) {
|
|
113
|
-
walkAndInject(child, filePath, sourceContent, keyMapping, injections);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function convertDataI18nToTransTag(element, key, filePath, sourceContent, injections) {
|
|
118
|
-
const line = element.range
|
|
119
|
-
? (sourceContent.slice(0, element.range[0]).match(/\n/g) || []).length + 1
|
|
120
|
-
: 0;
|
|
121
|
-
// Remove the data-i18n attribute
|
|
122
|
-
element.removeAttribute('data-i18n');
|
|
123
|
-
const children = element.childNodes || [];
|
|
124
|
-
const hasElementChildren = children.some((c) => c.nodeType === 1);
|
|
125
|
-
if (hasElementChildren) {
|
|
126
|
-
// Mixed content: use blocktrans / endblocktrans
|
|
127
|
-
// Preserve inner HTML and wrap with blocktrans
|
|
128
|
-
const innerHtml = children
|
|
129
|
-
.map((c) => (c.nodeType === 1 ? c.toString() : c.toString()))
|
|
130
|
-
.join('');
|
|
131
|
-
// Replace children with raw blocktrans content
|
|
132
|
-
element.childNodes = [];
|
|
133
|
-
// node-html-parser doesn't support raw HTML children directly,
|
|
134
|
-
// so we set innerHTML which parses and inserts
|
|
135
|
-
element.innerHTML = `{% blocktrans %}${innerHtml}{% endblocktrans %}`;
|
|
136
|
-
injections.push({
|
|
137
|
-
file: filePath,
|
|
138
|
-
line,
|
|
139
|
-
column: 0,
|
|
140
|
-
original: innerHtml.trim(),
|
|
141
|
-
replacement: `{% blocktrans %}${innerHtml}{% endblocktrans %}`,
|
|
142
|
-
key,
|
|
143
|
-
confidence: 100,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
// Pure text content: use {% trans "key" %}
|
|
148
|
-
const text = children
|
|
149
|
-
.map((c) => c.toString())
|
|
150
|
-
.join('')
|
|
151
|
-
.trim();
|
|
152
|
-
element.childNodes = [];
|
|
153
|
-
element.innerHTML = `{% trans "${key}" %}`;
|
|
154
|
-
injections.push({
|
|
155
|
-
file: filePath,
|
|
156
|
-
line,
|
|
157
|
-
column: 0,
|
|
158
|
-
original: text,
|
|
159
|
-
replacement: `{% trans "${key}" %}`,
|
|
160
|
-
key,
|
|
161
|
-
confidence: 100,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function convertDataI18nAttrTags(element, filePath, sourceContent, injections) {
|
|
166
|
-
const i18nAttrs = [
|
|
167
|
-
'data-i18n-title',
|
|
168
|
-
'data-i18n-placeholder',
|
|
169
|
-
'data-i18n-alt',
|
|
170
|
-
'data-i18n-aria-label',
|
|
171
|
-
'data-i18n-aria-description',
|
|
172
|
-
];
|
|
173
|
-
for (const dataAttr of i18nAttrs) {
|
|
174
|
-
const key = element.getAttribute(dataAttr);
|
|
175
|
-
if (!key)
|
|
176
|
-
continue;
|
|
177
|
-
// Extract the target attribute name: "data-i18n-placeholder" → "placeholder"
|
|
178
|
-
const attrSuffix = dataAttr.replace('data-i18n-', '');
|
|
179
|
-
const originalValue = element.getAttribute(attrSuffix) || '';
|
|
180
|
-
element.removeAttribute(dataAttr);
|
|
181
|
-
const line = element.range
|
|
182
|
-
? (sourceContent.slice(0, element.range[0]).match(/\n/g) || []).length + 1
|
|
183
|
-
: 0;
|
|
184
|
-
// Replace attribute value with {% trans "key" %}
|
|
185
|
-
element.setAttribute(attrSuffix, `{% trans "${key}" %}`);
|
|
186
|
-
injections.push({
|
|
187
|
-
file: filePath,
|
|
188
|
-
line,
|
|
189
|
-
column: 0,
|
|
190
|
-
original: originalValue,
|
|
191
|
-
replacement: `{% trans "${key}" %}`,
|
|
192
|
-
key,
|
|
193
|
-
confidence: 100,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Ensure {% load i18n %} is present in the template.
|
|
199
|
-
* DTL requires it after any {% extends %} tag (at the top of the template
|
|
200
|
-
* but after extends).
|
|
201
|
-
*/
|
|
202
|
-
function ensureI18nLoad(content) {
|
|
203
|
-
if (/{%\s*load\s+i18n\s*%}/i.test(content)) {
|
|
204
|
-
return content;
|
|
205
|
-
}
|
|
206
|
-
// If there's an extends tag, insert load right after the last extends
|
|
207
|
-
const extendsMatch = content.match(/({%\s*extends\s+.*?%\})\s*\n?/s);
|
|
208
|
-
if (extendsMatch) {
|
|
209
|
-
const lastExtendsEnd = content.indexOf(extendsMatch[0]) + extendsMatch[0].length;
|
|
210
|
-
return (content.slice(0, lastExtendsEnd) +
|
|
211
|
-
'\n{% load i18n %}\n' +
|
|
212
|
-
content.slice(lastExtendsEnd));
|
|
213
|
-
}
|
|
214
|
-
// No extends: insert at the very top
|
|
215
|
-
return '{% load i18n %}\n' + content;
|
|
216
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { AlwaysTranslateAsEntry } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* Post-translation processor for "always translate as" glossary entries.
|
|
4
|
-
*
|
|
5
|
-
* After the HF model returns a translation, this function scans the result
|
|
6
|
-
* for each always-translate-as term and replaces it with the fixed
|
|
7
|
-
* translation for the target locale.
|
|
8
|
-
*/
|
|
9
|
-
export declare function applyAlwaysTranslateAs(translatedText: string, entries: AlwaysTranslateAsEntry[], targetLocale: string): string;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.applyAlwaysTranslateAs = applyAlwaysTranslateAs;
|
|
4
|
-
const string_utils_1 = require("../../utils/string-utils");
|
|
5
|
-
/**
|
|
6
|
-
* Post-translation processor for "always translate as" glossary entries.
|
|
7
|
-
*
|
|
8
|
-
* After the HF model returns a translation, this function scans the result
|
|
9
|
-
* for each always-translate-as term and replaces it with the fixed
|
|
10
|
-
* translation for the target locale.
|
|
11
|
-
*/
|
|
12
|
-
function applyAlwaysTranslateAs(translatedText, entries, targetLocale) {
|
|
13
|
-
if (!entries.length || !translatedText)
|
|
14
|
-
return translatedText;
|
|
15
|
-
let result = translatedText;
|
|
16
|
-
// Sort longest-first to avoid partial replacements
|
|
17
|
-
const sorted = [...entries].sort((a, b) => b.term.length - a.term.length);
|
|
18
|
-
for (const entry of sorted) {
|
|
19
|
-
const fixedTranslation = entry.translations[targetLocale];
|
|
20
|
-
if (!fixedTranslation)
|
|
21
|
-
continue;
|
|
22
|
-
// Replace all occurrences of the term (case-insensitive, word-boundary)
|
|
23
|
-
const regex = new RegExp(`\\b${(0, string_utils_1.escapeRegex)(entry.term)}\\b`, 'gi');
|
|
24
|
-
result = result.replace(regex, fixedTranslation);
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export interface MaskedString {
|
|
2
|
-
masked: string;
|
|
3
|
-
terms: MaskedTerm[];
|
|
4
|
-
}
|
|
5
|
-
export interface MaskedTerm {
|
|
6
|
-
term: string;
|
|
7
|
-
matches: string[];
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Replace never-translate terms with placeholders (__TERM_0__, __TERM_1__, ...)
|
|
11
|
-
* so NLLB does not attempt to translate them.
|
|
12
|
-
*
|
|
13
|
-
* Rules:
|
|
14
|
-
* - Whole-word matching only (word-boundary anchors)
|
|
15
|
-
* - Case-insensitive matching
|
|
16
|
-
* - Longest terms matched first to prevent partial overlaps
|
|
17
|
-
* - All occurrences of each term are masked
|
|
18
|
-
*/
|
|
19
|
-
export declare function applyGlossaryMask(text: string, terms: string[]): MaskedString;
|
|
20
|
-
/**
|
|
21
|
-
* Restore __TERM_N__ placeholders back to their original terms.
|
|
22
|
-
*/
|
|
23
|
-
export declare function restoreGlossaryMask(text: string, terms: MaskedTerm[]): string;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.applyGlossaryMask = applyGlossaryMask;
|
|
4
|
-
exports.restoreGlossaryMask = restoreGlossaryMask;
|
|
5
|
-
const string_utils_1 = require("../../utils/string-utils");
|
|
6
|
-
/**
|
|
7
|
-
* Replace never-translate terms with placeholders (__TERM_0__, __TERM_1__, ...)
|
|
8
|
-
* so NLLB does not attempt to translate them.
|
|
9
|
-
*
|
|
10
|
-
* Rules:
|
|
11
|
-
* - Whole-word matching only (word-boundary anchors)
|
|
12
|
-
* - Case-insensitive matching
|
|
13
|
-
* - Longest terms matched first to prevent partial overlaps
|
|
14
|
-
* - All occurrences of each term are masked
|
|
15
|
-
*/
|
|
16
|
-
function applyGlossaryMask(text, terms) {
|
|
17
|
-
if (!terms.length || !text) {
|
|
18
|
-
return { masked: text, terms: [] };
|
|
19
|
-
}
|
|
20
|
-
// Sort terms longest-first to avoid smaller terms eating larger ones.
|
|
21
|
-
const sorted = [...terms].sort((a, b) => b.length - a.length);
|
|
22
|
-
let masked = text;
|
|
23
|
-
const usedTerms = [];
|
|
24
|
-
sorted.forEach((term) => {
|
|
25
|
-
let placeholderIndex = usedTerms.findIndex((entry) => entry.term === term);
|
|
26
|
-
if (placeholderIndex === -1) {
|
|
27
|
-
placeholderIndex = usedTerms.push({ term, matches: [] }) - 1;
|
|
28
|
-
}
|
|
29
|
-
const placeholder = `__TERM_${placeholderIndex}__`;
|
|
30
|
-
// Word-boundary + case-insensitive matching
|
|
31
|
-
const regex = new RegExp(`(?:^|(?<=\\W))${(0, string_utils_1.escapeRegex)(term)}(?:$|(?=\\W))`, 'gi');
|
|
32
|
-
masked = masked.replace(regex, (matched) => {
|
|
33
|
-
usedTerms[placeholderIndex].matches.push(matched);
|
|
34
|
-
return placeholder;
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
return { masked, terms: usedTerms };
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Restore __TERM_N__ placeholders back to their original terms.
|
|
41
|
-
*/
|
|
42
|
-
function restoreGlossaryMask(text, terms) {
|
|
43
|
-
if (!terms.length || !text)
|
|
44
|
-
return text;
|
|
45
|
-
let restored = text;
|
|
46
|
-
terms.forEach((maskedTerm, index) => {
|
|
47
|
-
const placeholderRegex = new RegExp(`__TERM_${index}__`, 'g');
|
|
48
|
-
let replacementIndex = 0;
|
|
49
|
-
restored = restored.replace(placeholderRegex, () => {
|
|
50
|
-
const replacement = maskedTerm.matches[replacementIndex] ?? maskedTerm.term;
|
|
51
|
-
replacementIndex++;
|
|
52
|
-
return replacement;
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
return restored;
|
|
56
|
-
}
|