@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
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export {
|
|
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.
|
|
4
|
-
var
|
|
5
|
-
Object.defineProperty(exports, "
|
|
6
|
-
Object.defineProperty(exports, "
|
|
7
|
-
|
|
8
|
-
Object.defineProperty(exports, "
|
|
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
|
|
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 (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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) {
|