@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,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal ICU MessageFormat parser and validator for translated messages.
|
|
4
|
+
*
|
|
5
|
+
* ICU messages can't be tokenized like simple placeholders: a translation
|
|
6
|
+
* legitimately changes their structure (Polish needs `few`/`many` plural
|
|
7
|
+
* branches English doesn't have). So instead of protecting them, we let the
|
|
8
|
+
* model rewrite the whole message and then check the result is still a valid
|
|
9
|
+
* message with the same arguments and selector branches.
|
|
10
|
+
*
|
|
11
|
+
* Covers what UI strings use: simple and formatted arguments, plural,
|
|
12
|
+
* selectordinal (with offset and =n keys), select, `#`, and apostrophe
|
|
13
|
+
* quoting. Not a full ICU implementation (no nested formatting skeletons).
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.IcuParseError = void 0;
|
|
17
|
+
exports.parseIcu = parseIcu;
|
|
18
|
+
exports.isIcuMessage = isIcuMessage;
|
|
19
|
+
exports.compareIcuMessages = compareIcuMessages;
|
|
20
|
+
exports.isCountLikeName = isCountLikeName;
|
|
21
|
+
exports.countArguments = countArguments;
|
|
22
|
+
exports.extractSimpleArguments = extractSimpleArguments;
|
|
23
|
+
exports.requiredPluralCategories = requiredPluralCategories;
|
|
24
|
+
class IcuParseError extends Error {
|
|
25
|
+
}
|
|
26
|
+
exports.IcuParseError = IcuParseError;
|
|
27
|
+
function parseIcu(message) {
|
|
28
|
+
let pos = 0;
|
|
29
|
+
const fail = (msg) => {
|
|
30
|
+
throw new IcuParseError(`${msg} at position ${pos}`);
|
|
31
|
+
};
|
|
32
|
+
const skipSpace = () => {
|
|
33
|
+
while (pos < message.length && /\s/.test(message[pos]))
|
|
34
|
+
pos++;
|
|
35
|
+
};
|
|
36
|
+
const readIdentifier = () => {
|
|
37
|
+
skipSpace();
|
|
38
|
+
const start = pos;
|
|
39
|
+
while (pos < message.length && /[^\s,{}#]/.test(message[pos]))
|
|
40
|
+
pos++;
|
|
41
|
+
if (pos === start)
|
|
42
|
+
fail('Expected identifier');
|
|
43
|
+
return message.slice(start, pos);
|
|
44
|
+
};
|
|
45
|
+
const parseMessage = (inPlural, depth) => {
|
|
46
|
+
const nodes = [];
|
|
47
|
+
let text = '';
|
|
48
|
+
const flush = () => {
|
|
49
|
+
if (text)
|
|
50
|
+
nodes.push({ type: 'text', value: text });
|
|
51
|
+
text = '';
|
|
52
|
+
};
|
|
53
|
+
while (pos < message.length) {
|
|
54
|
+
const ch = message[pos];
|
|
55
|
+
if (ch === "'") {
|
|
56
|
+
// '' is a literal apostrophe; '{…}' / '#…' quotes syntax characters.
|
|
57
|
+
if (message[pos + 1] === "'") {
|
|
58
|
+
text += "'";
|
|
59
|
+
pos += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const next = message[pos + 1];
|
|
63
|
+
if (next === '{' || next === '}' || (inPlural && next === '#')) {
|
|
64
|
+
const end = message.indexOf("'", pos + 1);
|
|
65
|
+
if (end === -1)
|
|
66
|
+
fail('Unterminated quote');
|
|
67
|
+
text += message.slice(pos + 1, end);
|
|
68
|
+
pos = end + 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
text += ch;
|
|
72
|
+
pos++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === '#' && inPlural) {
|
|
76
|
+
flush();
|
|
77
|
+
nodes.push({ type: 'pound' });
|
|
78
|
+
pos++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '}') {
|
|
82
|
+
if (depth === 0)
|
|
83
|
+
fail('Unbalanced }');
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
if (ch === '{') {
|
|
87
|
+
flush();
|
|
88
|
+
nodes.push(parseArgument(depth));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
text += ch;
|
|
92
|
+
pos++;
|
|
93
|
+
}
|
|
94
|
+
flush();
|
|
95
|
+
return nodes;
|
|
96
|
+
};
|
|
97
|
+
const parseArgument = (depth) => {
|
|
98
|
+
pos++; // {
|
|
99
|
+
const name = readIdentifier();
|
|
100
|
+
skipSpace();
|
|
101
|
+
if (message[pos] === '}') {
|
|
102
|
+
pos++;
|
|
103
|
+
return { type: 'argument', name };
|
|
104
|
+
}
|
|
105
|
+
if (message[pos] !== ',')
|
|
106
|
+
fail('Expected , or }');
|
|
107
|
+
pos++;
|
|
108
|
+
const kind = readIdentifier();
|
|
109
|
+
skipSpace();
|
|
110
|
+
if (kind === 'plural' || kind === 'selectordinal' || kind === 'select') {
|
|
111
|
+
if (message[pos] !== ',')
|
|
112
|
+
fail(`Expected , after ${kind}`);
|
|
113
|
+
pos++;
|
|
114
|
+
const options = {};
|
|
115
|
+
skipSpace();
|
|
116
|
+
if (kind !== 'select' && message.startsWith('offset:', pos)) {
|
|
117
|
+
pos += 'offset:'.length;
|
|
118
|
+
readIdentifier();
|
|
119
|
+
}
|
|
120
|
+
while (true) {
|
|
121
|
+
skipSpace();
|
|
122
|
+
if (message[pos] === '}') {
|
|
123
|
+
pos++;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
if (pos >= message.length)
|
|
127
|
+
fail(`Unterminated ${kind}`);
|
|
128
|
+
const key = readIdentifier();
|
|
129
|
+
skipSpace();
|
|
130
|
+
if (message[pos] !== '{')
|
|
131
|
+
fail(`Expected { after option "${key}"`);
|
|
132
|
+
if (key in options)
|
|
133
|
+
fail(`Duplicate option "${key}"`);
|
|
134
|
+
pos++;
|
|
135
|
+
options[key] = parseMessage(kind !== 'select', depth + 1);
|
|
136
|
+
if (message[pos] !== '}')
|
|
137
|
+
fail(`Unterminated option "${key}"`);
|
|
138
|
+
pos++;
|
|
139
|
+
}
|
|
140
|
+
if (Object.keys(options).length === 0)
|
|
141
|
+
fail(`${kind} has no options`);
|
|
142
|
+
return { type: kind, name, options };
|
|
143
|
+
}
|
|
144
|
+
// Formatted argument: {price, number} / {d, date, short} — keep the rest verbatim.
|
|
145
|
+
let format = kind;
|
|
146
|
+
if (message[pos] === ',') {
|
|
147
|
+
const end = message.indexOf('}', pos);
|
|
148
|
+
if (end === -1)
|
|
149
|
+
fail('Unterminated argument');
|
|
150
|
+
format += message.slice(pos, end);
|
|
151
|
+
pos = end;
|
|
152
|
+
}
|
|
153
|
+
skipSpace();
|
|
154
|
+
if (message[pos] !== '}')
|
|
155
|
+
fail('Expected }');
|
|
156
|
+
pos++;
|
|
157
|
+
return { type: 'argument', name, format };
|
|
158
|
+
};
|
|
159
|
+
const nodes = parseMessage(false, 0);
|
|
160
|
+
if (pos < message.length)
|
|
161
|
+
fail('Unexpected }');
|
|
162
|
+
return nodes;
|
|
163
|
+
}
|
|
164
|
+
/** True when the message uses plural/select/selectordinal syntax. */
|
|
165
|
+
function isIcuMessage(message) {
|
|
166
|
+
return /\{\s*[\w.]+\s*,\s*(?:plural|select|selectordinal)\s*,/.test(message);
|
|
167
|
+
}
|
|
168
|
+
function collect(nodes, args, selectors, texts) {
|
|
169
|
+
for (const node of nodes) {
|
|
170
|
+
if (node.type === 'text')
|
|
171
|
+
texts.push(node.value);
|
|
172
|
+
else if (node.type === 'argument')
|
|
173
|
+
args.add(node.name);
|
|
174
|
+
else if (node.type !== 'pound') {
|
|
175
|
+
args.add(node.name);
|
|
176
|
+
selectors.push({ kind: node.type, name: node.name, keys: Object.keys(node.options), branches: Object.values(node.options) });
|
|
177
|
+
for (const branch of Object.values(node.options))
|
|
178
|
+
collect(branch, args, selectors, texts);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const CLDR_ORDER = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
183
|
+
/** Plural categories for a locale, always in CLDR order (runtimes differ in ordering). */
|
|
184
|
+
function pluralCategories(locale, type) {
|
|
185
|
+
try {
|
|
186
|
+
const found = new Set(new Intl.PluralRules(locale, { type }).resolvedOptions().pluralCategories);
|
|
187
|
+
return CLDR_ORDER.filter((c) => found.has(c));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [...CLDR_ORDER];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Compare a translated ICU message to its source. Errors mean the message is
|
|
195
|
+
* broken at runtime (unparseable, arguments renamed, select branches changed,
|
|
196
|
+
* no `other`). Missing target plural categories are only a warning: ICU
|
|
197
|
+
* falls back to `other` for them.
|
|
198
|
+
*/
|
|
199
|
+
function compareIcuMessages(source, translated, targetLocale, options = {}) {
|
|
200
|
+
let sourceAst;
|
|
201
|
+
try {
|
|
202
|
+
sourceAst = parseIcu(source);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Source isn't valid ICU itself; nothing meaningful to compare.
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
let targetAst;
|
|
209
|
+
try {
|
|
210
|
+
targetAst = parseIcu(translated);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
return [{ severity: 'error', code: 'icu_invalid', message: `Translation is not a valid ICU message: ${err.message}` }];
|
|
214
|
+
}
|
|
215
|
+
const issues = [];
|
|
216
|
+
const srcArgs = new Set();
|
|
217
|
+
const tgtArgs = new Set();
|
|
218
|
+
const srcSelectors = [];
|
|
219
|
+
const tgtSelectors = [];
|
|
220
|
+
collect(sourceAst, srcArgs, srcSelectors, []);
|
|
221
|
+
collect(targetAst, tgtArgs, tgtSelectors, []);
|
|
222
|
+
const missingArgs = [...srcArgs].filter((a) => !tgtArgs.has(a));
|
|
223
|
+
const extraArgs = [...tgtArgs].filter((a) => !srcArgs.has(a));
|
|
224
|
+
if (missingArgs.length || extraArgs.length) {
|
|
225
|
+
issues.push({
|
|
226
|
+
severity: 'error',
|
|
227
|
+
code: 'icu_arguments_changed',
|
|
228
|
+
message: [
|
|
229
|
+
missingArgs.length ? `missing {${missingArgs.join('}, {')}}` : '',
|
|
230
|
+
extraArgs.length ? `unknown {${extraArgs.join('}, {')}}` : '',
|
|
231
|
+
].filter(Boolean).join('; '),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// A plural/select dropped from the translation (e.g. "{count} fichiers")
|
|
235
|
+
// still renders, but wrongly for every count but one.
|
|
236
|
+
for (const src of srcSelectors) {
|
|
237
|
+
if (!tgtSelectors.some((t) => t.name === src.name && t.kind === src.kind)) {
|
|
238
|
+
issues.push({
|
|
239
|
+
severity: 'error',
|
|
240
|
+
code: 'icu_selector_removed',
|
|
241
|
+
message: `{${src.name}, ${src.kind}} was removed from the translation`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// A translation may add a plural over a simple argument of the source:
|
|
246
|
+
// "{files} files" → Polish "{files, plural, one {# plik} few {# pliki} …}".
|
|
247
|
+
// Only a cardinal plural, and every branch must still show the number.
|
|
248
|
+
for (const target of tgtSelectors) {
|
|
249
|
+
if (srcSelectors.some((s) => s.name === target.name && s.kind === target.kind))
|
|
250
|
+
continue;
|
|
251
|
+
if (target.kind !== 'plural') {
|
|
252
|
+
issues.push({ severity: 'error', code: 'icu_selector_added', message: `{${target.name}, ${target.kind}} is not in the source; only a plural may be added` });
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!(options.pluralizable ?? []).includes(target.name)) {
|
|
256
|
+
issues.push({ severity: 'error', code: 'icu_selector_added', message: `{${target.name}} is not a number; don't add a plural over it` });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const lost = target.keys.filter((k, i) => !mentions(target.branches[i], target.name));
|
|
260
|
+
if (lost.length) {
|
|
261
|
+
issues.push({
|
|
262
|
+
severity: 'error',
|
|
263
|
+
code: 'icu_branch_lost_argument',
|
|
264
|
+
message: `{${target.name}, plural} branches [${lost.join(', ')}] don't show the number (use # or {${target.name}})`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const target of tgtSelectors) {
|
|
269
|
+
if (!target.keys.includes('other')) {
|
|
270
|
+
issues.push({ severity: 'error', code: 'icu_missing_other', message: `{${target.name}, ${target.kind}} has no "other" branch` });
|
|
271
|
+
}
|
|
272
|
+
if (target.kind === 'select') {
|
|
273
|
+
const source = srcSelectors.find((s) => s.kind === 'select' && s.name === target.name);
|
|
274
|
+
if (source && [...source.keys].sort().join('|') !== [...target.keys].sort().join('|')) {
|
|
275
|
+
issues.push({
|
|
276
|
+
severity: 'error',
|
|
277
|
+
code: 'icu_select_keys_changed',
|
|
278
|
+
message: `{${target.name}, select} branches changed from [${source.keys.join(', ')}] to [${target.keys.join(', ')}]`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const allowed = pluralCategories(targetLocale, target.kind === 'selectordinal' ? 'ordinal' : 'cardinal');
|
|
284
|
+
const invalid = target.keys.filter((k) => !/^=\d+$/.test(k) && !allowed.includes(k));
|
|
285
|
+
if (invalid.length) {
|
|
286
|
+
issues.push({
|
|
287
|
+
severity: 'error',
|
|
288
|
+
code: 'icu_plural_category_invalid',
|
|
289
|
+
message: `{${target.name}, ${target.kind}} uses categories [${invalid.join(', ')}] that ${targetLocale} doesn't have (valid: ${allowed.join(', ')})`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const missing = allowed.filter((c) => !target.keys.includes(c));
|
|
293
|
+
if (missing.length) {
|
|
294
|
+
issues.push({
|
|
295
|
+
severity: 'warning',
|
|
296
|
+
code: 'icu_plural_category_missing',
|
|
297
|
+
message: `{${target.name}, ${target.kind}} has no [${missing.join(', ')}] branch for ${targetLocale}; "other" will be used`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return issues;
|
|
302
|
+
}
|
|
303
|
+
/** Whether a plural branch shows its number: `#`, or `{name}` (also inside nested selectors). */
|
|
304
|
+
function mentions(nodes, name) {
|
|
305
|
+
return nodes.some((n) => n.type === 'pound' ||
|
|
306
|
+
(n.type === 'argument' && n.name === name) ||
|
|
307
|
+
((n.type === 'plural' || n.type === 'select' || n.type === 'selectordinal') && Object.values(n.options).some((b) => mentions(b, name))));
|
|
308
|
+
}
|
|
309
|
+
/** Argument names that read like counts (`count`, `total`, `itemCount`, `items.length` → itemsLength). */
|
|
310
|
+
const COUNT_NAME = /^(?:count|total|num|n|length)$|(?:Count|Length)$/;
|
|
311
|
+
/** Argument names that are a plural noun, i.e. how many of them (`{files}`, `{unreadMessages}`). */
|
|
312
|
+
const COUNTED_NOUNS = new Set([
|
|
313
|
+
'attempts', 'comments', 'credits', 'days', 'documents', 'entries', 'errors', 'events', 'files', 'followers',
|
|
314
|
+
'hours', 'images', 'issues', 'items', 'keys', 'licenses', 'likes', 'members', 'messages', 'minutes',
|
|
315
|
+
'notifications', 'orders', 'pages', 'photos', 'points', 'products', 'projects', 'records', 'results', 'rows',
|
|
316
|
+
'seats', 'seconds', 'steps', 'tasks', 'users', 'views', 'warnings', 'weeks', 'months', 'years', 'people',
|
|
317
|
+
]);
|
|
318
|
+
function isCountLikeName(name) {
|
|
319
|
+
if (COUNT_NAME.test(name))
|
|
320
|
+
return true;
|
|
321
|
+
const last = name.split(/(?=[A-Z])|_/).pop().toLowerCase();
|
|
322
|
+
return COUNTED_NOUNS.has(last);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Simple `{name}` arguments of a non-ICU message that look like counts. A
|
|
326
|
+
* translation may turn such an argument into a plural for languages that
|
|
327
|
+
* inflect words by number, without the source having to.
|
|
328
|
+
*/
|
|
329
|
+
function countArguments(message) {
|
|
330
|
+
// Only ICU-style messages: i18next `{{count}}`, printf and template
|
|
331
|
+
// placeholders keep token protection and its checks.
|
|
332
|
+
if (isIcuMessage(message) || /\{\{|\}\}|%(?:\(\w+\))?[sd]|\$\{/.test(message))
|
|
333
|
+
return [];
|
|
334
|
+
return extractSimpleArguments(message).filter(isCountLikeName);
|
|
335
|
+
}
|
|
336
|
+
/** Single-brace `{name}` arguments (not `{{name}}`). */
|
|
337
|
+
function extractSimpleArguments(message) {
|
|
338
|
+
const names = new Set();
|
|
339
|
+
for (const m of message.matchAll(/(?<!\{)\{\s*([A-Za-z_]\w*)\s*\}(?!\})/g))
|
|
340
|
+
names.add(m[1]);
|
|
341
|
+
return [...names];
|
|
342
|
+
}
|
|
343
|
+
/** CLDR plural categories a translator should produce for this locale. */
|
|
344
|
+
function requiredPluralCategories(locale) {
|
|
345
|
+
return { cardinal: pluralCategories(locale, 'cardinal'), ordinal: pluralCategories(locale, 'ordinal') };
|
|
346
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Span differences of `target` per branch combination (empty when fine). */
|
|
2
|
+
export declare function branchSpanProblems(source: string, target: string, neverTranslate?: string[]): BranchSpanProblem[];
|
|
3
|
+
export interface BranchSpanProblem {
|
|
4
|
+
/** Which branch combination, e.g. "count=other" ("message" when the target has no selectors). */
|
|
5
|
+
branch: string;
|
|
6
|
+
lost: string[];
|
|
7
|
+
added: string[];
|
|
8
|
+
}
|
|
9
|
+
/** "count=other: lost <b>, </b>" */
|
|
10
|
+
export declare function describeBranchSpanProblem(p: BranchSpanProblem): string;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.branchSpanProblems = branchSpanProblems;
|
|
4
|
+
exports.describeBranchSpanProblem = describeBranchSpanProblem;
|
|
5
|
+
/**
|
|
6
|
+
* Per-branch check of the protected spans (tags, URLs, emails, code,
|
|
7
|
+
* never-translate terms) of an ICU translation. Shared by the translate
|
|
8
|
+
* phase and locale QA.
|
|
9
|
+
*
|
|
10
|
+
* Each combination of plural/select branches the translation can render
|
|
11
|
+
* (a "variant") must carry exactly the spans of the matching source variant:
|
|
12
|
+
* same branch keys where the source has them, the source's `other` branch for
|
|
13
|
+
* categories only the target language has, and the whole source when a plain
|
|
14
|
+
* source was turned into a plural. So `<b>` dropped from one branch, or kept
|
|
15
|
+
* in only one branch of an added plural, is caught — not just a span missing
|
|
16
|
+
* from every branch.
|
|
17
|
+
*/
|
|
18
|
+
const icu_message_1 = require("./icu-message");
|
|
19
|
+
const placeholder_protection_1 = require("./placeholder-protection");
|
|
20
|
+
/** Variants beyond this are not expanded (deeply nested selects); the check is skipped. */
|
|
21
|
+
const MAX_VARIANTS = 256;
|
|
22
|
+
class TooManyVariants extends Error {
|
|
23
|
+
}
|
|
24
|
+
function expand(nodes) {
|
|
25
|
+
let variants = [{ path: new Map(), text: '' }];
|
|
26
|
+
for (const node of nodes) {
|
|
27
|
+
if (node.type === 'text') {
|
|
28
|
+
for (const v of variants)
|
|
29
|
+
v.text += node.value;
|
|
30
|
+
}
|
|
31
|
+
else if (node.type === 'pound') {
|
|
32
|
+
for (const v of variants)
|
|
33
|
+
v.text += '#';
|
|
34
|
+
}
|
|
35
|
+
else if (node.type === 'argument') {
|
|
36
|
+
for (const v of variants)
|
|
37
|
+
v.text += `{${node.name}}`;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const next = [];
|
|
41
|
+
for (const v of variants) {
|
|
42
|
+
for (const [key, branch] of Object.entries(node.options)) {
|
|
43
|
+
for (const b of expand(branch)) {
|
|
44
|
+
const path = new Map([...v.path, [node.name, key], ...b.path]);
|
|
45
|
+
next.push({ path, text: v.text + b.text });
|
|
46
|
+
if (next.length > MAX_VARIANTS)
|
|
47
|
+
throw new TooManyVariants();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
variants = next;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return variants;
|
|
55
|
+
}
|
|
56
|
+
function spans(text, neverTranslate) {
|
|
57
|
+
const counts = new Map();
|
|
58
|
+
for (const t of (0, placeholder_protection_1.protectText)(text, neverTranslate, { icu: true }).tokens) {
|
|
59
|
+
const k = t.toLowerCase();
|
|
60
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
61
|
+
}
|
|
62
|
+
return counts;
|
|
63
|
+
}
|
|
64
|
+
/** The source variant a target variant is checked against. */
|
|
65
|
+
function matchSource(source, target) {
|
|
66
|
+
const score = (s) => {
|
|
67
|
+
let exact = 0;
|
|
68
|
+
for (const [name, key] of s.path) {
|
|
69
|
+
const want = target.path.get(name);
|
|
70
|
+
if (want === key)
|
|
71
|
+
exact += 2;
|
|
72
|
+
else if (key !== 'other')
|
|
73
|
+
return -1;
|
|
74
|
+
}
|
|
75
|
+
return exact;
|
|
76
|
+
};
|
|
77
|
+
// Prefer the variant with the most matching branch keys; unmatched selectors fall back to `other`.
|
|
78
|
+
let best = source[0];
|
|
79
|
+
let bestScore = -Infinity;
|
|
80
|
+
for (const s of source) {
|
|
81
|
+
const sc = score(s);
|
|
82
|
+
if (sc > bestScore) {
|
|
83
|
+
best = s;
|
|
84
|
+
bestScore = sc;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return best;
|
|
88
|
+
}
|
|
89
|
+
function label(v) {
|
|
90
|
+
return v.path.size ? [...v.path].map(([n, k]) => `${n}=${k}`).join(', ') : 'message';
|
|
91
|
+
}
|
|
92
|
+
/** Span differences of `target` per branch combination (empty when fine). */
|
|
93
|
+
function branchSpanProblems(source, target, neverTranslate = []) {
|
|
94
|
+
let src;
|
|
95
|
+
let tgt;
|
|
96
|
+
try {
|
|
97
|
+
src = expand((0, icu_message_1.parseIcu)(source));
|
|
98
|
+
tgt = expand((0, icu_message_1.parseIcu)(target));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Unparseable (reported by the ICU checks) or too many branch combinations to expand.
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const problems = new Map();
|
|
105
|
+
for (const t of tgt) {
|
|
106
|
+
const s = matchSource(src, t);
|
|
107
|
+
const want = spans(s.text, neverTranslate);
|
|
108
|
+
const have = spans(t.text, neverTranslate);
|
|
109
|
+
const lost = [];
|
|
110
|
+
const added = [];
|
|
111
|
+
for (const [span, n] of want)
|
|
112
|
+
if ((have.get(span) ?? 0) < n)
|
|
113
|
+
lost.push(span);
|
|
114
|
+
for (const [span, n] of have)
|
|
115
|
+
if ((want.get(span) ?? 0) < n)
|
|
116
|
+
added.push(span);
|
|
117
|
+
if (lost.length || added.length)
|
|
118
|
+
problems.set(label(t), { branch: label(t), lost, added });
|
|
119
|
+
}
|
|
120
|
+
return [...problems.values()];
|
|
121
|
+
}
|
|
122
|
+
/** "count=other: lost <b>, </b>" */
|
|
123
|
+
function describeBranchSpanProblem(p) {
|
|
124
|
+
const parts = [p.lost.length ? `lost ${p.lost.join(', ')}` : '', p.added.length ? `added ${p.added.join(', ')}` : ''].filter(Boolean);
|
|
125
|
+
return `${p.branch}: ${parts.join('; ')}`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* JSX sentences as one message.
|
|
4
|
+
*
|
|
5
|
+
* `<h1>Welcome back, {user}</h1>` used to be extracted as the fragment
|
|
6
|
+
* "Welcome back," and injected as `{t('…')} {user}`. A translation can't move
|
|
7
|
+
* words around a variable it never sees, and a count can't be pluralized. This
|
|
8
|
+
* module turns an element's mixed children — text, `{expressions}` and inline
|
|
9
|
+
* elements — into a single ICU message:
|
|
10
|
+
*
|
|
11
|
+
* <h1>Welcome back, {user}</h1> → "Welcome back, {user}"
|
|
12
|
+
* <p>Hi {user.name}</p> → "Hi {userName}"
|
|
13
|
+
* <p>Total: {format(sum)} today</p> → "Total: {0} today"
|
|
14
|
+
* <p>Read the <Link href="/t">terms</Link></p> → "Read the <link>terms</link>"
|
|
15
|
+
* <p>You have {count} items</p> → "You have {count, plural, one {# item} other {# items}}"
|
|
16
|
+
*
|
|
17
|
+
* The extractor and the injector both call {@link buildJsxMessage} on the same
|
|
18
|
+
* untouched source, so they agree on the text (and so on the extraction id).
|
|
19
|
+
*/
|
|
20
|
+
export interface MessagePlaceholder {
|
|
21
|
+
kind: 'placeholder';
|
|
22
|
+
/** Argument name used in the message and in the values object. */
|
|
23
|
+
name: string;
|
|
24
|
+
expression: t.Expression;
|
|
25
|
+
}
|
|
26
|
+
export interface MessageTag {
|
|
27
|
+
kind: 'tag';
|
|
28
|
+
/** Tag name used in the message (`<link>…</link>`) and as the rich-text key. */
|
|
29
|
+
name: string;
|
|
30
|
+
element: t.JSXElement;
|
|
31
|
+
}
|
|
32
|
+
export interface JsxMessage {
|
|
33
|
+
/** The ICU message, e.g. "Welcome back, {user}". */
|
|
34
|
+
text: string;
|
|
35
|
+
/** Placeholders and tags in order of first appearance (each name once). */
|
|
36
|
+
entries: Array<MessagePlaceholder | MessageTag>;
|
|
37
|
+
/** Source nodes folded into the message (text, literal expressions, inline elements). */
|
|
38
|
+
consumed: t.Node[];
|
|
39
|
+
/** Name of the placeholder turned into an ICU plural, if any. */
|
|
40
|
+
plural?: string;
|
|
41
|
+
}
|
|
42
|
+
export type JsxMessageResult = {
|
|
43
|
+
kind: 'message';
|
|
44
|
+
message: JsxMessage;
|
|
45
|
+
}
|
|
46
|
+
/** Not a sentence: plain text, only expressions, block-level children, … */
|
|
47
|
+
| {
|
|
48
|
+
kind: 'none';
|
|
49
|
+
}
|
|
50
|
+
/** Reads like a sentence but can't be moved safely; extract fragments as before. */
|
|
51
|
+
| {
|
|
52
|
+
kind: 'unsafe';
|
|
53
|
+
reason: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Names that suggest a React node rather than text or a number — such a value
|
|
57
|
+
* can't be a message argument. Matched case-insensitively against the last
|
|
58
|
+
* name segment (`user.avatar`, `statusIcon`, `renderBadge()`). Extendable.
|
|
59
|
+
*/
|
|
60
|
+
export declare const ELEMENT_LIKE_NAMES: RegExp[];
|
|
61
|
+
/** Rendered text of a JSXText node, with JSX's whitespace rules (same as Babel's cleanJSXElementLiteralChild). */
|
|
62
|
+
export declare function cleanJsxText(value: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the expression is very likely a string or number. Message values
|
|
65
|
+
* must be: next-intl throws on a React element, i18next prints [object Object].
|
|
66
|
+
*/
|
|
67
|
+
export declare function isLikelyPrimitive(expr: t.Node): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Build the single message for `node`'s children, or say why there is none.
|
|
70
|
+
* Pure: reads the AST, never changes it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildJsxMessage(node: t.JSXElement | t.JSXFragment, options?: {
|
|
73
|
+
/**
|
|
74
|
+
* Turn "{count} items" into an ICU plural. Only when an LLM translator is
|
|
75
|
+
* configured: the NLLB service can't translate ICU plural messages.
|
|
76
|
+
*/
|
|
77
|
+
plurals?: boolean;
|
|
78
|
+
}): JsxMessageResult;
|
|
79
|
+
/**
|
|
80
|
+
* Text used to name the key: the message without arguments and tags, with a
|
|
81
|
+
* plural's `other` branch standing in for it. "Welcome back, {user}" →
|
|
82
|
+
* "Welcome back,"; "{count, plural, one {# item} other {# items}} left" → "items left".
|
|
83
|
+
*/
|
|
84
|
+
export declare function messageKeyText(message: string): string;
|
|
85
|
+
/** `{ user, userName: user.name, 0: format(x) }` — placeholders only. */
|
|
86
|
+
export declare function placeholderValues(message: JsxMessage): t.ObjectProperty[];
|
|
87
|
+
/** next-intl rich values: `b: (chunks) => <b>{chunks}</b>`. Moves the element into the callback. */
|
|
88
|
+
export declare function richTagValue(tag: MessageTag): t.ObjectProperty;
|
|
89
|
+
/** react-i18next Trans components: `b: <b />`. */
|
|
90
|
+
export declare function transComponent(tag: MessageTag): t.ObjectProperty;
|
|
91
|
+
export declare function hasTags(message: JsxMessage): boolean;
|
|
92
|
+
/** `t("key")` or `t("key", { …values })`. */
|
|
93
|
+
export declare function createMessageTCall(key: string, message: JsxMessage, callee?: t.Expression): t.CallExpression;
|