@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
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Entry point for extracting a Django project file into engine strings.
3
+ */
4
+ import type { ExtractedString, NotInjectedString, Warning } from '../runtime/types';
5
+ import type { DjangoFileAnalysis, TemplateEdit } from './types';
6
+ /** A Django template: an .html/.txt file inside a `templates` directory (not Jinja2). */
7
+ export declare function isDjangoTemplatePath(rel: string): boolean;
8
+ export declare function isDjangoPythonSource(rel: string): boolean;
9
+ export declare function analyzeDjangoFile(content: string, rel: string): (DjangoFileAnalysis & {
10
+ extraEdits: TemplateEdit[];
11
+ }) | null;
12
+ /** Extraction id of a finding; injection matches findings back by it. */
13
+ export declare function findingId(rel: string, line: number, msgctxt: string | undefined, msgid: string, plural: string | undefined, editStart?: number): string;
14
+ export declare function extractDjangoFile(content: string, rel: string, options?: {
15
+ icu?: boolean;
16
+ }): {
17
+ strings: ExtractedString[];
18
+ warnings: Warning[];
19
+ notInjected: NotInjectedString[];
20
+ };
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Entry point for extracting a Django project file into engine strings.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isDjangoTemplatePath = isDjangoTemplatePath;
7
+ exports.isDjangoPythonSource = isDjangoPythonSource;
8
+ exports.analyzeDjangoFile = analyzeDjangoFile;
9
+ exports.findingId = findingId;
10
+ exports.extractDjangoFile = extractDjangoFile;
11
+ const crypto_1 = require("crypto");
12
+ const python_extract_1 = require("./python-extract");
13
+ const template_extract_1 = require("./template-extract");
14
+ /** Python files that are not application code shown to users. */
15
+ const NON_APP_PY = /(?:^|\/)(?:manage|setup|conftest|wsgi|asgi|settings|tests|test_[^/]*|[^/]*_tests?)\.py$|(?:^|\/)settings\/[^/]+\.py$/;
16
+ /** A Django template: an .html/.txt file inside a `templates` directory (not Jinja2). */
17
+ function isDjangoTemplatePath(rel) {
18
+ const p = rel.replace(/\\/g, '/');
19
+ return /\.html?$/i.test(p) && /(?:^|\/)templates\//.test(p) && !/(?:^|\/)jinja2\//.test(p);
20
+ }
21
+ function isDjangoPythonSource(rel) {
22
+ const p = rel.replace(/\\/g, '/');
23
+ return p.endsWith('.py') && !NON_APP_PY.test(p) && !/(?:^|\/)migrations\//.test(p);
24
+ }
25
+ function analyzeDjangoFile(content, rel) {
26
+ if (isDjangoPythonSource(rel))
27
+ return { ...(0, python_extract_1.extractDjangoPython)(content, { filePath: rel }), extraEdits: [] };
28
+ if (isDjangoTemplatePath(rel))
29
+ return (0, template_extract_1.extractDjangoTemplate)(content, { filePath: rel });
30
+ return null;
31
+ }
32
+ function messageHash(msgctxt, msgid, plural) {
33
+ return (0, crypto_1.createHash)('sha1').update(`${msgctxt ?? ''}\u0004${msgid}\u0000${plural ?? ''}`).digest('hex').slice(0, 12);
34
+ }
35
+ /** Extraction id of a finding; injection matches findings back by it. */
36
+ function findingId(rel, line, msgctxt, msgid, plural, editStart) {
37
+ const base = `${rel}:${line}:${messageHash(msgctxt, msgid, plural)}`;
38
+ return editStart === undefined ? base : `${base}:${editStart}`;
39
+ }
40
+ function extractDjangoFile(content, rel, options = {}) {
41
+ const analysis = analyzeDjangoFile(content, rel);
42
+ if (!analysis)
43
+ return { strings: [], warnings: [], notInjected: [] };
44
+ const strings = [];
45
+ const seen = new Set();
46
+ for (const f of analysis.findings) {
47
+ const m = f.message;
48
+ const id = findingId(rel, f.line, m.msgctxt, m.msgid, m.msgidPlural, f.edit?.start);
49
+ if (seen.has(id))
50
+ continue;
51
+ seen.add(id);
52
+ strings.push({
53
+ text: m.msgid,
54
+ file: rel,
55
+ line: f.line,
56
+ column: 0,
57
+ context: rel.endsWith('.py') ? 'string-literal' : 'jsx-text',
58
+ id,
59
+ parentContext: m.msgctxt,
60
+ offsets: f.edit ? [f.edit.start] : undefined,
61
+ gettext: {
62
+ msgid: m.msgid,
63
+ msgidPlural: m.msgidPlural,
64
+ msgctxt: m.msgctxt,
65
+ flags: m.flags,
66
+ comments: m.comments,
67
+ wrapped: f.wrapped,
68
+ icu: options.icu,
69
+ },
70
+ });
71
+ }
72
+ const notInjected = analysis.unwired.map((u) => ({
73
+ file: rel,
74
+ key: '',
75
+ text: u.text,
76
+ reason: `line ${u.line}: ${u.reason}`,
77
+ }));
78
+ return { strings, warnings: [], notInjected };
79
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Injection phase for one Django file: re-analyze it, wrap the hardcoded
3
+ * strings extraction selected (by extraction id), and report each wrap.
4
+ */
5
+ import type { FileInjection } from '../runtime/types';
6
+ export declare function injectDjangoFile(content: string, rel: string, fileKeys: Map<string, string>): FileInjection;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * Injection phase for one Django file: re-analyze it, wrap the hardcoded
4
+ * strings extraction selected (by extraction id), and report each wrap.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.injectDjangoFile = injectDjangoFile;
8
+ const extract_1 = require("./extract");
9
+ const inject_1 = require("./inject");
10
+ function injectDjangoFile(content, rel, fileKeys) {
11
+ const unchanged = { file: rel, originalContent: content, modifiedContent: content, injections: [], importAdded: false, hookAdded: false };
12
+ const analysis = (0, extract_1.analyzeDjangoFile)(content, rel);
13
+ if (!analysis)
14
+ return unchanged;
15
+ const injections = [];
16
+ const pyEdits = [];
17
+ const tplEdits = [];
18
+ for (const f of analysis.findings) {
19
+ if (!f.edit)
20
+ continue;
21
+ const m = f.message;
22
+ const key = fileKeys.get((0, extract_1.findingId)(rel, f.line, m.msgctxt, m.msgid, m.msgidPlural, f.edit.start));
23
+ if (!key)
24
+ continue;
25
+ if (f.edit.kind === 'python')
26
+ pyEdits.push(f.edit);
27
+ else
28
+ tplEdits.push(f.edit);
29
+ injections.push({
30
+ file: rel,
31
+ line: f.line,
32
+ column: 0,
33
+ original: content.slice(f.edit.start, f.edit.end),
34
+ replacement: f.edit.kind === 'template' ? f.edit.replacement : `gettext(${f.edit.inner})${f.edit.suffix}`,
35
+ key,
36
+ confidence: 100,
37
+ });
38
+ }
39
+ if (injections.length === 0)
40
+ return unchanged;
41
+ const modifiedContent = rel.endsWith('.py')
42
+ ? (0, inject_1.injectPython)(content, pyEdits)
43
+ : (0, inject_1.injectTemplate)(content, [...tplEdits, ...analysis.extraEdits]);
44
+ return {
45
+ file: rel,
46
+ originalContent: content,
47
+ modifiedContent,
48
+ injections,
49
+ importAdded: modifiedContent !== content,
50
+ hookAdded: false,
51
+ };
52
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Apply the edits found by the Django extractors: wrap hardcoded strings in
3
+ * gettext / gettext_lazy (adding the import) or translate tags (adding
4
+ * `{% load i18n %}`). Edits replace exact source spans, so the rest of the
5
+ * file — formatting, comments, line endings — is unchanged.
6
+ */
7
+ import type { PythonEdit, TemplateEdit } from './types';
8
+ /** Insert `{% load i18n %}` after `{% extends %}` (which must stay first), or at the top. */
9
+ export declare function ensureLoadI18n(src: string): string;
10
+ export declare function injectTemplate(src: string, edits: TemplateEdit[]): string;
11
+ export declare function injectPython(src: string, edits: PythonEdit[]): string;
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ /**
3
+ * Apply the edits found by the Django extractors: wrap hardcoded strings in
4
+ * gettext / gettext_lazy (adding the import) or translate tags (adding
5
+ * `{% load i18n %}`). Edits replace exact source spans, so the rest of the
6
+ * file — formatting, comments, line endings — is unchanged.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ensureLoadI18n = ensureLoadI18n;
10
+ exports.injectTemplate = injectTemplate;
11
+ exports.injectPython = injectPython;
12
+ const python_lexer_1 = require("./python-lexer");
13
+ function applySpans(src, edits) {
14
+ // Non-overlapping, applied back to front so earlier offsets stay valid.
15
+ const sorted = [...edits].sort((a, b) => b.start - a.start || b.end - a.end);
16
+ let out = src;
17
+ let limit = Infinity;
18
+ for (const e of sorted) {
19
+ if (e.end > limit)
20
+ continue; // overlaps an edit already applied
21
+ out = out.slice(0, e.start) + e.text + out.slice(e.end);
22
+ limit = e.start;
23
+ }
24
+ return out;
25
+ }
26
+ function newlineOf(src) {
27
+ return src.includes('\r\n') ? '\r\n' : '\n';
28
+ }
29
+ // ── Templates ────────────────────────────────────────────────
30
+ const LOAD_I18N = /\{%\s*load\s+(?:[\w.]+\s+)*i18n(?:\s+[\w.]+)*\s*%\}/;
31
+ /** Insert `{% load i18n %}` after `{% extends %}` (which must stay first), or at the top. */
32
+ function ensureLoadI18n(src) {
33
+ if (LOAD_I18N.test(src))
34
+ return src;
35
+ const nl = newlineOf(src);
36
+ const ext = /\{%\s*extends\s[^%]*%\}[ \t]*(\r?\n)?/.exec(src);
37
+ if (ext) {
38
+ const at = ext.index + ext[0].length;
39
+ return ext[1] ? `${src.slice(0, at)}{% load i18n %}${nl}${src.slice(at)}` : `${src.slice(0, at)}${nl}{% load i18n %}${nl}${src.slice(at)}`;
40
+ }
41
+ return `{% load i18n %}${nl}${src}`;
42
+ }
43
+ function injectTemplate(src, edits) {
44
+ if (edits.length === 0)
45
+ return src;
46
+ const out = applySpans(src, edits.map((e) => ({ start: e.start, end: e.end, text: e.replacement })));
47
+ return ensureLoadI18n(out);
48
+ }
49
+ function existingImports(src, tokens) {
50
+ const local = new Map();
51
+ let extendable;
52
+ for (const ln of (0, python_lexer_1.logicalLines)(src, tokens)) {
53
+ const t = ln.tokens;
54
+ if (ln.indent !== 0 || t[0]?.value !== 'from')
55
+ continue;
56
+ const mod = t.slice(1, t.findIndex((x) => x.value === 'import')).map((x) => x.value).join('');
57
+ if (mod !== 'django.utils.translation')
58
+ continue;
59
+ const importAt = t.findIndex((x) => x.value === 'import');
60
+ const names = t.slice(importAt + 1).filter((x) => x.value !== '(' && x.value !== ')');
61
+ for (let k = 0; k < names.length; k++) {
62
+ if (names[k].type !== 'name')
63
+ continue;
64
+ if (names[k + 1]?.value === 'as') {
65
+ local.set(names[k + 2].value, names[k].value);
66
+ k += 2;
67
+ }
68
+ else {
69
+ local.set(names[k].value, names[k].value);
70
+ }
71
+ }
72
+ const last = t[t.length - 1];
73
+ const text = src.slice(t[0].start, last.end);
74
+ if (!text.includes('(') && !text.includes('\n') && !text.includes('\\'))
75
+ extendable = { end: last.end, text };
76
+ }
77
+ return { local, extendable };
78
+ }
79
+ /** Is `_` taken by something other than Django's gettext in this file? */
80
+ function underscoreTaken(tokens, imports) {
81
+ if (imports.local.has('_'))
82
+ return false;
83
+ for (let i = 0; i < tokens.length; i++) {
84
+ const t = tokens[i];
85
+ if (t.type !== 'name' || t.value !== '_')
86
+ continue;
87
+ if (tokens[i + 1]?.value === '(') {
88
+ // `def _(` defines it; `_(` alone is a call to something already named `_`.
89
+ if (tokens[i - 1]?.value === 'def')
90
+ return true;
91
+ return true;
92
+ }
93
+ return true; // `for _ in …`, `x, _ = …`, `import foo as _`
94
+ }
95
+ return false;
96
+ }
97
+ /** Where a new import line goes: after the last top-level import of the leading block. */
98
+ function importInsertPoint(src, tokens) {
99
+ const lines = (0, python_lexer_1.logicalLines)(src, tokens);
100
+ let lastImportEnd = -1;
101
+ let lastDjangoImportEnd = -1;
102
+ let afterDocstring = -1;
103
+ for (let idx = 0; idx < lines.length; idx++) {
104
+ const ln = lines[idx];
105
+ if (ln.indent !== 0) {
106
+ if (lastImportEnd !== -1)
107
+ break;
108
+ continue;
109
+ }
110
+ const first = ln.tokens[0];
111
+ if (idx === 0 && first.type === 'string' && ln.tokens.length === 1) {
112
+ afterDocstring = first.end;
113
+ continue;
114
+ }
115
+ if (first.value === 'import' || first.value === 'from') {
116
+ lastImportEnd = ln.tokens[ln.tokens.length - 1].end;
117
+ // Keep the Django imports together (isort's third-party section).
118
+ if (ln.tokens[1]?.value === 'django')
119
+ lastDjangoImportEnd = lastImportEnd;
120
+ continue;
121
+ }
122
+ if (first.value === 'try' || first.value === 'except' || first.value === 'if') {
123
+ // Conditional imports: stop scanning, keep what we have.
124
+ break;
125
+ }
126
+ break;
127
+ }
128
+ const anchor = lastDjangoImportEnd !== -1 ? lastDjangoImportEnd : lastImportEnd !== -1 ? lastImportEnd : afterDocstring;
129
+ if (anchor === -1) {
130
+ // Top of file, after a shebang / encoding comment block.
131
+ let offset = 0;
132
+ const header = /^(?:#![^\n]*\n|#[^\n]*coding[:=][^\n]*\n)+/.exec(src);
133
+ if (header)
134
+ offset = header[0].length;
135
+ return { offset, before: true };
136
+ }
137
+ const eol = src.indexOf('\n', anchor);
138
+ return { offset: eol === -1 ? src.length : eol + 1, before: false };
139
+ }
140
+ function injectPython(src, edits) {
141
+ if (edits.length === 0)
142
+ return src;
143
+ const tokens = (0, python_lexer_1.tokenizePython)(src);
144
+ const imports = existingImports(src, tokens);
145
+ const needLazy = edits.some((e) => e.lazy);
146
+ const needEager = edits.some((e) => !e.lazy);
147
+ const nl = newlineOf(src);
148
+ const findLocal = (imported, alt) => {
149
+ for (const [loc, imp] of imports.local)
150
+ if (imp === imported || imp === alt)
151
+ return loc;
152
+ return undefined;
153
+ };
154
+ let lazyName = needLazy ? findLocal('gettext_lazy', 'ugettext_lazy') : undefined;
155
+ let eagerName = needEager ? findLocal('gettext', 'ugettext') : undefined;
156
+ const toImport = [];
157
+ const underscoreFree = !underscoreTaken(tokens, imports) && !imports.local.has('_');
158
+ const nameInUse = (n) => tokens.some((t) => t.type === 'name' && t.value === n) && !imports.local.has(n);
159
+ // `_` goes to the kind used most, unless the file already uses `_`.
160
+ const lazyCount = edits.filter((e) => e.lazy).length;
161
+ const eagerCount = edits.length - lazyCount;
162
+ const preferLazyUnderscore = lazyCount >= eagerCount;
163
+ const assign = (kind) => {
164
+ const imported = kind === 'lazy' ? 'gettext_lazy' : 'gettext';
165
+ const otherHasUnderscore = toImport.some((s) => s.endsWith(' as _'));
166
+ if (underscoreFree && !otherHasUnderscore && (kind === 'lazy') === preferLazyUnderscore) {
167
+ toImport.push(`${imported} as _`);
168
+ return '_';
169
+ }
170
+ if (!nameInUse(imported)) {
171
+ toImport.push(imported);
172
+ return imported;
173
+ }
174
+ toImport.push(`${imported} as django_${imported}`);
175
+ return `django_${imported}`;
176
+ };
177
+ if (needLazy && !lazyName)
178
+ lazyName = assign('lazy');
179
+ if (needEager && !eagerName)
180
+ eagerName = assign('eager');
181
+ const spans = edits.map((e) => ({
182
+ start: e.start,
183
+ end: e.end,
184
+ text: `${e.lazy ? lazyName : eagerName}(${e.inner})${e.suffix}`,
185
+ }));
186
+ let out = applySpans(src, spans);
187
+ if (toImport.length > 0) {
188
+ // Recompute against the edited text (offsets before the import block don't move,
189
+ // but recomputing is simpler than tracking).
190
+ const outTokens = (0, python_lexer_1.tokenizePython)(out);
191
+ const ext = existingImports(out, outTokens).extendable;
192
+ if (ext && `${ext.text}, ${toImport.join(', ')}`.length <= 99) {
193
+ out = `${out.slice(0, ext.end)}, ${toImport.join(', ')}${out.slice(ext.end)}`;
194
+ }
195
+ else {
196
+ const { offset, before } = importInsertPoint(out, outTokens);
197
+ const line = `from django.utils.translation import ${toImport.join(', ')}`;
198
+ if (before)
199
+ out = `${out.slice(0, offset)}${line}${nl}${offset === 0 && out.length && !/^\r?\n/.test(out) ? nl : ''}${out.slice(offset)}`;
200
+ else
201
+ out = `${out.slice(0, offset)}${line}${nl}${out.slice(offset)}`;
202
+ }
203
+ }
204
+ return out;
205
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Keys for gettext messages.
3
+ *
4
+ * In a gettext project the code looks up the message itself (msgctxt +
5
+ * msgid), so the engine's keys are internal: they only need to be stable
6
+ * across runs (for the lockfile, TM and the dashboard) and unique per
7
+ * message. They are derived from the message: a readable slug plus a hash
8
+ * of context, singular and plural.
9
+ */
10
+ import type { ExtractedString, GeneratedKey, GettextInfo } from '../runtime/types';
11
+ export declare const ICU_PLURAL_ARG = "count";
12
+ export declare function gettextMessageKey(m: Pick<GettextInfo, 'msgid' | 'msgidPlural' | 'msgctxt'>): string;
13
+ /** Can this plural message travel as one ICU plural without its text being misread as ICU syntax? */
14
+ export declare function icuSafe(m: Pick<GettextInfo, 'msgid' | 'msgidPlural'>): boolean;
15
+ export declare function toIcuPlural(singular: string, plural: string): string;
16
+ /**
17
+ * The engine keys (and source values to translate) for one gettext message:
18
+ * one key for a plain message; one ICU-plural key, or a singular and a
19
+ * plural key, for a plural message.
20
+ */
21
+ export declare function gettextKeys(s: ExtractedString): GeneratedKey[];
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Keys for gettext messages.
4
+ *
5
+ * In a gettext project the code looks up the message itself (msgctxt +
6
+ * msgid), so the engine's keys are internal: they only need to be stable
7
+ * across runs (for the lockfile, TM and the dashboard) and unique per
8
+ * message. They are derived from the message: a readable slug plus a hash
9
+ * of context, singular and plural.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ICU_PLURAL_ARG = void 0;
13
+ exports.gettextMessageKey = gettextMessageKey;
14
+ exports.icuSafe = icuSafe;
15
+ exports.toIcuPlural = toIcuPlural;
16
+ exports.gettextKeys = gettextKeys;
17
+ const crypto_1 = require("crypto");
18
+ exports.ICU_PLURAL_ARG = 'count';
19
+ function gettextMessageKey(m) {
20
+ const hash = (0, crypto_1.createHash)('sha256')
21
+ .update(`${m.msgctxt ?? ''}\u0004${m.msgid}\u0000${m.msgidPlural ?? ''}`, 'utf8')
22
+ .digest('hex')
23
+ .slice(0, 10);
24
+ const words = m.msgid
25
+ .replace(/%\(\w+\)[sdif]|%[sdif]|\{[^{}]*\}|<[^>]*>|&[#\w]+;/g, ' ')
26
+ .toLowerCase()
27
+ .match(/[a-z0-9]+/g) ?? [];
28
+ const slug = words.slice(0, 5).join('_').slice(0, 40) || 'msg';
29
+ return `${slug}_${hash}`;
30
+ }
31
+ /** Can this plural message travel as one ICU plural without its text being misread as ICU syntax? */
32
+ function icuSafe(m) {
33
+ const text = `${m.msgid}${m.msgidPlural ?? ''}`;
34
+ return !/[{}#]|''/.test(text);
35
+ }
36
+ function toIcuPlural(singular, plural) {
37
+ return `{${exports.ICU_PLURAL_ARG}, plural, one {${singular}} other {${plural}}}`;
38
+ }
39
+ /**
40
+ * The engine keys (and source values to translate) for one gettext message:
41
+ * one key for a plain message; one ICU-plural key, or a singular and a
42
+ * plural key, for a plural message.
43
+ */
44
+ function gettextKeys(s) {
45
+ const g = s.gettext;
46
+ const base = gettextMessageKey(g);
47
+ const common = {
48
+ namespace: 'django',
49
+ extractionId: s.id,
50
+ parentContext: g.msgctxt,
51
+ extractionContext: s.context,
52
+ };
53
+ if (g.msgidPlural === undefined) {
54
+ return [{ ...common, key: base, value: g.msgid, gettext: { ...g, form: 'single' } }];
55
+ }
56
+ if (g.icu && icuSafe(g)) {
57
+ return [{ ...common, key: base, value: toIcuPlural(g.msgid, g.msgidPlural), gettext: { ...g, form: 'icu' } }];
58
+ }
59
+ return [
60
+ { ...common, key: `${base}_sg`, value: g.msgid, gettext: { ...g, form: 'singular' } },
61
+ { ...common, key: `${base}_pl`, value: g.msgidPlural, extractionId: `${s.id}_pl`, gettext: { ...g, form: 'plural' } },
62
+ ];
63
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Django's two spellings of a language: the locale directory name
3
+ * (`to_locale()`: pt_BR, zh_Hans, sr_Latn) and the language code used in
4
+ * LANGUAGES / URLs (`to_language()`: pt-br, zh-hans, sr-latn).
5
+ */
6
+ /** Locale directory name Django loads for a Babelize locale code. */
7
+ export declare function djangoLocaleName(locale: string): string;
8
+ /** Language code for Django's LANGUAGES setting. */
9
+ export declare function djangoLanguageCode(locale: string): string;
10
+ /** Babelize locale code for a Django locale directory name (pt_BR → pt-BR). */
11
+ export declare function localeFromDjangoName(dir: string): string;
12
+ /** English name of a language, for LANGUAGES ("German", "Brazilian Portuguese"). */
13
+ export declare function languageName(locale: string): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Django's two spellings of a language: the locale directory name
4
+ * (`to_locale()`: pt_BR, zh_Hans, sr_Latn) and the language code used in
5
+ * LANGUAGES / URLs (`to_language()`: pt-br, zh-hans, sr-latn).
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.djangoLocaleName = djangoLocaleName;
9
+ exports.djangoLanguageCode = djangoLanguageCode;
10
+ exports.localeFromDjangoName = localeFromDjangoName;
11
+ exports.languageName = languageName;
12
+ const CHINESE = {
13
+ zh: { dir: 'zh_Hans', code: 'zh-hans' },
14
+ 'zh-cn': { dir: 'zh_Hans', code: 'zh-hans' },
15
+ 'zh-sg': { dir: 'zh_Hans', code: 'zh-hans' },
16
+ 'zh-hans': { dir: 'zh_Hans', code: 'zh-hans' },
17
+ 'zh-tw': { dir: 'zh_Hant', code: 'zh-hant' },
18
+ 'zh-hk': { dir: 'zh_Hant', code: 'zh-hant' },
19
+ 'zh-mo': { dir: 'zh_Hant', code: 'zh-hant' },
20
+ 'zh-hant': { dir: 'zh_Hant', code: 'zh-hant' },
21
+ };
22
+ /** Locale directory name Django loads for a Babelize locale code. */
23
+ function djangoLocaleName(locale) {
24
+ const lower = locale.toLowerCase().replace(/_/g, '-');
25
+ if (CHINESE[lower])
26
+ return CHINESE[lower].dir;
27
+ const [lang, ...rest] = lower.split('-');
28
+ if (!rest.length)
29
+ return lang;
30
+ const tail = rest.map((p) => (p.length === 2 ? p.toUpperCase() : p.length === 4 ? p[0].toUpperCase() + p.slice(1) : p));
31
+ return [lang, ...tail].join('_');
32
+ }
33
+ /** Language code for Django's LANGUAGES setting. */
34
+ function djangoLanguageCode(locale) {
35
+ const lower = locale.toLowerCase().replace(/_/g, '-');
36
+ if (CHINESE[lower])
37
+ return CHINESE[lower].code;
38
+ return lower;
39
+ }
40
+ /** Babelize locale code for a Django locale directory name (pt_BR → pt-BR). */
41
+ function localeFromDjangoName(dir) {
42
+ const [lang, ...rest] = dir.split('_');
43
+ if (lang === 'zh' && rest[0] === 'Hans')
44
+ return 'zh-CN';
45
+ if (lang === 'zh' && rest[0] === 'Hant')
46
+ return 'zh-TW';
47
+ return [lang.toLowerCase(), ...rest].join('-');
48
+ }
49
+ /** English name of a language, for LANGUAGES ("German", "Brazilian Portuguese"). */
50
+ function languageName(locale) {
51
+ try {
52
+ const name = new Intl.DisplayNames(['en'], { type: 'language' }).of(locale.replace('_', '-'));
53
+ if (name && name.toLowerCase() !== locale.toLowerCase())
54
+ return name;
55
+ }
56
+ catch {
57
+ // fall through
58
+ }
59
+ return locale;
60
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Compile a .po catalogue to GNU .mo, the way `msgfmt` does: fuzzy and
3
+ * untranslated entries (and obsolete ones) are left out, so Django falls
4
+ * back to the msgid for them.
5
+ */
6
+ export declare function compileMo(poContent: string): Buffer;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Compile a .po catalogue to GNU .mo, the way `msgfmt` does: fuzzy and
4
+ * untranslated entries (and obsolete ones) are left out, so Django falls
5
+ * back to the msgid for them.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.compileMo = compileMo;
9
+ const po_file_1 = require("./po-file");
10
+ function compileMo(poContent) {
11
+ const po = (0, po_file_1.parsePo)(poContent);
12
+ const pairs = [];
13
+ if (po.header)
14
+ pairs.push([Buffer.alloc(0), Buffer.from(po.header.msgstr[0] ?? '', 'utf8')]);
15
+ for (const e of po.entries) {
16
+ if (e.obsolete || e.flags.includes('fuzzy'))
17
+ continue;
18
+ if (e.msgstr.length === 0 || e.msgstr.some((s) => !s))
19
+ continue;
20
+ const id = (e.msgctxt !== undefined ? `${e.msgctxt}\u0004` : '') + e.msgid + (e.msgidPlural !== undefined ? `\u0000${e.msgidPlural}` : '');
21
+ pairs.push([Buffer.from(id, 'utf8'), Buffer.from(e.msgstr.join('\u0000'), 'utf8')]);
22
+ }
23
+ pairs.sort((a, b) => Buffer.compare(a[0], b[0]));
24
+ const n = pairs.length;
25
+ const headerSize = 28;
26
+ const origTable = headerSize;
27
+ const transTable = origTable + n * 8;
28
+ let offset = transTable + n * 8;
29
+ const origOffsets = [];
30
+ const transOffsets = [];
31
+ for (const [id] of pairs) {
32
+ origOffsets.push([id.length, offset]);
33
+ offset += id.length + 1;
34
+ }
35
+ for (const [, str] of pairs) {
36
+ transOffsets.push([str.length, offset]);
37
+ offset += str.length + 1;
38
+ }
39
+ const out = Buffer.alloc(offset);
40
+ out.writeUInt32LE(0x950412de, 0); // magic
41
+ out.writeUInt32LE(0, 4); // revision
42
+ out.writeUInt32LE(n, 8);
43
+ out.writeUInt32LE(origTable, 12);
44
+ out.writeUInt32LE(transTable, 16);
45
+ out.writeUInt32LE(0, 20); // hash table size
46
+ out.writeUInt32LE(offset, 24); // hash table offset (unused)
47
+ origOffsets.forEach(([len, off], i) => {
48
+ out.writeUInt32LE(len, origTable + i * 8);
49
+ out.writeUInt32LE(off, origTable + i * 8 + 4);
50
+ });
51
+ transOffsets.forEach(([len, off], i) => {
52
+ out.writeUInt32LE(len, transTable + i * 8);
53
+ out.writeUInt32LE(off, transTable + i * 8 + 4);
54
+ });
55
+ pairs.forEach(([id, str], i) => {
56
+ id.copy(out, origOffsets[i][1]);
57
+ str.copy(out, transOffsets[i][1]);
58
+ });
59
+ return out;
60
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * gettext plural forms.
3
+ *
4
+ * A PO file's `Plural-Forms` header says how many msgstr[n] forms a plural
5
+ * entry has and which one a number picks. CLDR (what translators and ICU
6
+ * think in) names categories instead: one, few, many, other. This module
7
+ * maps between the two by evaluating the gettext expression on sample
8
+ * numbers and asking Intl.PluralRules which category each sample is, so the
9
+ * mapping is right even for headers written by other tools.
10
+ *
11
+ * Expressions are evaluated by a small parser — never `eval` — because
12
+ * headers come from repositories Babelize doesn't control.
13
+ */
14
+ export declare class PluralFormsError extends Error {
15
+ }
16
+ export interface PluralForms {
17
+ /** The header value, e.g. `nplurals=2; plural=(n != 1);` */
18
+ header: string;
19
+ nplurals: number;
20
+ /** msgstr index for a number. */
21
+ index(n: number): number;
22
+ }
23
+ export declare function parsePluralForms(header: string): PluralForms;
24
+ /** Plural-Forms header for a Babelize locale code. */
25
+ export declare function pluralFormsFor(locale: string): {
26
+ header: string;
27
+ known: boolean;
28
+ };
29
+ export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
30
+ /**
31
+ * CLDR category of each msgstr index for `locale` under `forms`: the
32
+ * category most sample integers that pick the index fall into.
33
+ */
34
+ export declare function indexCategories(forms: PluralForms, locale: string): PluralCategory[];