@babelize/engine 0.0.0-stage → 0.1.2
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/LICENSE +202 -0
- package/dist/BabelizeEngine.d.ts +12 -0
- package/dist/BabelizeEngine.js +47 -0
- package/dist/analysis/analyze.d.ts +106 -0
- package/dist/analysis/analyze.js +323 -0
- package/dist/config/env.d.ts +9 -0
- package/dist/config/env.js +16 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.js +47 -0
- 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 +59 -0
- package/dist/index.js +140 -0
- package/dist/injection/InjectionStrategy.d.ts +62 -0
- package/dist/injection/InjectionStrategy.js +2 -0
- package/dist/injection/StrategyFactory.d.ts +4 -0
- package/dist/injection/StrategyFactory.js +29 -0
- package/dist/injection/html/html-injector.d.ts +11 -0
- package/dist/injection/html/html-injector.js +173 -0
- package/dist/injection/strategies/NextIntlStrategy.d.ts +25 -0
- package/dist/injection/strategies/NextIntlStrategy.js +141 -0
- package/dist/injection/strategies/ReactI18nextStrategy.d.ts +19 -0
- package/dist/injection/strategies/ReactI18nextStrategy.js +116 -0
- package/dist/injection/strategies/VanillaStrategy.d.ts +11 -0
- package/dist/injection/strategies/VanillaStrategy.js +73 -0
- package/dist/injection/strategies/VueI18nStrategy.d.ts +11 -0
- package/dist/injection/strategies/VueI18nStrategy.js +84 -0
- package/dist/local/formatter.d.ts +26 -0
- package/dist/local/formatter.js +105 -0
- package/dist/local/git.d.ts +23 -0
- package/dist/local/git.js +136 -0
- package/dist/local/hosted-provider.d.ts +68 -0
- package/dist/local/hosted-provider.js +215 -0
- package/dist/local/index.d.ts +23 -0
- package/dist/local/index.js +75 -0
- package/dist/local/provider.d.ts +56 -0
- package/dist/local/provider.js +119 -0
- package/dist/local/pseudo-provider.d.ts +18 -0
- package/dist/local/pseudo-provider.js +208 -0
- package/dist/local/report.d.ts +177 -0
- package/dist/local/report.js +186 -0
- package/dist/local/translate-core.d.ts +218 -0
- package/dist/local/translate-core.js +499 -0
- package/dist/local/translate-messages.d.ts +49 -0
- package/dist/local/translate-messages.js +89 -0
- package/dist/local/workspace-copy.d.ts +91 -0
- package/dist/local/workspace-copy.js +355 -0
- package/dist/modules/extractor/extract.d.ts +20 -0
- package/dist/modules/extractor/extract.js +709 -0
- package/dist/modules/extractor/index.d.ts +2 -0
- package/dist/modules/extractor/index.js +63 -0
- package/dist/modules/finalization/django-finalize.d.ts +28 -0
- package/dist/modules/finalization/django-finalize.js +412 -0
- package/dist/modules/finalization/finalize.d.ts +38 -0
- package/dist/modules/finalization/finalize.js +745 -0
- package/dist/modules/finalization/index.d.ts +1 -0
- package/dist/modules/finalization/index.js +5 -0
- package/dist/modules/finalization/nextjs-finalize.d.ts +40 -0
- package/dist/modules/finalization/nextjs-finalize.js +328 -0
- package/dist/modules/finalization/static-runtime.d.ts +9 -0
- package/dist/modules/finalization/static-runtime.js +261 -0
- package/dist/modules/finalization/writes.d.ts +29 -0
- package/dist/modules/finalization/writes.js +78 -0
- package/dist/modules/format-injection/index.d.ts +19 -0
- package/dist/modules/format-injection/index.js +273 -0
- package/dist/modules/format-injection/inject-formats.d.ts +31 -0
- package/dist/modules/format-injection/inject-formats.js +208 -0
- package/dist/modules/glossary/index.d.ts +2 -0
- package/dist/modules/glossary/index.js +16 -0
- package/dist/modules/glossary/rules.d.ts +81 -0
- package/dist/modules/glossary/rules.js +244 -0
- package/dist/modules/glossary/types.d.ts +22 -0
- package/dist/modules/glossary/types.js +2 -0
- package/dist/modules/injection/index.d.ts +1 -0
- package/dist/modules/injection/index.js +6 -0
- package/dist/modules/injection/inject.d.ts +29 -0
- package/dist/modules/injection/inject.js +1050 -0
- package/dist/modules/keygen/index.d.ts +1 -0
- package/dist/modules/keygen/index.js +11 -0
- package/dist/modules/keygen/keygen.d.ts +4 -0
- package/dist/modules/keygen/keygen.js +123 -0
- package/dist/modules/keygen/locale-data.d.ts +7 -0
- package/dist/modules/keygen/locale-data.js +73 -0
- package/dist/modules/keygen/public.d.ts +2 -0
- package/dist/modules/keygen/public.js +12 -0
- package/dist/modules/locale-builder/index.d.ts +1 -0
- package/dist/modules/locale-builder/index.js +7 -0
- package/dist/modules/locale-builder/locales.d.ts +13 -0
- package/dist/modules/locale-builder/locales.js +130 -0
- package/dist/modules/lockfile/hasher.d.ts +7 -0
- package/dist/modules/lockfile/hasher.js +13 -0
- package/dist/modules/lockfile/index.d.ts +3 -0
- package/dist/modules/lockfile/index.js +22 -0
- package/dist/modules/lockfile/lockfile-manager.d.ts +99 -0
- package/dist/modules/lockfile/lockfile-manager.js +497 -0
- package/dist/modules/lockfile/schema.d.ts +196 -0
- package/dist/modules/lockfile/schema.js +56 -0
- package/dist/modules/parser/index.d.ts +2 -0
- package/dist/modules/parser/index.js +9 -0
- package/dist/modules/summary/index.d.ts +1 -0
- package/dist/modules/summary/index.js +7 -0
- package/dist/modules/summary/summary.d.ts +9 -0
- package/dist/modules/summary/summary.js +195 -0
- package/dist/modules/sync-detector/index.d.ts +2 -0
- package/dist/modules/sync-detector/index.js +7 -0
- package/dist/modules/sync-detector/reporter.d.ts +3 -0
- package/dist/modules/sync-detector/reporter.js +16 -0
- package/dist/modules/sync-detector/sync-detector.d.ts +39 -0
- package/dist/modules/sync-detector/sync-detector.js +374 -0
- package/dist/modules/translator/batch-optimizer.d.ts +12 -0
- package/dist/modules/translator/batch-optimizer.js +30 -0
- package/dist/modules/translator/client/byok.provider.d.ts +64 -0
- package/dist/modules/translator/client/byok.provider.js +319 -0
- package/dist/modules/translator/client/direct-translator.client.d.ts +12 -0
- package/dist/modules/translator/client/direct-translator.client.js +71 -0
- package/dist/modules/translator/client/gemini.provider.d.ts +1 -0
- package/dist/modules/translator/client/gemini.provider.js +110 -0
- package/dist/modules/translator/client/groq.provider.d.ts +1 -0
- package/dist/modules/translator/client/groq.provider.js +111 -0
- package/dist/modules/translator/client/request.types.d.ts +1 -0
- package/dist/modules/translator/client/request.types.js +2 -0
- package/dist/modules/translator/client/retry.d.ts +29 -0
- package/dist/modules/translator/client/retry.js +94 -0
- package/dist/modules/translator/client/test-utils/mock.client.d.ts +31 -0
- package/dist/modules/translator/client/test-utils/mock.client.js +59 -0
- package/dist/modules/translator/client/translator.interface.d.ts +1 -0
- package/dist/modules/translator/client/translator.interface.js +2 -0
- package/dist/modules/translator/icu-branches.d.ts +138 -0
- package/dist/modules/translator/icu-branches.js +393 -0
- package/dist/modules/translator/language-codes/errors.d.ts +10 -0
- package/dist/modules/translator/language-codes/errors.js +18 -0
- package/dist/modules/translator/language-codes/flores-map.d.ts +1 -0
- package/dist/modules/translator/language-codes/flores-map.js +107 -0
- package/dist/modules/translator/language-codes/index.d.ts +11 -0
- package/dist/modules/translator/language-codes/index.js +24 -0
- package/dist/modules/translator/language-codes/language-mapper.d.ts +25 -0
- package/dist/modules/translator/language-codes/language-mapper.js +132 -0
- package/dist/modules/translator/language-codes/rtl-languages.d.ts +2 -0
- package/dist/modules/translator/language-codes/rtl-languages.js +9 -0
- package/dist/modules/translator/memory/memory-matcher.d.ts +8 -0
- package/dist/modules/translator/memory/memory-matcher.js +218 -0
- package/dist/modules/translator/memory/types.d.ts +22 -0
- package/dist/modules/translator/memory/types.js +2 -0
- package/dist/modules/translator/translate.d.ts +74 -0
- package/dist/modules/translator/translate.js +1047 -0
- package/dist/modules/vue/migrate.d.ts +13 -0
- package/dist/modules/vue/migrate.js +40 -0
- package/dist/modules/vue/script-inject.d.ts +16 -0
- package/dist/modules/vue/script-inject.js +115 -0
- package/dist/modules/vue/sfc.d.ts +11 -0
- package/dist/modules/vue/sfc.js +51 -0
- package/dist/modules/vue/template-rewrite.d.ts +25 -0
- package/dist/modules/vue/template-rewrite.js +76 -0
- package/dist/modules/vue/template-scan.d.ts +29 -0
- package/dist/modules/vue/template-scan.js +264 -0
- package/dist/modules/workspace/index.d.ts +1 -0
- package/dist/modules/workspace/index.js +5 -0
- package/dist/modules/workspace/validate.d.ts +13 -0
- package/dist/modules/workspace/validate.js +333 -0
- package/dist/orchestrator.d.ts +13 -0
- package/dist/orchestrator.js +727 -0
- package/dist/providers/default-translation-provider.d.ts +20 -0
- package/dist/providers/default-translation-provider.js +30 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +21 -0
- package/dist/providers/types.d.ts +153 -0
- package/dist/providers/types.js +2 -0
- package/dist/qa/check-project.d.ts +50 -0
- package/dist/qa/check-project.js +193 -0
- package/dist/qa/gettext-files.d.ts +15 -0
- package/dist/qa/gettext-files.js +115 -0
- package/dist/qa/locale-files.d.ts +44 -0
- package/dist/qa/locale-files.js +159 -0
- package/dist/qa/locale-qa.d.ts +61 -0
- package/dist/qa/locale-qa.js +342 -0
- package/dist/qa/review.d.ts +43 -0
- package/dist/qa/review.js +175 -0
- package/dist/runtime/config.d.ts +8 -0
- package/dist/runtime/config.js +37 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/types.d.ts +427 -0
- package/dist/runtime/types.js +38 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +12 -0
- package/dist/utils/ast-helpers.d.ts +57 -0
- package/dist/utils/ast-helpers.js +608 -0
- package/dist/utils/cache-keys.d.ts +8 -0
- package/dist/utils/cache-keys.js +18 -0
- package/dist/utils/circuit-breaker.d.ts +43 -0
- package/dist/utils/circuit-breaker.js +119 -0
- package/dist/utils/concurrent-map.d.ts +4 -0
- package/dist/utils/concurrent-map.js +21 -0
- package/dist/utils/detect-format.d.ts +5 -0
- package/dist/utils/detect-format.js +125 -0
- package/dist/utils/extraction-options.d.ts +29 -0
- package/dist/utils/extraction-options.js +33 -0
- package/dist/utils/file-utils.d.ts +33 -0
- package/dist/utils/file-utils.js +267 -0
- package/dist/utils/glossary-match.d.ts +9 -0
- package/dist/utils/glossary-match.js +25 -0
- package/dist/utils/html-extractor.d.ts +8 -0
- package/dist/utils/html-extractor.js +317 -0
- package/dist/utils/i18next-icu.d.ts +38 -0
- package/dist/utils/i18next-icu.js +159 -0
- package/dist/utils/icu-message.d.ts +65 -0
- package/dist/utils/icu-message.js +346 -0
- package/dist/utils/icu-spans.d.ts +10 -0
- package/dist/utils/icu-spans.js +126 -0
- package/dist/utils/jsx-message.d.ts +93 -0
- package/dist/utils/jsx-message.js +628 -0
- package/dist/utils/key-usage.d.ts +35 -0
- package/dist/utils/key-usage.js +247 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/metadata.d.ts +82 -0
- package/dist/utils/metadata.js +261 -0
- package/dist/utils/module-constants.d.ts +30 -0
- package/dist/utils/module-constants.js +265 -0
- package/dist/utils/not-injected.d.ts +6 -0
- package/dist/utils/not-injected.js +38 -0
- package/dist/utils/placeholder-protection.d.ts +74 -0
- package/dist/utils/placeholder-protection.js +181 -0
- package/dist/utils/pseudo-markers.d.ts +9 -0
- package/dist/utils/pseudo-markers.js +16 -0
- package/dist/utils/string-utils.d.ts +52 -0
- package/dist/utils/string-utils.js +270 -0
- package/dist/utils/translation-quality.d.ts +34 -0
- package/dist/utils/translation-quality.js +156 -0
- package/dist/utils/translation-sanitizer.d.ts +11 -0
- package/dist/utils/translation-sanitizer.js +71 -0
- package/dist/utils/translator-caller.d.ts +32 -0
- package/dist/utils/translator-caller.js +217 -0
- package/dist/utils/translator-load-balancer.d.ts +73 -0
- package/dist/utils/translator-load-balancer.js +297 -0
- package/dist/utils/ui-copy.d.ts +32 -0
- package/dist/utils/ui-copy.js +126 -0
- package/dist/utils/untranslated.d.ts +15 -0
- package/dist/utils/untranslated.js +34 -0
- package/dist/utils/workspace.d.ts +14 -0
- package/dist/utils/workspace.js +89 -0
- package/package.json +64 -4
- package/schemas/translate-report.schema.json +297 -0
- package/README.md +0 -3
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Django Template Language extraction.
|
|
4
|
+
*
|
|
5
|
+
* Finds strings already marked in templates — `{% translate %}` / `{% trans %}`,
|
|
6
|
+
* `{% blocktranslate %}` / `{% blocktrans %}` (with count/plural, context,
|
|
7
|
+
* with/trimmed/asvar) and `_("…")` in filter and tag arguments — and
|
|
8
|
+
* hardcoded user-facing text the injector can mark:
|
|
9
|
+
*
|
|
10
|
+
* - text in the page, as `{% translate "…" %}`, or as one
|
|
11
|
+
* `{% blocktranslate %}` when the sentence carries `{{ variables }}` or
|
|
12
|
+
* inline markup (`<a>`, `<strong>`, …). Variables with lookups or
|
|
13
|
+
* filters are bound with `with name=expr` because blocktranslate only
|
|
14
|
+
* accepts plain names;
|
|
15
|
+
* - title / alt / placeholder / aria-label attributes and button values;
|
|
16
|
+
* - string arguments of the `default` / `default_if_none` filters.
|
|
17
|
+
*
|
|
18
|
+
* Every edit is an exact replacement of the original span, so nothing else
|
|
19
|
+
* in the template (whitespace, attribute order, other tags) changes.
|
|
20
|
+
* Text inside <script>, <style>, <pre>, <code>, <textarea>, <svg>,
|
|
21
|
+
* {% comment %} and {% verbatim %} is never touched.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.smartSplit = smartSplit;
|
|
25
|
+
exports.trimWhitespace = trimWhitespace;
|
|
26
|
+
exports.extractDjangoTemplate = extractDjangoTemplate;
|
|
27
|
+
const types_1 = require("./types");
|
|
28
|
+
/** Inline elements that may sit inside a translated sentence. */
|
|
29
|
+
const INLINE = new Set([
|
|
30
|
+
'a', 'abbr', 'b', 'bdi', 'bdo', 'cite', 'data', 'dfn', 'em', 'i', 'kbd', 'mark', 'q', 's', 'samp',
|
|
31
|
+
'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var',
|
|
32
|
+
]);
|
|
33
|
+
/** Elements whose content is never treated as page text. */
|
|
34
|
+
const SKIP_CONTENT = new Set(['script', 'style', 'pre', 'code', 'textarea', 'svg', 'math', 'template']);
|
|
35
|
+
const RAW_TEXT = new Set(['script', 'style']);
|
|
36
|
+
const TEXT_ATTRS = new Set(['title', 'alt', 'placeholder', 'aria-label', 'aria-description']);
|
|
37
|
+
const VOID = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
38
|
+
const ENTITY = /&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#\d{1,7}|#x[0-9a-fA-F]{1,6});/g;
|
|
39
|
+
const SEPARATOR = String.raw `(?:&(?:mdash|ndash|middot|bull|raquo|laquo|nbsp|#8212|#8211|#183);|[—–·•|»«])`;
|
|
40
|
+
const SEPARATOR_LEAD = new RegExp(`^(?:${SEPARATOR}\\s*)+`);
|
|
41
|
+
const SEPARATOR_TRAIL = new RegExp(`(?:\\s*${SEPARATOR})+$`);
|
|
42
|
+
/** Split DTL tag contents into bits, keeping quoted strings (and _("…")) whole. */
|
|
43
|
+
function smartSplit(text) {
|
|
44
|
+
const bits = [];
|
|
45
|
+
const re = /(?:[^\s'"]*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')[^\s'"]*)+|\S+/g;
|
|
46
|
+
for (const m of text.matchAll(re))
|
|
47
|
+
bits.push(m[0]);
|
|
48
|
+
return bits;
|
|
49
|
+
}
|
|
50
|
+
/** Value of a DTL string literal ("…" or '…'), or null if `bit` isn't one. */
|
|
51
|
+
function literalValue(bit) {
|
|
52
|
+
const m = /^_\((.*)\)$/s.exec(bit);
|
|
53
|
+
const b = m ? m[1] : bit;
|
|
54
|
+
if (b.length < 2 || !((b[0] === '"' || b[0] === "'") && b[b.length - 1] === b[0]))
|
|
55
|
+
return null;
|
|
56
|
+
const q = b[0];
|
|
57
|
+
return b.slice(1, -1).replace(new RegExp(`\\\\${q}`, 'g'), q).replace(/\\\\/g, '\\');
|
|
58
|
+
}
|
|
59
|
+
/** Django's trim_whitespace() for {% blocktranslate trimmed %}. */
|
|
60
|
+
function trimWhitespace(s) {
|
|
61
|
+
return s.trim().replace(/\s*\n\s*/g, ' ');
|
|
62
|
+
}
|
|
63
|
+
function lineAt(src, offset) {
|
|
64
|
+
let n = 1;
|
|
65
|
+
for (let i = 0; i < offset && i < src.length; i++)
|
|
66
|
+
if (src.charCodeAt(i) === 10)
|
|
67
|
+
n++;
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
function hasLetters(text) {
|
|
71
|
+
return /\p{L}/u.test(text.replace(ENTITY, ' '));
|
|
72
|
+
}
|
|
73
|
+
/** End of a DTL token starting at `i` (`{%`, `{{`, `{#`), or -1. DTL tokens never span lines. */
|
|
74
|
+
function dtlEnd(src, i) {
|
|
75
|
+
const close = src[i + 1] === '%' ? '%}' : src[i + 1] === '{' ? '}}' : '#}';
|
|
76
|
+
const nl = src.indexOf('\n', i);
|
|
77
|
+
const end = src.indexOf(close, i + 2);
|
|
78
|
+
if (end === -1 || (nl !== -1 && nl < end))
|
|
79
|
+
return -1;
|
|
80
|
+
return end + 2;
|
|
81
|
+
}
|
|
82
|
+
/** End (after `>`) of an HTML tag starting at `i`, skipping quoted values and DTL tokens. */
|
|
83
|
+
function htmlTagEnd(src, i) {
|
|
84
|
+
let k = i + 1;
|
|
85
|
+
let quote = '';
|
|
86
|
+
while (k < src.length) {
|
|
87
|
+
const ch = src[k];
|
|
88
|
+
if (!quote && ch === '{' && (src[k + 1] === '%' || src[k + 1] === '{' || src[k + 1] === '#')) {
|
|
89
|
+
const e = dtlEnd(src, k);
|
|
90
|
+
if (e !== -1) {
|
|
91
|
+
k = e;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (quote) {
|
|
96
|
+
if (ch === quote)
|
|
97
|
+
quote = '';
|
|
98
|
+
}
|
|
99
|
+
else if (ch === '"' || ch === "'") {
|
|
100
|
+
quote = ch;
|
|
101
|
+
}
|
|
102
|
+
else if (ch === '>') {
|
|
103
|
+
return k + 1;
|
|
104
|
+
}
|
|
105
|
+
else if (ch === '<') {
|
|
106
|
+
return -1;
|
|
107
|
+
}
|
|
108
|
+
k++;
|
|
109
|
+
}
|
|
110
|
+
return -1;
|
|
111
|
+
}
|
|
112
|
+
function parseAttrs(src, start, end) {
|
|
113
|
+
const attrs = [];
|
|
114
|
+
const text = src.slice(start, end);
|
|
115
|
+
const re = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
116
|
+
// Mask DTL tokens so `{% if x %}` inside a tag isn't read as attributes.
|
|
117
|
+
const masked = text.replace(/\{%.*?%\}|\{\{.*?\}\}|\{#.*?#\}/g, (m) => '\u0000'.repeat(m.length));
|
|
118
|
+
for (const m of masked.matchAll(re)) {
|
|
119
|
+
const name = m[1].toLowerCase();
|
|
120
|
+
if (name.includes('\u0000'))
|
|
121
|
+
continue;
|
|
122
|
+
if (m[2] === undefined && m[3] === undefined && m[4] === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
const quote = m[2] !== undefined ? '"' : m[3] !== undefined ? "'" : '';
|
|
125
|
+
const rawVal = m[2] ?? m[3] ?? m[4] ?? '';
|
|
126
|
+
const valueOffsetInMatch = m[0].lastIndexOf(rawVal + quote) - (quote ? 0 : 0);
|
|
127
|
+
const valueStart = start + m.index + valueOffsetInMatch;
|
|
128
|
+
const valueEnd = valueStart + rawVal.length;
|
|
129
|
+
attrs.push({ name, value: src.slice(valueStart, valueEnd), valueStart, valueEnd, quote });
|
|
130
|
+
}
|
|
131
|
+
return attrs;
|
|
132
|
+
}
|
|
133
|
+
/** `{% translate "…" %}` for `text`, or null when it can't be quoted safely. */
|
|
134
|
+
function translateTag(text, preferQuote = '"', onlyPreferred = false) {
|
|
135
|
+
if (text.includes('\\'))
|
|
136
|
+
return null;
|
|
137
|
+
const other = preferQuote === '"' ? "'" : '"';
|
|
138
|
+
if (!text.includes(preferQuote))
|
|
139
|
+
return `{% translate ${preferQuote}${text}${preferQuote} %}`;
|
|
140
|
+
if (!onlyPreferred && !text.includes(other))
|
|
141
|
+
return `{% translate ${other}${text}${other} %}`;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/** Variable name for a `{{ expr }}` inside blocktranslate. */
|
|
145
|
+
function bindingName(expr) {
|
|
146
|
+
const base = expr.split('|')[0].trim();
|
|
147
|
+
const parts = base.split('.').filter((p) => /^[A-Za-z_]\w*$/.test(p));
|
|
148
|
+
const last = parts[parts.length - 1] ?? 'value';
|
|
149
|
+
return last === 'super' && parts.length > 1 ? 'block_super' : last;
|
|
150
|
+
}
|
|
151
|
+
function extractDjangoTemplate(src, _options) {
|
|
152
|
+
const findings = [];
|
|
153
|
+
const unwired = [];
|
|
154
|
+
const extraEdits = [];
|
|
155
|
+
let run = [];
|
|
156
|
+
/** Open SKIP_CONTENT elements (by name) we are currently inside. */
|
|
157
|
+
const skipStack = [];
|
|
158
|
+
/** Translator comment from the closest preceding {# Translators: … #}. */
|
|
159
|
+
let pendingComment = null;
|
|
160
|
+
const comment = (at) => {
|
|
161
|
+
if (!pendingComment)
|
|
162
|
+
return [];
|
|
163
|
+
const between = src.slice(pendingComment.end, at);
|
|
164
|
+
return /^\s*$/.test(between) && (between.match(/\n/g)?.length ?? 0) <= 1 ? [pendingComment.text] : [];
|
|
165
|
+
};
|
|
166
|
+
const addWrapped = (msgid, line, extra = {}) => {
|
|
167
|
+
if (!msgid)
|
|
168
|
+
return;
|
|
169
|
+
findings.push({
|
|
170
|
+
message: {
|
|
171
|
+
msgid,
|
|
172
|
+
msgidPlural: extra.plural,
|
|
173
|
+
msgctxt: extra.ctx,
|
|
174
|
+
flags: (0, types_1.formatFlags)(msgid, extra.plural).filter((f) => f === 'python-format'),
|
|
175
|
+
comments: extra.comments ?? [],
|
|
176
|
+
},
|
|
177
|
+
line,
|
|
178
|
+
wrapped: true,
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
const addHardcoded = (msgid, start, end, replacement, line) => {
|
|
182
|
+
findings.push({
|
|
183
|
+
message: { msgid, flags: (0, types_1.formatFlags)(msgid).filter((f) => f === 'python-format'), comments: comment(start) },
|
|
184
|
+
line,
|
|
185
|
+
wrapped: false,
|
|
186
|
+
edit: { kind: 'template', start, end, replacement },
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
/** `_("…")` inside a DTL var or tag: already marked. */
|
|
190
|
+
const scanUnderscore = (token, offset) => {
|
|
191
|
+
for (const m of token.matchAll(/_\(\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\)/g)) {
|
|
192
|
+
const v = literalValue(m[1]);
|
|
193
|
+
if (v)
|
|
194
|
+
addWrapped(v.replace(/%/g, '%%'), lineAt(src, offset + m.index));
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
/** `{{ x|default:"Text" }}` → `{{ x|default:_("Text") }}` */
|
|
198
|
+
const scanFilterDefaults = (token, offset) => {
|
|
199
|
+
if (skipStack.length)
|
|
200
|
+
return;
|
|
201
|
+
for (const m of token.matchAll(/\|\s*(default|default_if_none)\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g)) {
|
|
202
|
+
const lit = m[2];
|
|
203
|
+
const v = literalValue(lit);
|
|
204
|
+
if (!v || !hasLetters(v) || v.includes('\\'))
|
|
205
|
+
continue;
|
|
206
|
+
const litStart = offset + m.index + m[0].length - lit.length;
|
|
207
|
+
addHardcoded(v.replace(/%/g, '%%'), litStart, litStart + lit.length, `_(${lit})`, lineAt(src, litStart));
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
// ── Hardcoded text runs ─────────────────────────────────────
|
|
211
|
+
const flush = () => {
|
|
212
|
+
const items = run;
|
|
213
|
+
run = [];
|
|
214
|
+
emitRun(items);
|
|
215
|
+
};
|
|
216
|
+
const emitRun = (input) => {
|
|
217
|
+
let items = [...input];
|
|
218
|
+
// Trim whitespace-only text at the edges.
|
|
219
|
+
const isBlank = (it) => it.type === 'text' && !src.slice(it.start, it.end).trim();
|
|
220
|
+
while (items.length && isBlank(items[0]))
|
|
221
|
+
items.shift();
|
|
222
|
+
while (items.length && isBlank(items[items.length - 1]))
|
|
223
|
+
items.pop();
|
|
224
|
+
if (!items.length)
|
|
225
|
+
return;
|
|
226
|
+
// Peel an element that wraps the whole run: translate its content instead.
|
|
227
|
+
for (;;) {
|
|
228
|
+
const first = items[0];
|
|
229
|
+
const last = items[items.length - 1];
|
|
230
|
+
if (first.type === 'open' && last.type === 'close' && matchingClose(items, 0) === items.length - 1) {
|
|
231
|
+
items = items.slice(1, -1);
|
|
232
|
+
while (items.length && isBlank(items[0]))
|
|
233
|
+
items.shift();
|
|
234
|
+
while (items.length && isBlank(items[items.length - 1]))
|
|
235
|
+
items.pop();
|
|
236
|
+
if (!items.length)
|
|
237
|
+
return;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
const balanced = isBalanced(items);
|
|
243
|
+
const topLevelText = items.some((it, idx) => it.type === 'text' && hasLetters(src.slice(it.start, it.end)) && depthAt(items, idx) === 0) ||
|
|
244
|
+
items.some((it, idx) => it.type === 'var' && depthAt(items, idx) === 0);
|
|
245
|
+
const hasTags = items.some((it) => it.type === 'open' || it.type === 'close');
|
|
246
|
+
if (hasTags && (!balanced || !topLevelText)) {
|
|
247
|
+
// Split at the tags and handle each piece on its own.
|
|
248
|
+
let piece = [];
|
|
249
|
+
for (const it of items) {
|
|
250
|
+
if (it.type === 'open' || it.type === 'close') {
|
|
251
|
+
emitRun(piece);
|
|
252
|
+
piece = [];
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
piece.push(it);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
emitRun(piece);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const textItems = items.filter((it) => it.type === 'text');
|
|
262
|
+
if (!textItems.some((it) => hasLetters(src.slice(it.start, it.end))))
|
|
263
|
+
return;
|
|
264
|
+
// Core span: from the first non-space char to the last.
|
|
265
|
+
const first = items[0];
|
|
266
|
+
const last = items[items.length - 1];
|
|
267
|
+
let start = first.start;
|
|
268
|
+
let end = last.end;
|
|
269
|
+
if (first.type === 'text')
|
|
270
|
+
start += src.slice(first.start, first.end).length - src.slice(first.start, first.end).trimStart().length;
|
|
271
|
+
if (last.type === 'text')
|
|
272
|
+
end -= src.slice(last.start, last.end).length - src.slice(last.start, last.end).trimEnd().length;
|
|
273
|
+
// Layout separators at the edges ("— City Library", "Home |") aren't part of the text.
|
|
274
|
+
if (first.type === 'text') {
|
|
275
|
+
const lead = SEPARATOR_LEAD.exec(src.slice(start, first.end));
|
|
276
|
+
if (lead && lead[0].length < first.end - start)
|
|
277
|
+
start += lead[0].length;
|
|
278
|
+
}
|
|
279
|
+
if (last.type === 'text') {
|
|
280
|
+
const trail = SEPARATOR_TRAIL.exec(src.slice(Math.max(last.start, start), end));
|
|
281
|
+
if (trail && end - trail[0].length > start)
|
|
282
|
+
end -= trail[0].length;
|
|
283
|
+
}
|
|
284
|
+
const core = src.slice(start, end);
|
|
285
|
+
const line = lineAt(src, textItems.find((it) => hasLetters(src.slice(it.start, it.end))).start + (src.slice(textItems[0].start, textItems[0].end).length - src.slice(textItems[0].start, textItems[0].end).trimStart().length));
|
|
286
|
+
const vars = items.filter((it) => it.type === 'var');
|
|
287
|
+
const multiline = core.includes('\n');
|
|
288
|
+
if (!vars.length && !hasTags && !multiline) {
|
|
289
|
+
const tag = translateTag(core);
|
|
290
|
+
if (tag) {
|
|
291
|
+
addHardcoded(core.replace(/%/g, '%%'), start, end, tag, line);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// blocktranslate: bind every non-trivial variable to a plain name.
|
|
296
|
+
const bindings = [];
|
|
297
|
+
const nameFor = new Map();
|
|
298
|
+
const allExprs = [
|
|
299
|
+
...vars.map((v) => v.expr),
|
|
300
|
+
...items.filter((it) => it.type === 'open').flatMap((it) => [...src.slice(it.start, it.end).matchAll(/\{\{(.*?)\}\}/g)].map((m) => m[1].trim())),
|
|
301
|
+
];
|
|
302
|
+
for (const expr of allExprs) {
|
|
303
|
+
if (nameFor.has(expr))
|
|
304
|
+
continue;
|
|
305
|
+
if (/^[A-Za-z_]\w*$/.test(expr)) {
|
|
306
|
+
nameFor.set(expr, expr);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const base = bindingName(expr);
|
|
310
|
+
let name = base;
|
|
311
|
+
for (let n = 2; [...nameFor.values()].includes(name) || bindings.some((b) => b.name === name); n++)
|
|
312
|
+
name = `${base}${n}`;
|
|
313
|
+
nameFor.set(expr, name);
|
|
314
|
+
bindings.push({ name, expr });
|
|
315
|
+
}
|
|
316
|
+
if (allExprs.some((e) => /_\(/.test(e))) {
|
|
317
|
+
unwired.push({ line, text: core, reason: 'not wrapped: a variable inside the sentence uses _() — wrap it yourself' });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Content with variables replaced by their bound names; msgid with %(name)s.
|
|
321
|
+
let content = '';
|
|
322
|
+
let msgidRaw = '';
|
|
323
|
+
let cursor = start;
|
|
324
|
+
const pieces = items.flatMap((it) => {
|
|
325
|
+
if (it.type !== 'open')
|
|
326
|
+
return [it];
|
|
327
|
+
// Variables inside an inline tag's attributes.
|
|
328
|
+
const tagText = src.slice(it.start, it.end);
|
|
329
|
+
const sub = [];
|
|
330
|
+
let at = it.start;
|
|
331
|
+
for (const m of tagText.matchAll(/\{\{(.*?)\}\}/g)) {
|
|
332
|
+
const s = it.start + m.index;
|
|
333
|
+
if (s > at)
|
|
334
|
+
sub.push({ type: 'text', start: at, end: s });
|
|
335
|
+
sub.push({ type: 'var', start: s, end: s + m[0].length, expr: m[1].trim() });
|
|
336
|
+
at = s + m[0].length;
|
|
337
|
+
}
|
|
338
|
+
if (at < it.end)
|
|
339
|
+
sub.push({ type: 'text', start: at, end: it.end });
|
|
340
|
+
return sub;
|
|
341
|
+
});
|
|
342
|
+
for (const it of pieces) {
|
|
343
|
+
const s = Math.max(it.start, cursor);
|
|
344
|
+
const e = Math.min(it.end, end);
|
|
345
|
+
if (e <= s)
|
|
346
|
+
continue;
|
|
347
|
+
if (it.type === 'var') {
|
|
348
|
+
const name = nameFor.get(it.expr);
|
|
349
|
+
content += `{{ ${name} }}`;
|
|
350
|
+
msgidRaw += `%(${name})s`;
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
const t = src.slice(s, e);
|
|
354
|
+
content += t;
|
|
355
|
+
msgidRaw += t.replace(/%/g, '%%');
|
|
356
|
+
}
|
|
357
|
+
cursor = e;
|
|
358
|
+
}
|
|
359
|
+
const msgid = multiline ? trimWhitespace(msgidRaw) : msgidRaw;
|
|
360
|
+
const withPart = bindings.length ? ` with ${bindings.map((b) => `${b.name}=${b.expr}`).join(' ')}` : '';
|
|
361
|
+
const replacement = `{% blocktranslate${withPart}${multiline ? ' trimmed' : ''} %}${content}{% endblocktranslate %}`;
|
|
362
|
+
addHardcoded(msgid, start, end, replacement, line);
|
|
363
|
+
};
|
|
364
|
+
// ── Main scan ───────────────────────────────────────────────
|
|
365
|
+
let i = 0;
|
|
366
|
+
let textStart = 0;
|
|
367
|
+
const pushText = (s, e) => {
|
|
368
|
+
if (e > s && !skipStack.length)
|
|
369
|
+
run.push({ type: 'text', start: s, end: e });
|
|
370
|
+
};
|
|
371
|
+
while (i < src.length) {
|
|
372
|
+
const ch = src[i];
|
|
373
|
+
if (ch === '{' && (src[i + 1] === '%' || src[i + 1] === '{' || src[i + 1] === '#')) {
|
|
374
|
+
const end = dtlEnd(src, i);
|
|
375
|
+
if (end === -1) {
|
|
376
|
+
i++;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
pushText(textStart, i);
|
|
380
|
+
const token = src.slice(i, end);
|
|
381
|
+
const inner = token.slice(2, -2).trim();
|
|
382
|
+
if (src[i + 1] === '#') {
|
|
383
|
+
const c = inner;
|
|
384
|
+
if (/^translators\b/i.test(c))
|
|
385
|
+
pendingComment = { text: c, end };
|
|
386
|
+
flush();
|
|
387
|
+
i = textStart = end;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (src[i + 1] === '{') {
|
|
391
|
+
scanUnderscore(token, i);
|
|
392
|
+
scanFilterDefaults(token, i);
|
|
393
|
+
if (!skipStack.length) {
|
|
394
|
+
if (/_\(/.test(inner))
|
|
395
|
+
flush();
|
|
396
|
+
else
|
|
397
|
+
run.push({ type: 'var', start: i, end, expr: inner });
|
|
398
|
+
}
|
|
399
|
+
i = textStart = end;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// {% … %}
|
|
403
|
+
flush();
|
|
404
|
+
const bits = smartSplit(inner);
|
|
405
|
+
const tagName = bits[0] ?? '';
|
|
406
|
+
if (tagName === 'comment') {
|
|
407
|
+
const close = src.slice(end).search(/\{%\s*endcomment\s*%\}/);
|
|
408
|
+
i = textStart = close === -1 ? src.length : end + close + src.slice(end + close).indexOf('%}') + 2;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (tagName === 'verbatim') {
|
|
412
|
+
const close = src.slice(end).search(/\{%\s*endverbatim\b[^%]*%\}/);
|
|
413
|
+
i = textStart = close === -1 ? src.length : end + close + src.slice(end + close).indexOf('%}') + 2;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (tagName === 'trans' || tagName === 'translate') {
|
|
417
|
+
const v = bits[1] ? literalValue(bits[1]) : null;
|
|
418
|
+
if (v !== null) {
|
|
419
|
+
const ctxIdx = bits.indexOf('context');
|
|
420
|
+
const ctx = ctxIdx !== -1 && bits[ctxIdx + 1] ? literalValue(bits[ctxIdx + 1]) ?? undefined : undefined;
|
|
421
|
+
addWrapped(v.replace(/%/g, '%%'), lineAt(src, i), { ctx, comments: comment(i) });
|
|
422
|
+
}
|
|
423
|
+
i = textStart = end;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (tagName === 'blocktrans' || tagName === 'blocktranslate') {
|
|
427
|
+
const r = parseBlockTranslate(src, i, end, bits);
|
|
428
|
+
if (r.msgid)
|
|
429
|
+
addWrapped(r.msgid, lineAt(src, i), { plural: r.plural, ctx: r.ctx, comments: comment(i) });
|
|
430
|
+
i = textStart = r.end;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
scanUnderscore(token, i);
|
|
434
|
+
i = textStart = end;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (ch === '<') {
|
|
438
|
+
if (src.startsWith('<!--', i)) {
|
|
439
|
+
pushText(textStart, i);
|
|
440
|
+
flush();
|
|
441
|
+
const close = src.indexOf('-->', i + 4);
|
|
442
|
+
i = textStart = close === -1 ? src.length : close + 3;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const m = /^<\/?([A-Za-z][\w:-]*)/.exec(src.slice(i, i + 64));
|
|
446
|
+
if (!m && src[i + 1] !== '!') {
|
|
447
|
+
i++;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const end = htmlTagEnd(src, i);
|
|
451
|
+
if (end === -1) {
|
|
452
|
+
i++;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
pushText(textStart, i);
|
|
456
|
+
const tagText = src.slice(i, end);
|
|
457
|
+
if (!m) {
|
|
458
|
+
// <!DOCTYPE …>
|
|
459
|
+
flush();
|
|
460
|
+
i = textStart = end;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const name = m[1].toLowerCase();
|
|
464
|
+
const closing = tagText[1] === '/';
|
|
465
|
+
if (closing) {
|
|
466
|
+
if (skipStack.length && skipStack[skipStack.length - 1] === name) {
|
|
467
|
+
skipStack.pop();
|
|
468
|
+
i = textStart = end;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (!skipStack.length && INLINE.has(name))
|
|
472
|
+
run.push({ type: 'close', start: i, end, name });
|
|
473
|
+
else
|
|
474
|
+
flush();
|
|
475
|
+
i = textStart = end;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const attrsStart = i + m[0].length;
|
|
479
|
+
const attrs = parseAttrs(src, attrsStart, end - 1);
|
|
480
|
+
const selfClosing = /\/\s*>$/.test(tagText) || VOID.has(name);
|
|
481
|
+
let hasTextAttr = false;
|
|
482
|
+
if (!skipStack.length) {
|
|
483
|
+
for (const a of attrs) {
|
|
484
|
+
const isText = TEXT_ATTRS.has(a.name) ||
|
|
485
|
+
(a.name === 'label' && ['option', 'optgroup', 'track'].includes(name)) ||
|
|
486
|
+
(a.name === 'value' && name === 'input' && attrs.some((x) => x.name === 'type' && /^(?:submit|button|reset)$/i.test(x.value)));
|
|
487
|
+
if (!isText || !hasLetters(a.value))
|
|
488
|
+
continue;
|
|
489
|
+
hasTextAttr = true;
|
|
490
|
+
const line = lineAt(src, a.valueStart);
|
|
491
|
+
if (/\{%|\{\{/.test(a.value)) {
|
|
492
|
+
if (!/\{%\s*(?:trans|translate|blocktrans|blocktranslate)\b/.test(a.value)) {
|
|
493
|
+
unwired.push({ line, text: a.value, reason: `not wrapped: ${a.name} attribute mixes text and template tags` });
|
|
494
|
+
}
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const prefer = a.quote === '"' ? "'" : '"';
|
|
498
|
+
// Inside a quoted attribute, the tag can't reuse the attribute's own quote.
|
|
499
|
+
const tag = translateTag(a.value, prefer, !!a.quote);
|
|
500
|
+
if (!tag) {
|
|
501
|
+
unwired.push({ line, text: a.value, reason: `not wrapped: ${a.name} attribute contains both quote characters or a backslash` });
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const replacement = a.quote ? tag : `"${tag}"`;
|
|
505
|
+
addHardcoded(a.value.replace(/%/g, '%%'), a.valueStart, a.valueEnd, replacement, line);
|
|
506
|
+
}
|
|
507
|
+
// <html lang="en"> → follow the active language (and its direction).
|
|
508
|
+
if (name === 'html') {
|
|
509
|
+
const lang = attrs.find((a) => a.name === 'lang');
|
|
510
|
+
const dir = attrs.find((a) => a.name === 'dir');
|
|
511
|
+
if (lang && /^[A-Za-z]{2,3}(?:[-_][A-Za-z0-9]+)*$/.test(lang.value) && lang.quote) {
|
|
512
|
+
extraEdits.push({
|
|
513
|
+
kind: 'template', start: i, end: i,
|
|
514
|
+
replacement: '{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}',
|
|
515
|
+
});
|
|
516
|
+
extraEdits.push({ kind: 'template', start: lang.valueStart, end: lang.valueEnd, replacement: '{{ LANGUAGE_CODE }}' });
|
|
517
|
+
if (!dir) {
|
|
518
|
+
const q = lang.quote;
|
|
519
|
+
extraEdits.push({
|
|
520
|
+
kind: 'template', start: lang.valueEnd + 1, end: lang.valueEnd + 1,
|
|
521
|
+
replacement: ` dir=${q}{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}${q}`,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (skipStack.length) {
|
|
528
|
+
if (!selfClosing && SKIP_CONTENT.has(name))
|
|
529
|
+
skipStack.push(name);
|
|
530
|
+
i = textStart = end;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (RAW_TEXT.has(name) && !selfClosing) {
|
|
534
|
+
flush();
|
|
535
|
+
// Raw text: only DTL tokens inside count (for already-marked strings).
|
|
536
|
+
const closeRe = new RegExp(`</${name}\\s*>`, 'i');
|
|
537
|
+
const rel = src.slice(end).search(closeRe);
|
|
538
|
+
const contentEnd = rel === -1 ? src.length : end + rel;
|
|
539
|
+
const body = src.slice(end, contentEnd);
|
|
540
|
+
for (const t of body.matchAll(/\{%.*?%\}|\{\{.*?\}\}/g)) {
|
|
541
|
+
const off = end + t.index;
|
|
542
|
+
const bits = smartSplit(t[0].slice(2, -2).trim());
|
|
543
|
+
if (t[0].startsWith('{%') && (bits[0] === 'trans' || bits[0] === 'translate')) {
|
|
544
|
+
const v = bits[1] ? literalValue(bits[1]) : null;
|
|
545
|
+
if (v !== null)
|
|
546
|
+
addWrapped(v.replace(/%/g, '%%'), lineAt(src, off));
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
scanUnderscore(t[0], off);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
i = textStart = contentEnd;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (SKIP_CONTENT.has(name) && !selfClosing) {
|
|
556
|
+
flush();
|
|
557
|
+
skipStack.push(name);
|
|
558
|
+
i = textStart = end;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (INLINE.has(name) && !hasTextAttr && !/\{%/.test(tagText)) {
|
|
562
|
+
run.push({ type: 'open', start: i, end, name, attrsHaveVars: /\{\{/.test(tagText) });
|
|
563
|
+
if (selfClosing)
|
|
564
|
+
run.push({ type: 'close', start: end, end, name });
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
flush();
|
|
568
|
+
}
|
|
569
|
+
i = textStart = end;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
i++;
|
|
573
|
+
}
|
|
574
|
+
pushText(textStart, src.length);
|
|
575
|
+
flush();
|
|
576
|
+
findings.sort((a, b) => a.line - b.line);
|
|
577
|
+
return { findings, unwired, extraEdits };
|
|
578
|
+
}
|
|
579
|
+
function matchingClose(items, openIdx) {
|
|
580
|
+
let depth = 0;
|
|
581
|
+
for (let k = openIdx; k < items.length; k++) {
|
|
582
|
+
const it = items[k];
|
|
583
|
+
if (it.type === 'open')
|
|
584
|
+
depth++;
|
|
585
|
+
else if (it.type === 'close') {
|
|
586
|
+
depth--;
|
|
587
|
+
if (depth === 0)
|
|
588
|
+
return k;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return -1;
|
|
592
|
+
}
|
|
593
|
+
function isBalanced(items) {
|
|
594
|
+
const stack = [];
|
|
595
|
+
for (const it of items) {
|
|
596
|
+
if (it.type === 'open')
|
|
597
|
+
stack.push(it.name);
|
|
598
|
+
else if (it.type === 'close') {
|
|
599
|
+
if (stack.pop() !== it.name)
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return stack.length === 0;
|
|
604
|
+
}
|
|
605
|
+
function depthAt(items, idx) {
|
|
606
|
+
let depth = 0;
|
|
607
|
+
for (let k = 0; k < idx; k++) {
|
|
608
|
+
if (items[k].type === 'open')
|
|
609
|
+
depth++;
|
|
610
|
+
else if (items[k].type === 'close')
|
|
611
|
+
depth--;
|
|
612
|
+
}
|
|
613
|
+
return depth;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Parse `{% blocktranslate … %}…{% plural %}…{% endblocktranslate %}` into
|
|
617
|
+
* the msgid Django looks up: text with `%` doubled and `{{ name }}` as
|
|
618
|
+
* `%(name)s`, trimmed when the tag says so.
|
|
619
|
+
*/
|
|
620
|
+
function parseBlockTranslate(src, tagStart, tagEnd, bits) {
|
|
621
|
+
const trimmed = bits.includes('trimmed');
|
|
622
|
+
const ctxIdx = bits.indexOf('context');
|
|
623
|
+
const ctx = ctxIdx !== -1 && bits[ctxIdx + 1] ? literalValue(bits[ctxIdx + 1]) ?? undefined : undefined;
|
|
624
|
+
const isCount = bits.includes('count');
|
|
625
|
+
let singular = '';
|
|
626
|
+
let plural = '';
|
|
627
|
+
let inPlural = false;
|
|
628
|
+
let k = tagEnd;
|
|
629
|
+
let valid = true;
|
|
630
|
+
while (k < src.length) {
|
|
631
|
+
const next = src.slice(k).search(/\{[%{#]/);
|
|
632
|
+
if (next === -1) {
|
|
633
|
+
valid = false;
|
|
634
|
+
k = src.length;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
const at = k + next;
|
|
638
|
+
const text = src.slice(k, at);
|
|
639
|
+
const e = dtlEnd(src, at);
|
|
640
|
+
if (e === -1) {
|
|
641
|
+
if (inPlural)
|
|
642
|
+
plural += src.slice(k, at + 2).replace(/%/g, '%%');
|
|
643
|
+
else
|
|
644
|
+
singular += src.slice(k, at + 2).replace(/%/g, '%%');
|
|
645
|
+
k = at + 2;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (inPlural)
|
|
649
|
+
plural += text.replace(/%/g, '%%');
|
|
650
|
+
else
|
|
651
|
+
singular += text.replace(/%/g, '%%');
|
|
652
|
+
const token = src.slice(at, e);
|
|
653
|
+
const inner = token.slice(2, -2).trim();
|
|
654
|
+
if (token.startsWith('{{')) {
|
|
655
|
+
const piece = `%(${inner})s`;
|
|
656
|
+
if (inPlural)
|
|
657
|
+
plural += piece;
|
|
658
|
+
else
|
|
659
|
+
singular += piece;
|
|
660
|
+
}
|
|
661
|
+
else if (token.startsWith('{%')) {
|
|
662
|
+
const name = inner.split(/\s+/)[0];
|
|
663
|
+
if (name === 'plural')
|
|
664
|
+
inPlural = true;
|
|
665
|
+
else if (name === 'endblocktrans' || name === 'endblocktranslate') {
|
|
666
|
+
k = e;
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
valid = false; // other tags aren't allowed inside blocktranslate
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
k = e;
|
|
674
|
+
}
|
|
675
|
+
if (!valid)
|
|
676
|
+
return { msgid: '', end: k };
|
|
677
|
+
const fix = (s) => (trimmed ? trimWhitespace(s) : s);
|
|
678
|
+
void tagStart;
|
|
679
|
+
return { msgid: fix(singular), plural: isCount && inPlural ? fix(plural) : undefined, ctx, end: k };
|
|
680
|
+
}
|