@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
@@ -1,3 +1,2 @@
1
- export { applyGlossaryMask, restoreGlossaryMask } from './masker';
2
- export { applyAlwaysTranslateAs } from './glossary';
1
+ export { canonicalLocale, canonicalizeLocaleKeys, conflictNotes, containsTerm, EMPTY_GLOSSARY, fixedTermsFor, glossaryFingerprint, hasFixedTerm, localeValue, normalizeGlossary, sourceHasTerm, termPattern, type GlossaryRuleInput, type GlossaryRules, } from './rules';
3
2
  export type { GlossaryEntry, GlossaryEntryType, BrandVoice, AlwaysTranslateAsEntry } from './types';
@@ -1,8 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyAlwaysTranslateAs = exports.restoreGlossaryMask = exports.applyGlossaryMask = void 0;
4
- var masker_1 = require("./masker");
5
- Object.defineProperty(exports, "applyGlossaryMask", { enumerable: true, get: function () { return masker_1.applyGlossaryMask; } });
6
- Object.defineProperty(exports, "restoreGlossaryMask", { enumerable: true, get: function () { return masker_1.restoreGlossaryMask; } });
7
- var glossary_1 = require("./glossary");
8
- Object.defineProperty(exports, "applyAlwaysTranslateAs", { enumerable: true, get: function () { return glossary_1.applyAlwaysTranslateAs; } });
3
+ exports.termPattern = exports.sourceHasTerm = exports.normalizeGlossary = exports.localeValue = exports.hasFixedTerm = exports.glossaryFingerprint = exports.fixedTermsFor = exports.EMPTY_GLOSSARY = exports.containsTerm = exports.conflictNotes = exports.canonicalizeLocaleKeys = exports.canonicalLocale = void 0;
4
+ var rules_1 = require("./rules");
5
+ Object.defineProperty(exports, "canonicalLocale", { enumerable: true, get: function () { return rules_1.canonicalLocale; } });
6
+ Object.defineProperty(exports, "canonicalizeLocaleKeys", { enumerable: true, get: function () { return rules_1.canonicalizeLocaleKeys; } });
7
+ Object.defineProperty(exports, "conflictNotes", { enumerable: true, get: function () { return rules_1.conflictNotes; } });
8
+ Object.defineProperty(exports, "containsTerm", { enumerable: true, get: function () { return rules_1.containsTerm; } });
9
+ Object.defineProperty(exports, "EMPTY_GLOSSARY", { enumerable: true, get: function () { return rules_1.EMPTY_GLOSSARY; } });
10
+ Object.defineProperty(exports, "fixedTermsFor", { enumerable: true, get: function () { return rules_1.fixedTermsFor; } });
11
+ Object.defineProperty(exports, "glossaryFingerprint", { enumerable: true, get: function () { return rules_1.glossaryFingerprint; } });
12
+ Object.defineProperty(exports, "hasFixedTerm", { enumerable: true, get: function () { return rules_1.hasFixedTerm; } });
13
+ Object.defineProperty(exports, "localeValue", { enumerable: true, get: function () { return rules_1.localeValue; } });
14
+ Object.defineProperty(exports, "normalizeGlossary", { enumerable: true, get: function () { return rules_1.normalizeGlossary; } });
15
+ Object.defineProperty(exports, "sourceHasTerm", { enumerable: true, get: function () { return rules_1.sourceHasTerm; } });
16
+ Object.defineProperty(exports, "termPattern", { enumerable: true, get: function () { return rules_1.termPattern; } });
@@ -0,0 +1,81 @@
1
+ import type { AlwaysTranslateAsEntry, GlossaryEntryType } from './types';
2
+ /**
3
+ * Regex source matching `term` as a whole term: not preceded or followed by a
4
+ * letter, digit or underscore in any script ("Café" matches in "Café menu",
5
+ * "app" does not match in "apply" or "appé"). Use with the `u` flag.
6
+ */
7
+ export declare function termPattern(term: string): string;
8
+ /** Whether `text` contains `term` as a whole term, ignoring case. */
9
+ export declare function containsTerm(text: string, term: string): boolean;
10
+ /**
11
+ * The words of a source string a glossary rule can apply to: placeholders,
12
+ * tags, URLs, emails and code removed, and for an ICU message its argument
13
+ * names and selector keys too. "Open https://x.com/dashboard" has no
14
+ * "dashboard" a translator would translate. With `neverTranslate`, those
15
+ * terms are removed too: "Dashboard" in "Babelize Dashboard" (never
16
+ * translated) is not a use of a "Dashboard" rule.
17
+ */
18
+ export declare function translatableText(source: string, neverTranslate?: readonly string[]): string;
19
+ /**
20
+ * Whether a glossary term occurs in a source string's translatable text (see
21
+ * translatableText). Pass the never-translate terms when asking about an
22
+ * "always translate as" term: inside a never-translate term it doesn't count.
23
+ */
24
+ export declare function sourceHasTerm(source: string, term: string, neverTranslate?: readonly string[]): boolean;
25
+ /**
26
+ * BCP-47 casing for a locale code: `pt_br` / `PT-br` → `pt-BR`,
27
+ * `zh-hant` → `zh-Hant`, `es-419` stays. Anything that isn't
28
+ * language[-Script][-REGION] is returned trimmed, otherwise unchanged.
29
+ */
30
+ export declare function canonicalLocale(code: string): string;
31
+ /** Keys of a locale → value record in canonical form; on a clash the first key (in insertion order) wins. */
32
+ export declare function canonicalizeLocaleKeys(record: Record<string, string> | null | undefined): Record<string, string>;
33
+ /**
34
+ * The value for `locale` in a locale-keyed record: the exact locale (in any
35
+ * casing / separator), else its base language (`pt` serves `pt-BR`).
36
+ */
37
+ export declare function localeValue(record: Record<string, string> | null | undefined, locale: string): string | undefined;
38
+ export interface GlossaryRuleInput {
39
+ term: string;
40
+ type: GlossaryEntryType | string;
41
+ translations?: Record<string, string> | null;
42
+ }
43
+ /** A project's glossary after normalization: deduplicated, deterministic, conflicts resolved. */
44
+ export interface GlossaryRules {
45
+ neverTranslate: string[];
46
+ alwaysTranslateAs: AlwaysTranslateAsEntry[];
47
+ /** Terms that had conflicting rules, with how the conflict was resolved (for QA notes). */
48
+ conflicts: Array<{
49
+ term: string;
50
+ message: string;
51
+ }>;
52
+ }
53
+ export declare const EMPTY_GLOSSARY: GlossaryRules;
54
+ /**
55
+ * Normalize raw glossary entries. Terms match case-insensitively, so entries
56
+ * are grouped by lowercased term:
57
+ * - never-translate wins over always-translate-as for the same term (keeping
58
+ * a term as-is can't corrupt the output; a translation for the wrong sense can);
59
+ * - several always-translate-as entries merge their locales (the first entry,
60
+ * in term order, wins a locale they both define);
61
+ * - locale keys are canonicalized (`pt_br` → `pt-BR`).
62
+ * The result doesn't depend on the input order.
63
+ */
64
+ export declare function normalizeGlossary(entries: GlossaryRuleInput[]): GlossaryRules;
65
+ /** Always-translate-as terms with a translation for `locale` (exact, then base language). */
66
+ export declare function fixedTermsFor(rules: GlossaryRules, locale: string): Array<{
67
+ term: string;
68
+ translation: string;
69
+ }>;
70
+ /** Whether `source` contains an always-translate-as term that has a translation for `locale`. */
71
+ export declare function hasFixedTerm(source: string, rules: GlossaryRules, locale: string): boolean;
72
+ /**
73
+ * Fingerprint of the glossary rules that shape `source`'s translation into
74
+ * `locale`: the never-translate terms it contains and the always-translate-as
75
+ * terms it contains with their translation for the locale. Empty when no rule
76
+ * applies, so adding a rule for an unrelated term changes nothing. Stored per
77
+ * locale in the lockfile: a key is re-translated when this changes.
78
+ */
79
+ export declare function glossaryFingerprint(source: string, locale: string, rules: GlossaryRules): string;
80
+ /** QA notes for conflicting rules whose term occurs in `source`. */
81
+ export declare function conflictNotes(source: string, rules: GlossaryRules): string[];
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EMPTY_GLOSSARY = void 0;
4
+ exports.termPattern = termPattern;
5
+ exports.containsTerm = containsTerm;
6
+ exports.translatableText = translatableText;
7
+ exports.sourceHasTerm = sourceHasTerm;
8
+ exports.canonicalLocale = canonicalLocale;
9
+ exports.canonicalizeLocaleKeys = canonicalizeLocaleKeys;
10
+ exports.localeValue = localeValue;
11
+ exports.normalizeGlossary = normalizeGlossary;
12
+ exports.fixedTermsFor = fixedTermsFor;
13
+ exports.hasFixedTerm = hasFixedTerm;
14
+ exports.glossaryFingerprint = glossaryFingerprint;
15
+ exports.conflictNotes = conflictNotes;
16
+ /**
17
+ * Project glossary rules, shared by the translate phase, locale QA, prompt
18
+ * hints and the CLI/MCP. One place decides how a term matches, which
19
+ * translation applies to a locale, and what happens when rules conflict.
20
+ */
21
+ const crypto_1 = require("crypto");
22
+ const placeholder_protection_1 = require("../../utils/placeholder-protection");
23
+ const icu_message_1 = require("../../utils/icu-message");
24
+ function escapeRegex(s) {
25
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
+ }
27
+ /**
28
+ * Regex source matching `term` as a whole term: not preceded or followed by a
29
+ * letter, digit or underscore in any script ("Café" matches in "Café menu",
30
+ * "app" does not match in "apply" or "appé"). Use with the `u` flag.
31
+ */
32
+ function termPattern(term) {
33
+ return `(?<![\\p{L}\\p{N}_])${escapeRegex(term)}(?![\\p{L}\\p{N}_])`;
34
+ }
35
+ const termRegexes = new Map();
36
+ /** Whether `text` contains `term` as a whole term, ignoring case. */
37
+ function containsTerm(text, term) {
38
+ if (!term.trim())
39
+ return false;
40
+ // Cheap pre-check: most (key, term) pairs share nothing.
41
+ if (!text.toLowerCase().includes(term.toLowerCase())) {
42
+ // toLowerCase can change length for a few letters (İ); fall through only then.
43
+ if (text.length === text.toLowerCase().length && term.length === term.toLowerCase().length)
44
+ return false;
45
+ }
46
+ let re = termRegexes.get(term);
47
+ if (!re) {
48
+ if (termRegexes.size > 10000)
49
+ termRegexes.clear();
50
+ re = new RegExp(termPattern(term), 'iu');
51
+ termRegexes.set(term, re);
52
+ }
53
+ return re.test(text);
54
+ }
55
+ const NO_TERMS = [];
56
+ /** Per never-translate list (by identity), source → translatable text. */
57
+ const translatableCache = new WeakMap();
58
+ /**
59
+ * The words of a source string a glossary rule can apply to: placeholders,
60
+ * tags, URLs, emails and code removed, and for an ICU message its argument
61
+ * names and selector keys too. "Open https://x.com/dashboard" has no
62
+ * "dashboard" a translator would translate. With `neverTranslate`, those
63
+ * terms are removed too: "Dashboard" in "Babelize Dashboard" (never
64
+ * translated) is not a use of a "Dashboard" rule.
65
+ */
66
+ function translatableText(source, neverTranslate = NO_TERMS) {
67
+ let cache = translatableCache.get(neverTranslate);
68
+ if (!cache) {
69
+ cache = new Map();
70
+ translatableCache.set(neverTranslate, cache);
71
+ }
72
+ const hit = cache.get(source);
73
+ if (hit !== undefined)
74
+ return hit;
75
+ let text = (0, placeholder_protection_1.protectText)(source, [...neverTranslate]).text.replace(/\[#VAR_\d+#\]/g, ' ');
76
+ if ((0, icu_message_1.isIcuMessage)(source)) {
77
+ text = text
78
+ .replace(/\{\s*[\w.]+\s*,\s*(?:plural|select|selectordinal)\s*,(?:\s*offset\s*:\s*\d+)?/g, ' ')
79
+ .replace(/(?:=\d+|[\p{L}\p{N}_]+)\s*\{/gu, ' ');
80
+ }
81
+ if (cache.size > 20000)
82
+ cache.clear();
83
+ cache.set(source, text);
84
+ return text;
85
+ }
86
+ /**
87
+ * Whether a glossary term occurs in a source string's translatable text (see
88
+ * translatableText). Pass the never-translate terms when asking about an
89
+ * "always translate as" term: inside a never-translate term it doesn't count.
90
+ */
91
+ function sourceHasTerm(source, term, neverTranslate = NO_TERMS) {
92
+ return containsTerm(translatableText(source, neverTranslate), term);
93
+ }
94
+ /**
95
+ * BCP-47 casing for a locale code: `pt_br` / `PT-br` → `pt-BR`,
96
+ * `zh-hant` → `zh-Hant`, `es-419` stays. Anything that isn't
97
+ * language[-Script][-REGION] is returned trimmed, otherwise unchanged.
98
+ */
99
+ function canonicalLocale(code) {
100
+ const parts = code.trim().replace(/_/g, '-').split('-');
101
+ if (!/^[a-z]{2,3}$/i.test(parts[0] ?? ''))
102
+ return code.trim();
103
+ const out = [parts[0].toLowerCase()];
104
+ for (let i = 1; i < parts.length; i++) {
105
+ const p = parts[i];
106
+ if (/^[a-z]{4}$/i.test(p))
107
+ out.push(p.charAt(0).toUpperCase() + p.slice(1).toLowerCase());
108
+ else if (/^[a-z]{2}$/i.test(p))
109
+ out.push(p.toUpperCase());
110
+ else if (/^\d{3}$/.test(p))
111
+ out.push(p);
112
+ else
113
+ return code.trim();
114
+ }
115
+ return out.join('-');
116
+ }
117
+ /** Keys of a locale → value record in canonical form; on a clash the first key (in insertion order) wins. */
118
+ function canonicalizeLocaleKeys(record) {
119
+ const out = {};
120
+ for (const [k, v] of Object.entries(record ?? {})) {
121
+ if (typeof v !== 'string' || !v.trim())
122
+ continue;
123
+ const key = canonicalLocale(k);
124
+ if (!(key in out))
125
+ out[key] = v;
126
+ }
127
+ return out;
128
+ }
129
+ /**
130
+ * The value for `locale` in a locale-keyed record: the exact locale (in any
131
+ * casing / separator), else its base language (`pt` serves `pt-BR`).
132
+ */
133
+ function localeValue(record, locale) {
134
+ if (!record || !locale)
135
+ return undefined;
136
+ const want = canonicalLocale(locale);
137
+ let base;
138
+ const baseLang = want.split('-')[0];
139
+ for (const [k, v] of Object.entries(record)) {
140
+ if (typeof v !== 'string' || !v.trim())
141
+ continue;
142
+ const key = canonicalLocale(k);
143
+ if (key === want)
144
+ return v;
145
+ if (key === baseLang && base === undefined)
146
+ base = v;
147
+ }
148
+ return base;
149
+ }
150
+ exports.EMPTY_GLOSSARY = { neverTranslate: [], alwaysTranslateAs: [], conflicts: [] };
151
+ /**
152
+ * Normalize raw glossary entries. Terms match case-insensitively, so entries
153
+ * are grouped by lowercased term:
154
+ * - never-translate wins over always-translate-as for the same term (keeping
155
+ * a term as-is can't corrupt the output; a translation for the wrong sense can);
156
+ * - several always-translate-as entries merge their locales (the first entry,
157
+ * in term order, wins a locale they both define);
158
+ * - locale keys are canonicalized (`pt_br` → `pt-BR`).
159
+ * The result doesn't depend on the input order.
160
+ */
161
+ function normalizeGlossary(entries) {
162
+ const groups = new Map();
163
+ for (const e of entries) {
164
+ if (!e || typeof e.term !== 'string')
165
+ continue;
166
+ const term = e.term.trim();
167
+ if (!term)
168
+ continue;
169
+ if (e.type !== 'never_translate' && e.type !== 'always_translate_as')
170
+ continue;
171
+ const k = term.toLowerCase();
172
+ const list = groups.get(k) ?? [];
173
+ list.push({ ...e, term });
174
+ groups.set(k, list);
175
+ }
176
+ const neverTranslate = [];
177
+ const alwaysTranslateAs = [];
178
+ const conflicts = [];
179
+ const byTerm = (a, b) => (a.term < b.term ? -1 : a.term > b.term ? 1 : 0);
180
+ for (const k of [...groups.keys()].sort()) {
181
+ const list = groups.get(k).slice().sort(byTerm);
182
+ const never = list.filter((e) => e.type === 'never_translate');
183
+ const always = list.filter((e) => e.type === 'always_translate_as');
184
+ if (never.length > 0) {
185
+ neverTranslate.push(never[0].term);
186
+ if (always.length > 0) {
187
+ const names = [...new Set(list.map((e) => `"${e.term}"`))].join(' / ');
188
+ conflicts.push({
189
+ term: never[0].term,
190
+ message: `Glossary: ${names} is both "never translate" and "always translate as" (terms match ignoring case); "never translate" was applied`,
191
+ });
192
+ }
193
+ continue;
194
+ }
195
+ const translations = {};
196
+ for (const e of always) {
197
+ for (const [loc, v] of Object.entries(canonicalizeLocaleKeys(e.translations))) {
198
+ if (!(loc in translations))
199
+ translations[loc] = v.trim();
200
+ }
201
+ }
202
+ alwaysTranslateAs.push({ term: always[0].term, type: 'always_translate_as', translations });
203
+ }
204
+ return { neverTranslate, alwaysTranslateAs, conflicts };
205
+ }
206
+ /** Always-translate-as terms with a translation for `locale` (exact, then base language). */
207
+ function fixedTermsFor(rules, locale) {
208
+ const out = [];
209
+ for (const e of rules.alwaysTranslateAs) {
210
+ const translation = localeValue(e.translations, locale);
211
+ if (translation)
212
+ out.push({ term: e.term, translation });
213
+ }
214
+ return out;
215
+ }
216
+ /** Whether `source` contains an always-translate-as term that has a translation for `locale`. */
217
+ function hasFixedTerm(source, rules, locale) {
218
+ return fixedTermsFor(rules, locale).some((g) => sourceHasTerm(source, g.term, rules.neverTranslate));
219
+ }
220
+ /**
221
+ * Fingerprint of the glossary rules that shape `source`'s translation into
222
+ * `locale`: the never-translate terms it contains and the always-translate-as
223
+ * terms it contains with their translation for the locale. Empty when no rule
224
+ * applies, so adding a rule for an unrelated term changes nothing. Stored per
225
+ * locale in the lockfile: a key is re-translated when this changes.
226
+ */
227
+ function glossaryFingerprint(source, locale, rules) {
228
+ const parts = [];
229
+ for (const t of rules.neverTranslate) {
230
+ if (sourceHasTerm(source, t))
231
+ parts.push(`n\u0000${t.toLowerCase()}`);
232
+ }
233
+ for (const g of fixedTermsFor(rules, locale)) {
234
+ if (sourceHasTerm(source, g.term, rules.neverTranslate))
235
+ parts.push(`a\u0000${g.term.toLowerCase()}\u0000${g.translation}`);
236
+ }
237
+ if (parts.length === 0)
238
+ return '';
239
+ return (0, crypto_1.createHash)('sha256').update(parts.sort().join('\u0001'), 'utf8').digest('hex').slice(0, 16);
240
+ }
241
+ /** QA notes for conflicting rules whose term occurs in `source`. */
242
+ function conflictNotes(source, rules) {
243
+ return rules.conflicts.filter((c) => sourceHasTerm(source, c.term)).map((c) => c.message);
244
+ }
@@ -43,8 +43,7 @@ const StrategyFactory_1 = require("../../injection/StrategyFactory");
43
43
  const jsx_message_1 = require("../../utils/jsx-message");
44
44
  const module_constants_1 = require("../../utils/module-constants");
45
45
  const html_injector_1 = require("../../injection/html/html-injector");
46
- const django_template_injector_1 = require("../../injection/django/django-template-injector");
47
- const dtl_mask_1 = require("../../utils/dtl-mask");
46
+ const inject_file_1 = require("../../django/inject-file");
48
47
  const types_2 = require("../../runtime/types");
49
48
  const file_utils_1 = require("../../utils/file-utils");
50
49
  const string_utils_1 = require("../../utils/string-utils");
@@ -296,34 +295,19 @@ positions) {
296
295
  try {
297
296
  const originalContent = await (0, file_utils_1.readFile)(filePath);
298
297
  let result;
299
- if (relativePath.endsWith('.html') || relativePath.endsWith('.htm')) {
300
- if (workspace.framework === 'django') {
301
- // Django: mask tags, run HTML injection, convert data-i18n to {% trans %}
302
- const { masked, restore } = (0, dtl_mask_1.maskTemplateTags)(originalContent);
303
- const maskedResult = await (0, html_injector_1.injectHtml)({
304
- content: masked,
305
- filePath: relativePath,
306
- keyMapping: fileKeys,
307
- injectSwitcher: false, // Django doesn't use JS switcher
308
- });
309
- result = await (0, django_template_injector_1.injectDjangoTemplates)({
310
- maskedContent: maskedResult.modifiedContent,
311
- restore,
312
- filePath: relativePath,
313
- keyMapping: fileKeys,
314
- sourceContent: originalContent,
315
- });
316
- }
317
- else {
318
- result = await (0, html_injector_1.injectHtml)({
319
- content: originalContent,
320
- filePath: relativePath,
321
- keyMapping: fileKeys,
322
- injectSwitcher: workspace.framework === 'static',
323
- targetLocales: workspace.existingLocales,
324
- sourceLocale: workspace.sourceDirs[0],
325
- });
326
- }
298
+ if (workspace.framework === 'django') {
299
+ // Django: wrap hardcoded strings with gettext / {% translate %} in place.
300
+ result = (0, inject_file_1.injectDjangoFile)(originalContent, relativePath, fileKeys);
301
+ }
302
+ else if (relativePath.endsWith('.html') || relativePath.endsWith('.htm')) {
303
+ result = await (0, html_injector_1.injectHtml)({
304
+ content: originalContent,
305
+ filePath: relativePath,
306
+ keyMapping: fileKeys,
307
+ injectSwitcher: workspace.framework === 'static',
308
+ targetLocales: workspace.existingLocales,
309
+ sourceLocale: workspace.sourceDirs[0],
310
+ });
327
311
  }
328
312
  else if (relativePath.endsWith('.vue')) {
329
313
  const existingDeps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateKeys = generateKeys;
4
4
  const string_utils_1 = require("../../utils/string-utils");
5
5
  const jsx_message_1 = require("../../utils/jsx-message");
6
+ const keys_1 = require("../../django/keys");
6
7
  function generateKeys(extractedStrings, existingKeys = {}) {
7
8
  const keys = [];
8
9
  const duplicates = [];
@@ -35,6 +36,12 @@ function generateKeys(extractedStrings, existingKeys = {}) {
35
36
  // The translation engine will only generate entries for categories the target locale needs.
36
37
  const pluralCategories = ['zero', 'one', 'two', 'few', 'many', 'other'];
37
38
  for (const extracted of extractedStrings) {
39
+ // gettext messages (Django) are looked up by their text; their keys are
40
+ // derived from the message, so they are stable without a source file.
41
+ if (extracted.gettext) {
42
+ keys.push(...(0, keys_1.gettextKeys)(extracted));
43
+ continue;
44
+ }
38
45
  const namespace = (0, string_utils_1.inferNamespace)(extracted.file);
39
46
  // A sentence message is named from its words: "Welcome back, {user}" → welcomeBack.
40
47
  const truncatedText = (0, string_utils_1.truncateForKey)(extracted.icuMessage ? (0, jsx_message_1.messageKeyText)(extracted.text) : extracted.text, 5);
@@ -16,6 +16,8 @@ function assertSafeLocale(locale) {
16
16
  }
17
17
  async function handleBaseLocale(workspace, generatedKeys, sourceLocale = 'en', dryRun = false) {
18
18
  assertSafeLocale(sourceLocale);
19
+ if (workspace.localeFormat === 'po')
20
+ return gettextSourceLocale(generatedKeys, sourceLocale);
19
21
  const localePaths = (0, file_utils_1.localeFilePath)(workspace.path, workspace.publicDir, workspace.localeFormat, sourceLocale);
20
22
  const localesDir = localePaths.dir;
21
23
  const localeFilePathTarget = localePaths.file;
@@ -71,6 +73,24 @@ async function handleBaseLocale(workspace, generatedKeys, sourceLocale = 'en', d
71
73
  },
72
74
  };
73
75
  }
76
+ /**
77
+ * gettext (Django): the source language has no catalogue file — the msgids
78
+ * in the code are the source text. The run's keys are the source locale.
79
+ */
80
+ function gettextSourceLocale(generatedKeys, sourceLocale) {
81
+ const data = {};
82
+ for (const k of generatedKeys)
83
+ data[k.key] = k.value;
84
+ const sourceLocaleFile = {
85
+ locale: sourceLocale,
86
+ path: '',
87
+ data,
88
+ isNew: false,
89
+ isRTL: (0, language_codes_1.isRTL)(sourceLocale),
90
+ noFile: true,
91
+ };
92
+ return { localeHandling: { sourceLocale: sourceLocaleFile, newKeysCount: 0, existingKeysCount: Object.keys(data).length } };
93
+ }
74
94
  function countKeys(data) {
75
95
  let count = 0;
76
96
  function traverse(obj) {
@@ -86,6 +106,9 @@ function countKeys(data) {
86
106
  }
87
107
  async function readLocaleFile(workspace, locale) {
88
108
  assertSafeLocale(locale);
109
+ // gettext projects have no source-locale file: the code is the catalogue.
110
+ if (workspace.localeFormat === 'po')
111
+ return null;
89
112
  if (workspace.localeFormat !== 'json') {
90
113
  throw new Error(`Locale format '${workspace.localeFormat}' reading not yet implemented`);
91
114
  }
@@ -1,3 +1,3 @@
1
1
  export { hashSourceString } from './hasher';
2
2
  export { type LockfileData, type LockfileEntry, type LockfilePartition, type LockfileStats, type PartitionedKey, createEmptyLockfile, } from './schema';
3
- export { buildKeysToTranslateByTargetLocale, clearLockfile, removeLocaleFromLockfile, collectCachedTranslations, computeLockfileStats, getLockfilePath, loadLockfile, partitionKeys, saveLockfile, updateLockfile, lockfileChanged, dropFailedTranslations, isStalePseudoEntry, type LockfileReadOptions, } from './lockfile-manager';
3
+ export { buildKeysToTranslateByTargetLocale, clearLockfile, removeLocaleFromLockfile, collectCachedTranslations, computeLockfileStats, getLockfilePath, loadLockfile, partitionKeys, saveLockfile, updateLockfile, lockfileChanged, dropFailedTranslations, isStalePseudoEntry, glossaryChanged, type LockfileReadOptions, } from './lockfile-manager';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isStalePseudoEntry = exports.dropFailedTranslations = exports.lockfileChanged = exports.updateLockfile = exports.saveLockfile = exports.partitionKeys = exports.loadLockfile = exports.getLockfilePath = exports.computeLockfileStats = exports.collectCachedTranslations = exports.removeLocaleFromLockfile = exports.clearLockfile = exports.buildKeysToTranslateByTargetLocale = exports.createEmptyLockfile = exports.hashSourceString = void 0;
3
+ exports.glossaryChanged = exports.isStalePseudoEntry = exports.dropFailedTranslations = exports.lockfileChanged = exports.updateLockfile = exports.saveLockfile = exports.partitionKeys = exports.loadLockfile = exports.getLockfilePath = exports.computeLockfileStats = exports.collectCachedTranslations = exports.removeLocaleFromLockfile = exports.clearLockfile = exports.buildKeysToTranslateByTargetLocale = exports.createEmptyLockfile = exports.hashSourceString = void 0;
4
4
  var hasher_1 = require("./hasher");
5
5
  Object.defineProperty(exports, "hashSourceString", { enumerable: true, get: function () { return hasher_1.hashSourceString; } });
6
6
  var schema_1 = require("./schema");
@@ -19,3 +19,4 @@ Object.defineProperty(exports, "updateLockfile", { enumerable: true, get: functi
19
19
  Object.defineProperty(exports, "lockfileChanged", { enumerable: true, get: function () { return lockfile_manager_1.lockfileChanged; } });
20
20
  Object.defineProperty(exports, "dropFailedTranslations", { enumerable: true, get: function () { return lockfile_manager_1.dropFailedTranslations; } });
21
21
  Object.defineProperty(exports, "isStalePseudoEntry", { enumerable: true, get: function () { return lockfile_manager_1.isStalePseudoEntry; } });
22
+ Object.defineProperty(exports, "glossaryChanged", { enumerable: true, get: function () { return lockfile_manager_1.glossaryChanged; } });
@@ -45,7 +45,16 @@ export declare function removeLocaleFromLockfile(projectId: string, localeCode:
45
45
  export interface LockfileReadOptions {
46
46
  /** This run is pseudo-localization: pseudo entries count as done. Otherwise they count as missing. */
47
47
  pseudo?: boolean;
48
+ /**
49
+ * The glossary fingerprint this run's rules give a key's source for a
50
+ * locale ('' when no rule applies). A stored translation made under a
51
+ * different fingerprint counts as missing, so glossary edits reach the
52
+ * keys they affect — and only those. Omit when the rules couldn't be loaded.
53
+ */
54
+ glossaryHash?: (source: string, locale: string) => string;
48
55
  }
56
+ /** True when the glossary rules for this key and locale changed since its translation was stored. */
57
+ export declare function glossaryChanged(entry: LockfileEntry, source: string, locale: string, options?: LockfileReadOptions): boolean;
49
58
  /**
50
59
  * True when the stored value for `locale` is pseudo-localization and this run
51
60
  * is a real one: it must be translated, never reused. Recognised by the
@@ -39,6 +39,7 @@ exports.loadLockfile = loadLockfile;
39
39
  exports.saveLockfile = saveLockfile;
40
40
  exports.clearLockfile = clearLockfile;
41
41
  exports.removeLocaleFromLockfile = removeLocaleFromLockfile;
42
+ exports.glossaryChanged = glossaryChanged;
42
43
  exports.isStalePseudoEntry = isStalePseudoEntry;
43
44
  exports.partitionKeys = partitionKeys;
44
45
  exports.dropFailedTranslations = dropFailedTranslations;
@@ -291,6 +292,15 @@ async function removeLocaleFromLockfile(projectId, localeCode, dataDir) {
291
292
  });
292
293
  }
293
294
  }
295
+ /** True when the glossary rules for this key and locale changed since its translation was stored. */
296
+ function glossaryChanged(entry, source, locale, options = {}) {
297
+ if (!options.glossaryHash)
298
+ return false;
299
+ const meta = entry.metadata?.[locale];
300
+ if (meta?.external)
301
+ return false;
302
+ return (meta?.glossaryHash ?? '') !== options.glossaryHash(source, locale);
303
+ }
294
304
  /**
295
305
  * True when the stored value for `locale` is pseudo-localization and this run
296
306
  * is a real one: it must be translated, never reused. Recognised by the
@@ -323,7 +333,8 @@ function partitionKeys(lockfile, keys, targetLocales, options = {}) {
323
333
  continue;
324
334
  }
325
335
  // Hash matches — check if all target locales are present
326
- const missing = targetLocales.filter((locale) => !(locale in entry.languages) || isStalePseudoEntry(entry, locale, options));
336
+ const missing = targetLocales.filter((locale) => !(locale in entry.languages) || isStalePseudoEntry(entry, locale, options) ||
337
+ glossaryChanged(entry, value, locale, options));
327
338
  if (missing.length === 0) {
328
339
  // Fully translated and unchanged
329
340
  result.unchanged.push({ key, value });
@@ -379,6 +390,10 @@ function pickLockfileMetadata(meta) {
379
390
  out.sourceHash = meta.sourceHash;
380
391
  if (typeof meta.inTm === 'boolean')
381
392
  out.inTm = meta.inTm;
393
+ if (typeof meta.glossaryHash === 'string' && meta.glossaryHash)
394
+ out.glossaryHash = meta.glossaryHash;
395
+ if (meta.external === true)
396
+ out.external = true;
382
397
  return out;
383
398
  }
384
399
  function sameJson(a, b) {