@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,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* gettext plural forms.
|
|
4
|
+
*
|
|
5
|
+
* A PO file's `Plural-Forms` header says how many msgstr[n] forms a plural
|
|
6
|
+
* entry has and which one a number picks. CLDR (what translators and ICU
|
|
7
|
+
* think in) names categories instead: one, few, many, other. This module
|
|
8
|
+
* maps between the two by evaluating the gettext expression on sample
|
|
9
|
+
* numbers and asking Intl.PluralRules which category each sample is, so the
|
|
10
|
+
* mapping is right even for headers written by other tools.
|
|
11
|
+
*
|
|
12
|
+
* Expressions are evaluated by a small parser — never `eval` — because
|
|
13
|
+
* headers come from repositories Babelize doesn't control.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.PluralFormsError = void 0;
|
|
17
|
+
exports.parsePluralForms = parsePluralForms;
|
|
18
|
+
exports.pluralFormsFor = pluralFormsFor;
|
|
19
|
+
exports.indexCategories = indexCategories;
|
|
20
|
+
/** Plural-Forms for common languages (GNU gettext / Django conventions). */
|
|
21
|
+
const PLURAL_FORMS = {
|
|
22
|
+
// One form
|
|
23
|
+
ja: 'nplurals=1; plural=0;', ko: 'nplurals=1; plural=0;', zh: 'nplurals=1; plural=0;', vi: 'nplurals=1; plural=0;',
|
|
24
|
+
th: 'nplurals=1; plural=0;', id: 'nplurals=1; plural=0;', ms: 'nplurals=1; plural=0;', km: 'nplurals=1; plural=0;',
|
|
25
|
+
lo: 'nplurals=1; plural=0;', my: 'nplurals=1; plural=0;', jv: 'nplurals=1; plural=0;', yo: 'nplurals=1; plural=0;',
|
|
26
|
+
ig: 'nplurals=1; plural=0;', bo: 'nplurals=1; plural=0;', dz: 'nplurals=1; plural=0;', su: 'nplurals=1; plural=0;',
|
|
27
|
+
// Two forms, singular for 0 and 1
|
|
28
|
+
fr: 'nplurals=2; plural=(n > 1);', 'pt-BR': 'nplurals=2; plural=(n > 1);', tr: 'nplurals=2; plural=(n > 1);',
|
|
29
|
+
fa: 'nplurals=2; plural=(n > 1);', am: 'nplurals=2; plural=(n > 1);', ti: 'nplurals=2; plural=(n > 1);',
|
|
30
|
+
ln: 'nplurals=2; plural=(n > 1);', mg: 'nplurals=2; plural=(n > 1);', oc: 'nplurals=2; plural=(n > 1);',
|
|
31
|
+
fil: 'nplurals=2; plural=(n > 1);', tl: 'nplurals=2; plural=(n > 1);', uz: 'nplurals=2; plural=(n > 1);',
|
|
32
|
+
// Special two-form rules
|
|
33
|
+
is: 'nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);',
|
|
34
|
+
mk: 'nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;',
|
|
35
|
+
// Three or more forms
|
|
36
|
+
ru: 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
|
37
|
+
uk: 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
|
38
|
+
be: 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
|
39
|
+
sr: 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
|
40
|
+
hr: 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
|
41
|
+
bs: 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
|
42
|
+
pl: 'nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
|
43
|
+
cs: 'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;',
|
|
44
|
+
sk: 'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;',
|
|
45
|
+
lt: 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
|
46
|
+
lv: 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);',
|
|
47
|
+
ro: 'nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2);',
|
|
48
|
+
sl: 'nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);',
|
|
49
|
+
ga: 'nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);',
|
|
50
|
+
cy: 'nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;',
|
|
51
|
+
mt: 'nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);',
|
|
52
|
+
ar: 'nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);',
|
|
53
|
+
he: 'nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);',
|
|
54
|
+
gd: 'nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;',
|
|
55
|
+
};
|
|
56
|
+
const DEFAULT_TWO = 'nplurals=2; plural=(n != 1);';
|
|
57
|
+
const BINARY_PRECEDENCE = {
|
|
58
|
+
'||': 1, '&&': 2, '==': 3, '!=': 3, '<': 4, '>': 4, '<=': 4, '>=': 4, '+': 5, '-': 5, '*': 6, '/': 6, '%': 6,
|
|
59
|
+
};
|
|
60
|
+
class PluralFormsError extends Error {
|
|
61
|
+
}
|
|
62
|
+
exports.PluralFormsError = PluralFormsError;
|
|
63
|
+
function parseExpression(src) {
|
|
64
|
+
const tokens = src.match(/\d+|n|\|\||&&|==|!=|<=|>=|[-+*/%<>!?:()]/g) ?? [];
|
|
65
|
+
if (tokens.join('') !== src.replace(/\s+/g, ''))
|
|
66
|
+
throw new PluralFormsError(`Unsupported plural expression: ${src}`);
|
|
67
|
+
let pos = 0;
|
|
68
|
+
const peek = () => tokens[pos];
|
|
69
|
+
const take = (expect) => {
|
|
70
|
+
const t = tokens[pos++];
|
|
71
|
+
if (expect !== undefined && t !== expect)
|
|
72
|
+
throw new PluralFormsError(`Expected ${expect} in plural expression`);
|
|
73
|
+
return t;
|
|
74
|
+
};
|
|
75
|
+
const primary = () => {
|
|
76
|
+
const t = take();
|
|
77
|
+
if (t === undefined)
|
|
78
|
+
throw new PluralFormsError('Unexpected end of plural expression');
|
|
79
|
+
if (t === 'n')
|
|
80
|
+
return { t: 'n' };
|
|
81
|
+
if (/^\d+$/.test(t))
|
|
82
|
+
return { t: 'num', v: Number(t) };
|
|
83
|
+
if (t === '(') {
|
|
84
|
+
const e = conditional();
|
|
85
|
+
take(')');
|
|
86
|
+
return e;
|
|
87
|
+
}
|
|
88
|
+
if (t === '!' || t === '-')
|
|
89
|
+
return { t: 'un', op: t, a: primary() };
|
|
90
|
+
throw new PluralFormsError(`Unexpected ${t} in plural expression`);
|
|
91
|
+
};
|
|
92
|
+
const binary = (minPrec) => {
|
|
93
|
+
let left = primary();
|
|
94
|
+
for (;;) {
|
|
95
|
+
const op = peek();
|
|
96
|
+
const prec = op !== undefined ? BINARY_PRECEDENCE[op] : undefined;
|
|
97
|
+
if (prec === undefined || prec < minPrec)
|
|
98
|
+
return left;
|
|
99
|
+
take();
|
|
100
|
+
left = { t: 'bin', op, a: left, b: binary(prec + 1) };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const conditional = () => {
|
|
104
|
+
const c = binary(1);
|
|
105
|
+
if (peek() !== '?')
|
|
106
|
+
return c;
|
|
107
|
+
take('?');
|
|
108
|
+
const a = conditional();
|
|
109
|
+
take(':');
|
|
110
|
+
const b = conditional();
|
|
111
|
+
return { t: 'cond', c, a, b };
|
|
112
|
+
};
|
|
113
|
+
const tree = conditional();
|
|
114
|
+
if (pos !== tokens.length)
|
|
115
|
+
throw new PluralFormsError(`Trailing tokens in plural expression: ${src}`);
|
|
116
|
+
return tree;
|
|
117
|
+
}
|
|
118
|
+
function evaluate(node, n) {
|
|
119
|
+
switch (node.t) {
|
|
120
|
+
case 'n': return n;
|
|
121
|
+
case 'num': return node.v;
|
|
122
|
+
case 'un': return node.op === '!' ? (evaluate(node.a, n) ? 0 : 1) : -evaluate(node.a, n);
|
|
123
|
+
case 'cond': return evaluate(node.c, n) ? evaluate(node.a, n) : evaluate(node.b, n);
|
|
124
|
+
case 'bin': {
|
|
125
|
+
const a = evaluate(node.a, n);
|
|
126
|
+
const b = evaluate(node.b, n);
|
|
127
|
+
switch (node.op) {
|
|
128
|
+
case '||': return a || b ? 1 : 0;
|
|
129
|
+
case '&&': return a && b ? 1 : 0;
|
|
130
|
+
case '==': return a === b ? 1 : 0;
|
|
131
|
+
case '!=': return a !== b ? 1 : 0;
|
|
132
|
+
case '<': return a < b ? 1 : 0;
|
|
133
|
+
case '>': return a > b ? 1 : 0;
|
|
134
|
+
case '<=': return a <= b ? 1 : 0;
|
|
135
|
+
case '>=': return a >= b ? 1 : 0;
|
|
136
|
+
case '+': return a + b;
|
|
137
|
+
case '-': return a - b;
|
|
138
|
+
case '*': return a * b;
|
|
139
|
+
case '/': return b === 0 ? 0 : Math.trunc(a / b);
|
|
140
|
+
case '%': return b === 0 ? 0 : a % b;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
function parsePluralForms(header) {
|
|
147
|
+
const np = /nplurals\s*=\s*(\d+)/.exec(header);
|
|
148
|
+
const pl = /plural\s*=\s*([^;]+);?/.exec(header);
|
|
149
|
+
if (!np || !pl)
|
|
150
|
+
throw new PluralFormsError(`Invalid Plural-Forms header: ${header}`);
|
|
151
|
+
const nplurals = Number(np[1]);
|
|
152
|
+
if (nplurals < 1 || nplurals > 10)
|
|
153
|
+
throw new PluralFormsError(`Unsupported nplurals: ${nplurals}`);
|
|
154
|
+
const tree = parseExpression(pl[1].trim());
|
|
155
|
+
return {
|
|
156
|
+
header: header.trim(),
|
|
157
|
+
nplurals,
|
|
158
|
+
index: (n) => {
|
|
159
|
+
const i = evaluate(tree, n);
|
|
160
|
+
return i >= 0 && i < nplurals ? i : 0;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function baseLanguage(locale) {
|
|
165
|
+
return locale.split(/[-_]/)[0].toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
/** Plural-Forms header for a Babelize locale code. */
|
|
168
|
+
function pluralFormsFor(locale) {
|
|
169
|
+
const exact = Object.keys(PLURAL_FORMS).find((k) => k.toLowerCase() === locale.toLowerCase().replace('_', '-'));
|
|
170
|
+
if (exact)
|
|
171
|
+
return { header: PLURAL_FORMS[exact], known: true };
|
|
172
|
+
const base = baseLanguage(locale);
|
|
173
|
+
if (PLURAL_FORMS[base])
|
|
174
|
+
return { header: PLURAL_FORMS[base], known: true };
|
|
175
|
+
// Derive the common shapes from CLDR when the language isn't in the table.
|
|
176
|
+
try {
|
|
177
|
+
const rules = new Intl.PluralRules(locale);
|
|
178
|
+
const cats = rules.resolvedOptions().pluralCategories;
|
|
179
|
+
if (cats.length === 1)
|
|
180
|
+
return { header: 'nplurals=1; plural=0;', known: true };
|
|
181
|
+
if (cats.length === 2 && cats.includes('one')) {
|
|
182
|
+
const zeroIsOne = rules.select(0) === 'one';
|
|
183
|
+
const onlyOne = [2, 11, 21, 101].every((n) => rules.select(n) === 'other');
|
|
184
|
+
if (onlyOne)
|
|
185
|
+
return { header: zeroIsOne ? 'nplurals=2; plural=(n > 1);' : DEFAULT_TWO, known: true };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// fall through
|
|
190
|
+
}
|
|
191
|
+
return { header: DEFAULT_TWO, known: false };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* CLDR category of each msgstr index for `locale` under `forms`: the
|
|
195
|
+
* category most sample integers that pick the index fall into.
|
|
196
|
+
*/
|
|
197
|
+
function indexCategories(forms, locale) {
|
|
198
|
+
let rules;
|
|
199
|
+
try {
|
|
200
|
+
rules = new Intl.PluralRules(locale);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
rules = new Intl.PluralRules('en');
|
|
204
|
+
}
|
|
205
|
+
const votes = Array.from({ length: forms.nplurals }, () => new Map());
|
|
206
|
+
for (let n = 0; n <= 1000; n++) {
|
|
207
|
+
const i = forms.index(n);
|
|
208
|
+
const cat = rules.select(n);
|
|
209
|
+
votes[i].set(cat, (votes[i].get(cat) ?? 0) + (n <= 200 ? 2 : 1));
|
|
210
|
+
}
|
|
211
|
+
return votes.map((v, i) => {
|
|
212
|
+
if (v.size === 0)
|
|
213
|
+
return i === forms.nplurals - 1 ? 'other' : 'other';
|
|
214
|
+
return [...v.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PO file reading and writing that preserves what it doesn't change.
|
|
3
|
+
*
|
|
4
|
+
* Entries Babelize doesn't touch are written back exactly as they were read
|
|
5
|
+
* (comments, references, flags, wrapping, obsolete `#~` entries), so merging
|
|
6
|
+
* into a hand-maintained catalogue produces a minimal diff. Changed and new
|
|
7
|
+
* entries are serialized the way GNU gettext tools write them.
|
|
8
|
+
*/
|
|
9
|
+
export interface PoEntry {
|
|
10
|
+
translatorComments: string[];
|
|
11
|
+
extractedComments: string[];
|
|
12
|
+
references: string[];
|
|
13
|
+
flags: string[];
|
|
14
|
+
/** `#|` previous-msgid lines, kept verbatim (without the `#| ` prefix). */
|
|
15
|
+
previous: string[];
|
|
16
|
+
msgctxt?: string;
|
|
17
|
+
msgid: string;
|
|
18
|
+
msgidPlural?: string;
|
|
19
|
+
msgstr: string[];
|
|
20
|
+
obsolete: boolean;
|
|
21
|
+
/** Original text of the entry; written back verbatim unless `dirty`. */
|
|
22
|
+
raw?: string;
|
|
23
|
+
dirty: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface PoFile {
|
|
26
|
+
/** Header entry (msgid ""), if present. */
|
|
27
|
+
header?: PoEntry;
|
|
28
|
+
entries: PoEntry[];
|
|
29
|
+
newline: '\n' | '\r\n';
|
|
30
|
+
}
|
|
31
|
+
export declare function entryKey(msgctxt: string | undefined, msgid: string): string;
|
|
32
|
+
export declare function parsePo(content: string): PoFile;
|
|
33
|
+
export declare function serializeEntry(e: PoEntry): string[];
|
|
34
|
+
export declare function serializePo(file: PoFile): string;
|
|
35
|
+
export declare function headerFields(header: PoEntry | undefined): Array<[string, string]>;
|
|
36
|
+
export declare function getHeaderField(header: PoEntry | undefined, name: string): string | undefined;
|
|
37
|
+
/** Set header fields (in place, keeping order; new fields are appended). */
|
|
38
|
+
export declare function setHeaderFields(header: PoEntry, updates: Array<[string, string]>): boolean;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PO file reading and writing that preserves what it doesn't change.
|
|
4
|
+
*
|
|
5
|
+
* Entries Babelize doesn't touch are written back exactly as they were read
|
|
6
|
+
* (comments, references, flags, wrapping, obsolete `#~` entries), so merging
|
|
7
|
+
* into a hand-maintained catalogue produces a minimal diff. Changed and new
|
|
8
|
+
* entries are serialized the way GNU gettext tools write them.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.entryKey = entryKey;
|
|
12
|
+
exports.parsePo = parsePo;
|
|
13
|
+
exports.serializeEntry = serializeEntry;
|
|
14
|
+
exports.serializePo = serializePo;
|
|
15
|
+
exports.headerFields = headerFields;
|
|
16
|
+
exports.getHeaderField = getHeaderField;
|
|
17
|
+
exports.setHeaderFields = setHeaderFields;
|
|
18
|
+
const ESCAPES = { n: '\n', t: '\t', r: '\r', '"': '"', '\\': '\\', a: '\x07', b: '\b', f: '\f', v: '\v' };
|
|
19
|
+
function unquote(line) {
|
|
20
|
+
const m = /^"(.*)"\s*$/.exec(line.trim());
|
|
21
|
+
if (!m)
|
|
22
|
+
throw new Error(`Malformed PO string: ${line}`);
|
|
23
|
+
return m[1].replace(/\\(?:([0-7]{1,3})|x([0-9a-fA-F]{1,2})|(.))/g, (_, oct, hex, ch) => {
|
|
24
|
+
if (oct)
|
|
25
|
+
return String.fromCharCode(parseInt(oct, 8));
|
|
26
|
+
if (hex)
|
|
27
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
28
|
+
return ESCAPES[ch] ?? ch;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function entryKey(msgctxt, msgid) {
|
|
32
|
+
return `${msgctxt ?? ''}\u0004${msgid}`;
|
|
33
|
+
}
|
|
34
|
+
function emptyEntry() {
|
|
35
|
+
return { translatorComments: [], extractedComments: [], references: [], flags: [], previous: [], msgid: '', msgstr: [], obsolete: false, dirty: false };
|
|
36
|
+
}
|
|
37
|
+
function parsePo(content) {
|
|
38
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
39
|
+
const lines = content.replace(/^\uFEFF/, '').split(/\r?\n/);
|
|
40
|
+
const entries = [];
|
|
41
|
+
let cur = emptyEntry();
|
|
42
|
+
let curLines = [];
|
|
43
|
+
let hasKeyword = false;
|
|
44
|
+
/** Which field continuation strings append to. */
|
|
45
|
+
let field = null;
|
|
46
|
+
const finish = () => {
|
|
47
|
+
// Drop trailing blank lines from the raw block.
|
|
48
|
+
while (curLines.length && !curLines[curLines.length - 1].trim())
|
|
49
|
+
curLines.pop();
|
|
50
|
+
if (hasKeyword) {
|
|
51
|
+
cur.raw = curLines.join(newline);
|
|
52
|
+
entries.push(cur);
|
|
53
|
+
}
|
|
54
|
+
else if (curLines.some((l) => l.trim())) {
|
|
55
|
+
// Comments with no entry after them (end of file): keep as a raw block.
|
|
56
|
+
cur.raw = curLines.join(newline);
|
|
57
|
+
cur.obsolete = true;
|
|
58
|
+
cur.msgid = `\u0000orphan-${entries.length}`;
|
|
59
|
+
entries.push(cur);
|
|
60
|
+
}
|
|
61
|
+
cur = emptyEntry();
|
|
62
|
+
curLines = [];
|
|
63
|
+
hasKeyword = false;
|
|
64
|
+
field = null;
|
|
65
|
+
};
|
|
66
|
+
const append = (value) => {
|
|
67
|
+
if (!field)
|
|
68
|
+
return;
|
|
69
|
+
if (field.kind === 'msgctxt')
|
|
70
|
+
cur.msgctxt = (cur.msgctxt ?? '') + value;
|
|
71
|
+
else if (field.kind === 'msgid')
|
|
72
|
+
cur.msgid += value;
|
|
73
|
+
else if (field.kind === 'msgid_plural')
|
|
74
|
+
cur.msgidPlural = (cur.msgidPlural ?? '') + value;
|
|
75
|
+
else
|
|
76
|
+
cur.msgstr[field.index] = (cur.msgstr[field.index] ?? '') + value;
|
|
77
|
+
};
|
|
78
|
+
for (const rawLine of lines) {
|
|
79
|
+
let line = rawLine;
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
if (!trimmed) {
|
|
82
|
+
if (hasKeyword)
|
|
83
|
+
finish();
|
|
84
|
+
else if (curLines.length)
|
|
85
|
+
curLines.push(rawLine);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let obsolete = false;
|
|
89
|
+
if (trimmed.startsWith('#~')) {
|
|
90
|
+
obsolete = true;
|
|
91
|
+
line = trimmed.replace(/^#~\s?/, '');
|
|
92
|
+
if (line.startsWith('|'))
|
|
93
|
+
line = `#${line}`; // #~| previous
|
|
94
|
+
}
|
|
95
|
+
const t = line.trim();
|
|
96
|
+
const isComment = t.startsWith('#');
|
|
97
|
+
const isKeyword = /^(msgctxt|msgid|msgid_plural|msgstr(\[\d+\])?)\s/.test(t);
|
|
98
|
+
// A comment or a new msgctxt/msgid after a complete entry starts a new one.
|
|
99
|
+
if (hasKeyword && (isComment || /^(msgctxt|msgid)\s/.test(t)) && (field?.kind === 'msgstr'))
|
|
100
|
+
finish();
|
|
101
|
+
curLines.push(rawLine);
|
|
102
|
+
if (obsolete)
|
|
103
|
+
cur.obsolete = true;
|
|
104
|
+
if (isComment) {
|
|
105
|
+
if (t.startsWith('#.'))
|
|
106
|
+
cur.extractedComments.push(t.slice(2).trim());
|
|
107
|
+
else if (t.startsWith('#:'))
|
|
108
|
+
cur.references.push(...t.slice(2).trim().split(/\s+/).filter(Boolean));
|
|
109
|
+
else if (t.startsWith('#,'))
|
|
110
|
+
cur.flags.push(...t.slice(2).split(',').map((f) => f.trim()).filter(Boolean));
|
|
111
|
+
else if (t.startsWith('#|'))
|
|
112
|
+
cur.previous.push(t.slice(2).trim());
|
|
113
|
+
else
|
|
114
|
+
cur.translatorComments.push(t.replace(/^#\s?/, ''));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (isKeyword) {
|
|
118
|
+
hasKeyword = true;
|
|
119
|
+
const m = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?\s+(.*)$/.exec(t);
|
|
120
|
+
const kind = m[1];
|
|
121
|
+
const index = m[2] !== undefined ? Number(m[2]) : 0;
|
|
122
|
+
field = { kind, index };
|
|
123
|
+
if (kind === 'msgstr' && cur.msgstr[index] === undefined)
|
|
124
|
+
cur.msgstr[index] = '';
|
|
125
|
+
if (kind === 'msgctxt')
|
|
126
|
+
cur.msgctxt = '';
|
|
127
|
+
append(unquote(m[3]));
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (t.startsWith('"')) {
|
|
131
|
+
append(unquote(t));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Unrecognized PO line: ${rawLine}`);
|
|
135
|
+
}
|
|
136
|
+
finish();
|
|
137
|
+
for (const e of entries)
|
|
138
|
+
e.msgstr = Array.from({ length: e.msgstr.length }, (_, i) => e.msgstr[i] ?? '');
|
|
139
|
+
const headerIdx = entries.findIndex((e) => !e.obsolete && e.msgid === '' && e.msgctxt === undefined);
|
|
140
|
+
const header = headerIdx !== -1 ? entries.splice(headerIdx, 1)[0] : undefined;
|
|
141
|
+
return { header, entries, newline };
|
|
142
|
+
}
|
|
143
|
+
// ── Serialization ────────────────────────────────────────────
|
|
144
|
+
function escapePo(s) {
|
|
145
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n')
|
|
146
|
+
// eslint-disable-next-line no-control-regex -- escaping control characters is the point
|
|
147
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
|
|
148
|
+
}
|
|
149
|
+
/** Split escaped text into pieces of at most `width` chars, breaking after spaces. */
|
|
150
|
+
function wrapEscaped(text, width) {
|
|
151
|
+
const out = [];
|
|
152
|
+
let rest = text;
|
|
153
|
+
while (rest.length > width) {
|
|
154
|
+
let cut = rest.lastIndexOf(' ', width - 1);
|
|
155
|
+
if (cut <= 0) {
|
|
156
|
+
cut = rest.indexOf(' ', width);
|
|
157
|
+
if (cut === -1)
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
// Don't split inside an escape sequence.
|
|
161
|
+
let pieceEnd = cut + 1;
|
|
162
|
+
let backslashes = 0;
|
|
163
|
+
for (let k = pieceEnd - 1; k >= 0 && rest[k] === '\\'; k--)
|
|
164
|
+
backslashes++;
|
|
165
|
+
if (backslashes % 2 === 1)
|
|
166
|
+
pieceEnd++;
|
|
167
|
+
out.push(rest.slice(0, pieceEnd));
|
|
168
|
+
rest = rest.slice(pieceEnd);
|
|
169
|
+
}
|
|
170
|
+
if (rest || out.length === 0)
|
|
171
|
+
out.push(rest);
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
function serializeString(keyword, value, prefix = '') {
|
|
175
|
+
const pieces = value.split(/(?<=\n)/).map(escapePo);
|
|
176
|
+
const single = `${prefix}${keyword} "${pieces.join('')}"`;
|
|
177
|
+
if (pieces.length <= 1 && single.length <= 79)
|
|
178
|
+
return [single];
|
|
179
|
+
const lines = [`${prefix}${keyword} ""`];
|
|
180
|
+
for (const p of pieces)
|
|
181
|
+
for (const w of wrapEscaped(p, 77 - prefix.length))
|
|
182
|
+
lines.push(`${prefix}"${w}"`);
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
function packed(prefix, items) {
|
|
186
|
+
const lines = [];
|
|
187
|
+
let cur = '';
|
|
188
|
+
for (const it of items) {
|
|
189
|
+
if (cur && `${prefix} ${cur} ${it}`.length > 79) {
|
|
190
|
+
lines.push(`${prefix} ${cur}`);
|
|
191
|
+
cur = it;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
cur = cur ? `${cur} ${it}` : it;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (cur)
|
|
198
|
+
lines.push(`${prefix} ${cur}`);
|
|
199
|
+
return lines;
|
|
200
|
+
}
|
|
201
|
+
function serializeEntry(e) {
|
|
202
|
+
const p = e.obsolete ? '#~ ' : '';
|
|
203
|
+
const lines = [];
|
|
204
|
+
for (const c of e.translatorComments)
|
|
205
|
+
lines.push(c ? `# ${c}` : '#');
|
|
206
|
+
for (const c of e.extractedComments)
|
|
207
|
+
lines.push(`#. ${c}`);
|
|
208
|
+
if (e.references.length)
|
|
209
|
+
lines.push(...packed('#:', e.references));
|
|
210
|
+
if (e.flags.length)
|
|
211
|
+
lines.push(`#, ${e.flags.join(', ')}`);
|
|
212
|
+
for (const prev of e.previous)
|
|
213
|
+
lines.push(`#| ${prev}`);
|
|
214
|
+
if (e.msgctxt !== undefined)
|
|
215
|
+
lines.push(...serializeString('msgctxt', e.msgctxt, p));
|
|
216
|
+
lines.push(...serializeString('msgid', e.msgid, p));
|
|
217
|
+
if (e.msgidPlural !== undefined) {
|
|
218
|
+
lines.push(...serializeString('msgid_plural', e.msgidPlural, p));
|
|
219
|
+
e.msgstr.forEach((s, i) => lines.push(...serializeString(`msgstr[${i}]`, s, p)));
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
lines.push(...serializeString('msgstr', e.msgstr[0] ?? '', p));
|
|
223
|
+
}
|
|
224
|
+
return lines;
|
|
225
|
+
}
|
|
226
|
+
function serializePo(file) {
|
|
227
|
+
const blocks = [];
|
|
228
|
+
const nl = file.newline;
|
|
229
|
+
const render = (e) => (!e.dirty && e.raw !== undefined ? e.raw : serializeEntry(e).join(nl));
|
|
230
|
+
if (file.header)
|
|
231
|
+
blocks.push(render(file.header));
|
|
232
|
+
for (const e of file.entries)
|
|
233
|
+
blocks.push(render(e));
|
|
234
|
+
return blocks.join(nl + nl) + nl;
|
|
235
|
+
}
|
|
236
|
+
// ── Header fields ────────────────────────────────────────────
|
|
237
|
+
function headerFields(header) {
|
|
238
|
+
if (!header)
|
|
239
|
+
return [];
|
|
240
|
+
return (header.msgstr[0] ?? '')
|
|
241
|
+
.split('\n')
|
|
242
|
+
.filter((l) => l.includes(':'))
|
|
243
|
+
.map((l) => {
|
|
244
|
+
const i = l.indexOf(':');
|
|
245
|
+
return [l.slice(0, i).trim(), l.slice(i + 1).trim()];
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function getHeaderField(header, name) {
|
|
249
|
+
return headerFields(header).find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];
|
|
250
|
+
}
|
|
251
|
+
/** Set header fields (in place, keeping order; new fields are appended). */
|
|
252
|
+
function setHeaderFields(header, updates) {
|
|
253
|
+
const fields = headerFields(header);
|
|
254
|
+
let changed = false;
|
|
255
|
+
for (const [name, value] of updates) {
|
|
256
|
+
const existing = fields.find(([k]) => k.toLowerCase() === name.toLowerCase());
|
|
257
|
+
if (existing) {
|
|
258
|
+
if (existing[1] !== value) {
|
|
259
|
+
existing[1] = value;
|
|
260
|
+
changed = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
fields.push([name, value]);
|
|
265
|
+
changed = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (changed) {
|
|
269
|
+
header.msgstr = [fields.map(([k, v]) => `${k}: ${v}\n`).join('')];
|
|
270
|
+
header.dirty = true;
|
|
271
|
+
}
|
|
272
|
+
return changed;
|
|
273
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Django Python extraction.
|
|
3
|
+
*
|
|
4
|
+
* Finds two kinds of strings in a .py file:
|
|
5
|
+
*
|
|
6
|
+
* 1. Strings already marked for translation — `_()`, `gettext()`,
|
|
7
|
+
* `gettext_lazy()`, `ngettext()`, `pgettext()`, `npgettext()` and their
|
|
8
|
+
* lazy/legacy variants, under whatever local names the file imports them
|
|
9
|
+
* as. These go into the catalogue as-is.
|
|
10
|
+
*
|
|
11
|
+
* 2. Hardcoded user-facing strings in places Django renders to users, which
|
|
12
|
+
* the injector wraps with gettext / gettext_lazy:
|
|
13
|
+
* - verbose_name, verbose_name_plural, help_text, label, empty_label,
|
|
14
|
+
* empty_value_display keyword arguments; `description=` on
|
|
15
|
+
* @admin.action / @admin.display; `message=` on validators;
|
|
16
|
+
* - labels in `choices` (CHOICES lists, choices=, TextChoices members);
|
|
17
|
+
* - error_messages dicts and widget attrs (placeholder, title, …);
|
|
18
|
+
* - messages.info/success/… and ModelAdmin.message_user() text,
|
|
19
|
+
* ValidationError messages — and string variables passed to them;
|
|
20
|
+
* - text returned by template tags and filters (templatetags/*.py).
|
|
21
|
+
*
|
|
22
|
+
* Everything else (dict keys, identifiers, log lines, URLs, codes) is left
|
|
23
|
+
* alone: wrapping a string that is not shown to users changes behaviour.
|
|
24
|
+
* f-strings become `_("… %(name)s …") % {"name": expr}`; f-strings this can't
|
|
25
|
+
* rewrite safely are reported, not guessed at.
|
|
26
|
+
*/
|
|
27
|
+
import { type DjangoFileAnalysis } from './types';
|
|
28
|
+
export interface PythonExtractOptions {
|
|
29
|
+
/** Relative path of the file (decides template-tag handling). */
|
|
30
|
+
filePath: string;
|
|
31
|
+
}
|
|
32
|
+
/** A string that reads as words, not an identifier, path or code. */
|
|
33
|
+
export declare function looksLikeNaturalLanguage(text: string): boolean;
|
|
34
|
+
export declare function extractDjangoPython(src: string, options: PythonExtractOptions): DjangoFileAnalysis;
|