@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.
Files changed (104) hide show
  1. package/dist/analysis/analyze.js +1 -1
  2. package/dist/detection/stack.d.ts +42 -0
  3. package/dist/detection/stack.js +277 -0
  4. package/dist/django/catalog.d.ts +92 -0
  5. package/dist/django/catalog.js +677 -0
  6. package/dist/django/extract.d.ts +20 -0
  7. package/dist/django/extract.js +79 -0
  8. package/dist/django/inject-file.d.ts +6 -0
  9. package/dist/django/inject-file.js +52 -0
  10. package/dist/django/inject.d.ts +11 -0
  11. package/dist/django/inject.js +205 -0
  12. package/dist/django/keys.d.ts +21 -0
  13. package/dist/django/keys.js +63 -0
  14. package/dist/django/locale-names.d.ts +13 -0
  15. package/dist/django/locale-names.js +60 -0
  16. package/dist/django/mo-file.d.ts +6 -0
  17. package/dist/django/mo-file.js +60 -0
  18. package/dist/django/plural-forms.d.ts +34 -0
  19. package/dist/django/plural-forms.js +216 -0
  20. package/dist/django/po-file.d.ts +38 -0
  21. package/dist/django/po-file.js +273 -0
  22. package/dist/django/python-extract.d.ts +34 -0
  23. package/dist/django/python-extract.js +619 -0
  24. package/dist/django/python-lexer.d.ts +65 -0
  25. package/dist/django/python-lexer.js +398 -0
  26. package/dist/django/template-extract.d.ts +32 -0
  27. package/dist/django/template-extract.js +680 -0
  28. package/dist/django/types.d.ts +49 -0
  29. package/dist/django/types.js +15 -0
  30. package/dist/index.d.ts +5 -0
  31. package/dist/index.js +16 -2
  32. package/dist/local/pseudo-provider.js +5 -4
  33. package/dist/local/translate-core.d.ts +5 -2
  34. package/dist/local/translate-core.js +43 -5
  35. package/dist/local/translate-messages.js +7 -9
  36. package/dist/modules/extractor/extract.js +22 -14
  37. package/dist/modules/finalization/django-finalize.d.ts +15 -0
  38. package/dist/modules/finalization/django-finalize.js +156 -19
  39. package/dist/modules/finalization/finalize.d.ts +2 -0
  40. package/dist/modules/finalization/finalize.js +58 -30
  41. package/dist/modules/finalization/nextjs-finalize.d.ts +4 -1
  42. package/dist/modules/finalization/nextjs-finalize.js +8 -9
  43. package/dist/modules/finalization/writes.d.ts +29 -0
  44. package/dist/modules/finalization/writes.js +78 -0
  45. package/dist/modules/format-injection/index.js +3 -2
  46. package/dist/modules/glossary/index.d.ts +1 -2
  47. package/dist/modules/glossary/index.js +14 -6
  48. package/dist/modules/glossary/rules.d.ts +81 -0
  49. package/dist/modules/glossary/rules.js +244 -0
  50. package/dist/modules/injection/inject.js +14 -30
  51. package/dist/modules/keygen/keygen.js +7 -0
  52. package/dist/modules/locale-builder/locales.js +23 -0
  53. package/dist/modules/lockfile/index.d.ts +1 -1
  54. package/dist/modules/lockfile/index.js +2 -1
  55. package/dist/modules/lockfile/lockfile-manager.d.ts +9 -0
  56. package/dist/modules/lockfile/lockfile-manager.js +16 -1
  57. package/dist/modules/lockfile/schema.d.ts +42 -0
  58. package/dist/modules/lockfile/schema.js +11 -0
  59. package/dist/modules/translator/icu-branches.d.ts +138 -0
  60. package/dist/modules/translator/icu-branches.js +393 -0
  61. package/dist/modules/translator/translate.d.ts +40 -4
  62. package/dist/modules/translator/translate.js +297 -173
  63. package/dist/modules/workspace/validate.js +67 -32
  64. package/dist/orchestrator.d.ts +5 -0
  65. package/dist/orchestrator.js +125 -14
  66. package/dist/providers/types.d.ts +10 -2
  67. package/dist/qa/check-project.d.ts +2 -1
  68. package/dist/qa/check-project.js +50 -7
  69. package/dist/qa/gettext-files.d.ts +15 -0
  70. package/dist/qa/gettext-files.js +115 -0
  71. package/dist/qa/locale-qa.js +20 -3
  72. package/dist/runtime/config.js +1 -0
  73. package/dist/runtime/types.d.ts +64 -0
  74. package/dist/utils/extraction-options.d.ts +9 -3
  75. package/dist/utils/extraction-options.js +13 -3
  76. package/dist/utils/file-utils.js +7 -2
  77. package/dist/utils/icu-message.d.ts +2 -2
  78. package/dist/utils/icu-message.js +43 -7
  79. package/dist/utils/jsx-message.js +7 -0
  80. package/dist/utils/metadata.d.ts +4 -2
  81. package/dist/utils/metadata.js +16 -10
  82. package/dist/utils/not-injected.js +2 -0
  83. package/dist/utils/placeholder-protection.d.ts +18 -1
  84. package/dist/utils/placeholder-protection.js +58 -13
  85. package/dist/utils/translation-sanitizer.d.ts +1 -0
  86. package/dist/utils/translation-sanitizer.js +23 -9
  87. package/dist/utils/translator-load-balancer.js +5 -1
  88. package/dist/utils/untranslated.d.ts +15 -0
  89. package/dist/utils/untranslated.js +34 -0
  90. package/package.json +2 -6
  91. package/dist/injection/django/django-template-injector.d.ts +0 -29
  92. package/dist/injection/django/django-template-injector.js +0 -216
  93. package/dist/modules/glossary/glossary.d.ts +0 -9
  94. package/dist/modules/glossary/glossary.js +0 -27
  95. package/dist/modules/glossary/masker.d.ts +0 -23
  96. package/dist/modules/glossary/masker.js +0 -56
  97. package/dist/modules/locale-builder/po.d.ts +0 -33
  98. package/dist/modules/locale-builder/po.js +0 -131
  99. package/dist/modules/translator/translator.d.ts +0 -5
  100. package/dist/modules/translator/translator.js +0 -46
  101. package/dist/utils/dtl-mask.d.ts +0 -25
  102. package/dist/utils/dtl-mask.js +0 -58
  103. package/dist/utils/py-extractor.d.ts +0 -14
  104. 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
- function buildPattern(neverTranslateTerms, icu) {
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 terms = [...new Set(neverTranslateTerms.filter((t) => t.trim().length > 0))]
57
- .sort((a, b) => b.length - a.length)
58
- .map((t) => `(?<![\\w])${escapeRegex(t)}(?![\\w])`);
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('|'), 'gi');
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 pattern = buildPattern(neverTranslateTerms, options.icu ?? false);
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, (match) => {
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
- /** Swap tokens back for their original text. Call only after checkTokens passed. */
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
  }
@@ -6,5 +6,6 @@
6
6
  */
7
7
  export declare function sanitizeTranslation(text: string, options?: {
8
8
  nllb?: boolean;
9
+ source?: string;
9
10
  }): string;
10
11
  export declare function validateRawTokens(aiOutput: string, expectedCount: number): boolean;
@@ -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('\\b(?:__)?[a-z]{3}_[A-Z][a-z]{3,4}(?:__)?\\b', 'g');
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
- 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(' ');
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
- return collapsePhraseStutter(clean);
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.0",
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.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
- }