@babelize/engine 0.0.0-stage → 0.1.0
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/index.d.ts +54 -0
- package/dist/index.js +126 -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/django/django-template-injector.d.ts +29 -0
- package/dist/injection/django/django-template-injector.js +216 -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 +207 -0
- package/dist/local/report.d.ts +177 -0
- package/dist/local/report.js +186 -0
- package/dist/local/translate-core.d.ts +215 -0
- package/dist/local/translate-core.js +461 -0
- package/dist/local/translate-messages.d.ts +49 -0
- package/dist/local/translate-messages.js +91 -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 +701 -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 +13 -0
- package/dist/modules/finalization/django-finalize.js +275 -0
- package/dist/modules/finalization/finalize.d.ts +36 -0
- package/dist/modules/finalization/finalize.js +717 -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 +37 -0
- package/dist/modules/finalization/nextjs-finalize.js +329 -0
- package/dist/modules/finalization/static-runtime.d.ts +9 -0
- package/dist/modules/finalization/static-runtime.js +261 -0
- package/dist/modules/format-injection/index.d.ts +19 -0
- package/dist/modules/format-injection/index.js +272 -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/glossary.d.ts +9 -0
- package/dist/modules/glossary/glossary.js +27 -0
- package/dist/modules/glossary/index.d.ts +3 -0
- package/dist/modules/glossary/index.js +8 -0
- package/dist/modules/glossary/masker.d.ts +23 -0
- package/dist/modules/glossary/masker.js +56 -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 +1066 -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 +116 -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 +107 -0
- package/dist/modules/locale-builder/po.d.ts +33 -0
- package/dist/modules/locale-builder/po.js +131 -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 +21 -0
- package/dist/modules/lockfile/lockfile-manager.d.ts +90 -0
- package/dist/modules/lockfile/lockfile-manager.js +482 -0
- package/dist/modules/lockfile/schema.d.ts +154 -0
- package/dist/modules/lockfile/schema.js +45 -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/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 +38 -0
- package/dist/modules/translator/translate.js +932 -0
- package/dist/modules/translator/translator.d.ts +5 -0
- package/dist/modules/translator/translator.js +46 -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 +298 -0
- package/dist/orchestrator.d.ts +8 -0
- package/dist/orchestrator.js +616 -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 +145 -0
- package/dist/providers/types.js +2 -0
- package/dist/qa/check-project.d.ts +49 -0
- package/dist/qa/check-project.js +150 -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 +325 -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 +36 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/types.d.ts +363 -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/dtl-mask.d.ts +25 -0
- package/dist/utils/dtl-mask.js +58 -0
- package/dist/utils/extraction-options.d.ts +23 -0
- package/dist/utils/extraction-options.js +23 -0
- package/dist/utils/file-utils.d.ts +33 -0
- package/dist/utils/file-utils.js +262 -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 +621 -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 +80 -0
- package/dist/utils/metadata.js +255 -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 +36 -0
- package/dist/utils/placeholder-protection.d.ts +57 -0
- package/dist/utils/placeholder-protection.js +136 -0
- package/dist/utils/pseudo-markers.d.ts +9 -0
- package/dist/utils/pseudo-markers.js +16 -0
- package/dist/utils/py-extractor.d.ts +14 -0
- package/dist/utils/py-extractor.js +285 -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 +10 -0
- package/dist/utils/translation-sanitizer.js +57 -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 +293 -0
- package/dist/utils/ui-copy.d.ts +32 -0
- package/dist/utils/ui-copy.js +126 -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,621 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ELEMENT_LIKE_NAMES = void 0;
|
|
40
|
+
exports.cleanJsxText = cleanJsxText;
|
|
41
|
+
exports.isLikelyPrimitive = isLikelyPrimitive;
|
|
42
|
+
exports.buildJsxMessage = buildJsxMessage;
|
|
43
|
+
exports.messageKeyText = messageKeyText;
|
|
44
|
+
exports.placeholderValues = placeholderValues;
|
|
45
|
+
exports.richTagValue = richTagValue;
|
|
46
|
+
exports.transComponent = transComponent;
|
|
47
|
+
exports.hasTags = hasTags;
|
|
48
|
+
exports.createMessageTCall = createMessageTCall;
|
|
49
|
+
const generator_1 = __importDefault(require("@babel/generator"));
|
|
50
|
+
const t = __importStar(require("@babel/types"));
|
|
51
|
+
const string_utils_1 = require("./string-utils");
|
|
52
|
+
/** Phrasing elements that live inside a sentence. Anything else is block-level. */
|
|
53
|
+
const INLINE_TAGS = new Set([
|
|
54
|
+
'a', 'abbr', 'b', 'bdi', 'bdo', 'cite', 'code', 'data', 'del', 'dfn', 'em', 'i', 'ins', 'kbd',
|
|
55
|
+
'mark', 'q', 's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var',
|
|
56
|
+
]);
|
|
57
|
+
/** react-i18next's Trans, and the alias the injector uses when `Trans` is taken. */
|
|
58
|
+
const TRANS_NAMES = new Set(['Trans', 'BabelizeTrans']);
|
|
59
|
+
/** Elements whose text must not be rewritten as a whole. */
|
|
60
|
+
const OPAQUE_PARENTS = new Set(['code', 'pre', 'script', 'style', 'svg', 'textarea']);
|
|
61
|
+
/**
|
|
62
|
+
* i18next reads these from the options object of `t(key, options)`, so a
|
|
63
|
+
* placeholder can't use them as its name.
|
|
64
|
+
*/
|
|
65
|
+
const RESERVED_NAMES = new Set([
|
|
66
|
+
'context', 'defaultValue', 'fallbackLng', 'interpolation', 'joinArrays', 'keySeparator', 'lng', 'lngs',
|
|
67
|
+
'ns', 'nsSeparator', 'ordinal', 'postProcess', 'replace', 'returnDetails', 'returnObjects', 'skipInterpolation',
|
|
68
|
+
]);
|
|
69
|
+
const COUNT_NAME = /^(?:count|total|num|n|length)$|(?:Count|Length)$/;
|
|
70
|
+
/** Words ending in "s" that are not regular plural nouns. */
|
|
71
|
+
const NOT_PLURAL_NOUNS = new Set([
|
|
72
|
+
'is', 'was', 'has', 'does', 'this', 'its', 'his', 'hers', 'ours', 'yours', 'theirs', 'us', 'yes', 'plus',
|
|
73
|
+
'minus', 'thus', 'always', 'perhaps', 'across', 'besides', 'towards', 'news', 'series', 'species',
|
|
74
|
+
]);
|
|
75
|
+
/** "-ies" plurals whose singular ends in "y". Other "-ies" words are only handled when listed below. */
|
|
76
|
+
const IES_TO_Y = new Set([
|
|
77
|
+
'activities', 'batteries', 'bodies', 'categories', 'cities', 'communities', 'companies', 'copies', 'countries',
|
|
78
|
+
'currencies', 'deliveries', 'dependencies', 'directories', 'entries', 'factories', 'families', 'galleries',
|
|
79
|
+
'histories', 'inquiries', 'libraries', 'parties', 'penalties', 'policies', 'priorities', 'properties', 'queries',
|
|
80
|
+
'replies', 'repositories', 'stories', 'strategies', 'studies', 'subsidiaries', 'summaries', 'technologies',
|
|
81
|
+
'territories', 'universities', 'utilities', 'varieties',
|
|
82
|
+
]);
|
|
83
|
+
/** "-ies" plurals that just drop the "s". */
|
|
84
|
+
const IES_DROP_S = new Set([
|
|
85
|
+
'brownies', 'calories', 'cookies', 'goalies', 'hippies', 'lies', 'movies', 'pies', 'rookies', 'selfies',
|
|
86
|
+
'smoothies', 'ties', 'zombies',
|
|
87
|
+
]);
|
|
88
|
+
/**
|
|
89
|
+
* Names that suggest a React node rather than text or a number — such a value
|
|
90
|
+
* can't be a message argument. Matched case-insensitively against the last
|
|
91
|
+
* name segment (`user.avatar`, `statusIcon`, `renderBadge()`). Extendable.
|
|
92
|
+
*/
|
|
93
|
+
exports.ELEMENT_LIKE_NAMES = [
|
|
94
|
+
/^(?:icon|badge|avatar|children|component|content|slot)$/i,
|
|
95
|
+
/(?:icon|badge|element|node|component)$/i,
|
|
96
|
+
/^render/i,
|
|
97
|
+
];
|
|
98
|
+
/** Calls that return text: formatters and conversions. */
|
|
99
|
+
const TEXT_CALLS = [/^format/i, /^to\w*String$/, /^toFixed$/, /^toPrecision$/, /^toLocale/, /^String$/, /^Number$/, /^t$/];
|
|
100
|
+
/** Words that may follow a counted noun without agreeing with it. */
|
|
101
|
+
const SAFE_AFTER_NOUN = new Set([
|
|
102
|
+
'in', 'on', 'at', 'for', 'from', 'to', 'of', 'with', 'by', 'per', 'left', 'remaining', 'selected', 'found',
|
|
103
|
+
'added', 'removed', 'total', 'ago', 'available', 'pending', 'unread', 'online', 'away', 'and', 'or', 'so', 'out',
|
|
104
|
+
]);
|
|
105
|
+
/** Rendered text of a JSXText node, with JSX's whitespace rules (same as Babel's cleanJSXElementLiteralChild). */
|
|
106
|
+
function cleanJsxText(value) {
|
|
107
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
108
|
+
let lastNonEmptyLine = 0;
|
|
109
|
+
for (let i = 0; i < lines.length; i++)
|
|
110
|
+
if (/[^ \t]/.test(lines[i]))
|
|
111
|
+
lastNonEmptyLine = i;
|
|
112
|
+
let out = '';
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
const isFirst = i === 0;
|
|
115
|
+
const isLast = i === lines.length - 1;
|
|
116
|
+
let line = lines[i].replace(/\t/g, ' ');
|
|
117
|
+
if (!isFirst)
|
|
118
|
+
line = line.replace(/^[ ]+/, '');
|
|
119
|
+
if (!isLast)
|
|
120
|
+
line = line.replace(/[ ]+$/, '');
|
|
121
|
+
if (line) {
|
|
122
|
+
if (i !== lastNonEmptyLine)
|
|
123
|
+
line += ' ';
|
|
124
|
+
out += line;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
class NotASentence extends Error {
|
|
130
|
+
}
|
|
131
|
+
const code = (node) => (0, generator_1.default)(node, { compact: true, comments: false }).code;
|
|
132
|
+
function lowerFirst(s) {
|
|
133
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
134
|
+
}
|
|
135
|
+
function isSafeName(name) {
|
|
136
|
+
return /^[A-Za-z][A-Za-z0-9_]*$/.test(name);
|
|
137
|
+
}
|
|
138
|
+
/** `user` → user, `user.name` → userName, `props.user.name` → userName; anything else → null. */
|
|
139
|
+
function placeholderNameFor(expr) {
|
|
140
|
+
if (t.isIdentifier(expr))
|
|
141
|
+
return isSafeName(expr.name) ? expr.name : null;
|
|
142
|
+
const parts = [];
|
|
143
|
+
let cur = expr;
|
|
144
|
+
while (t.isMemberExpression(cur) || t.isOptionalMemberExpression(cur)) {
|
|
145
|
+
if (cur.computed || !t.isIdentifier(cur.property))
|
|
146
|
+
return null;
|
|
147
|
+
parts.unshift(cur.property.name);
|
|
148
|
+
cur = cur.object;
|
|
149
|
+
}
|
|
150
|
+
if (t.isThisExpression(cur)) {
|
|
151
|
+
// this.props.x / this.state.x
|
|
152
|
+
}
|
|
153
|
+
else if (t.isIdentifier(cur)) {
|
|
154
|
+
parts.unshift(cur.name);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
while (parts.length > 1 && (parts[0] === 'props' || parts[0] === 'state'))
|
|
160
|
+
parts.shift();
|
|
161
|
+
if (parts.length === 0)
|
|
162
|
+
return null;
|
|
163
|
+
const name = parts.map((p, i) => (i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1))).join('');
|
|
164
|
+
return isSafeName(name) ? name : null;
|
|
165
|
+
}
|
|
166
|
+
/** `t('…')`, `t.rich(…)`, `i18n.t(…)`, `$t(…)` — already translated. */
|
|
167
|
+
function isTranslationCall(node) {
|
|
168
|
+
if (!t.isCallExpression(node))
|
|
169
|
+
return false;
|
|
170
|
+
const callee = node.callee;
|
|
171
|
+
if (t.isIdentifier(callee))
|
|
172
|
+
return callee.name === 't' || callee.name === '$t';
|
|
173
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
174
|
+
if (callee.property.name === 't')
|
|
175
|
+
return true;
|
|
176
|
+
return t.isIdentifier(callee.object) && callee.object.name === 't';
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
/** Walk an expression (not into nested functions' bodies for side effects). */
|
|
181
|
+
function scanExpression(expr) {
|
|
182
|
+
const found = {};
|
|
183
|
+
const visit = (node, inFunction) => {
|
|
184
|
+
if (!node || typeof node !== 'object')
|
|
185
|
+
return;
|
|
186
|
+
if (isTranslationCall(node))
|
|
187
|
+
found.translated = true;
|
|
188
|
+
if (t.isJSXElement(node) || t.isJSXFragment(node))
|
|
189
|
+
found.jsx = true;
|
|
190
|
+
if (!inFunction) {
|
|
191
|
+
if (t.isAssignmentExpression(node))
|
|
192
|
+
found.sideEffect ?? (found.sideEffect = 'an assignment');
|
|
193
|
+
else if (t.isUpdateExpression(node))
|
|
194
|
+
found.sideEffect ?? (found.sideEffect = `${node.operator}`);
|
|
195
|
+
else if (t.isAwaitExpression(node))
|
|
196
|
+
found.sideEffect ?? (found.sideEffect = 'await');
|
|
197
|
+
else if (t.isYieldExpression(node))
|
|
198
|
+
found.sideEffect ?? (found.sideEffect = 'yield');
|
|
199
|
+
else if (t.isUnaryExpression(node) && node.operator === 'delete')
|
|
200
|
+
found.sideEffect ?? (found.sideEffect = 'delete');
|
|
201
|
+
}
|
|
202
|
+
const fn = inFunction || t.isFunction(node);
|
|
203
|
+
for (const key of t.VISITOR_KEYS[node.type] ?? []) {
|
|
204
|
+
const child = node[key];
|
|
205
|
+
if (Array.isArray(child))
|
|
206
|
+
child.forEach((c) => visit(c, fn));
|
|
207
|
+
else
|
|
208
|
+
visit(child, fn);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
visit(expr, false);
|
|
212
|
+
return found;
|
|
213
|
+
}
|
|
214
|
+
function calleeName(callee) {
|
|
215
|
+
if (t.isIdentifier(callee))
|
|
216
|
+
return callee.name;
|
|
217
|
+
if ((t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) && !callee.computed && t.isIdentifier(callee.property))
|
|
218
|
+
return callee.property.name;
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
/** Name of an identifier / member expression's last segment (`user.avatar` → avatar). */
|
|
222
|
+
function lastSegment(expr) {
|
|
223
|
+
if (t.isIdentifier(expr))
|
|
224
|
+
return expr.name;
|
|
225
|
+
if ((t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) && !expr.computed && t.isIdentifier(expr.property))
|
|
226
|
+
return expr.property.name;
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const looksLikeElement = (name) => exports.ELEMENT_LIKE_NAMES.some((re) => re.test(name));
|
|
230
|
+
/**
|
|
231
|
+
* Whether the expression is very likely a string or number. Message values
|
|
232
|
+
* must be: next-intl throws on a React element, i18next prints [object Object].
|
|
233
|
+
*/
|
|
234
|
+
function isLikelyPrimitive(expr) {
|
|
235
|
+
if (t.isTSNonNullExpression(expr) || t.isTSAsExpression(expr) || t.isTSSatisfiesExpression(expr) || t.isParenthesizedExpression(expr)) {
|
|
236
|
+
return isLikelyPrimitive(expr.expression);
|
|
237
|
+
}
|
|
238
|
+
if (t.isNumericLiteral(expr) || t.isStringLiteral(expr) || t.isTemplateLiteral(expr))
|
|
239
|
+
return true;
|
|
240
|
+
if (t.isIdentifier(expr) || t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) {
|
|
241
|
+
const name = lastSegment(expr);
|
|
242
|
+
return !!name && !looksLikeElement(name);
|
|
243
|
+
}
|
|
244
|
+
if (t.isCallExpression(expr) || t.isOptionalCallExpression(expr)) {
|
|
245
|
+
const name = calleeName(expr.callee);
|
|
246
|
+
return !!name && TEXT_CALLS.some((re) => re.test(name));
|
|
247
|
+
}
|
|
248
|
+
if (t.isBinaryExpression(expr)) {
|
|
249
|
+
if (['-', '*', '/', '%', '**'].includes(expr.operator))
|
|
250
|
+
return true;
|
|
251
|
+
return expr.operator === '+' && isLikelyPrimitive(expr.left) && isLikelyPrimitive(expr.right);
|
|
252
|
+
}
|
|
253
|
+
if (t.isUnaryExpression(expr))
|
|
254
|
+
return expr.operator === '-' || expr.operator === '+';
|
|
255
|
+
if (t.isConditionalExpression(expr))
|
|
256
|
+
return isLikelyPrimitive(expr.consequent) && isLikelyPrimitive(expr.alternate);
|
|
257
|
+
if (t.isLogicalExpression(expr))
|
|
258
|
+
return expr.operator !== '&&' && isLikelyPrimitive(expr.left) && isLikelyPrimitive(expr.right);
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
/** A plain `count` / `cart.items.length` — no optional chain, call or fallback that could make it undefined. */
|
|
262
|
+
function isPlainReference(expr) {
|
|
263
|
+
if (t.isIdentifier(expr))
|
|
264
|
+
return true;
|
|
265
|
+
return t.isMemberExpression(expr) && !expr.computed && t.isIdentifier(expr.property) && isPlainReference(expr.object);
|
|
266
|
+
}
|
|
267
|
+
/** `children` / `props.children`: React nodes, not message values. */
|
|
268
|
+
function isChildren(expr) {
|
|
269
|
+
if (t.isIdentifier(expr))
|
|
270
|
+
return expr.name === 'children';
|
|
271
|
+
return (t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) && !expr.computed && t.isIdentifier(expr.property) && expr.property.name === 'children';
|
|
272
|
+
}
|
|
273
|
+
function elementName(el) {
|
|
274
|
+
const n = el.openingElement.name;
|
|
275
|
+
if (t.isJSXIdentifier(n))
|
|
276
|
+
return { intrinsic: /^[a-z]/.test(n.name), name: n.name };
|
|
277
|
+
if (t.isJSXMemberExpression(n))
|
|
278
|
+
return { intrinsic: false, name: n.property.name };
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function hasContent(children) {
|
|
282
|
+
return children.some((c) => !(t.isJSXText(c) && cleanJsxText(c.value) === '') && !(t.isJSXExpressionContainer(c) && t.isJSXEmptyExpression(c.expression) && !c.expression.innerComments?.length));
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Build the single message for `node`'s children, or say why there is none.
|
|
286
|
+
* Pure: reads the AST, never changes it.
|
|
287
|
+
*/
|
|
288
|
+
function buildJsxMessage(node, options = {}) {
|
|
289
|
+
if (t.isJSXElement(node)) {
|
|
290
|
+
const own = elementName(node);
|
|
291
|
+
if (!own || (own.intrinsic && OPAQUE_PARENTS.has(own.name)) || TRANS_NAMES.has(own.name))
|
|
292
|
+
return { kind: 'none' };
|
|
293
|
+
}
|
|
294
|
+
// Cheap pre-check: a sentence has more than one child, not all of them text.
|
|
295
|
+
const kids = node.children;
|
|
296
|
+
if (kids.length < 2 || kids.every((c) => t.isJSXText(c)))
|
|
297
|
+
return { kind: 'none' };
|
|
298
|
+
const entries = [];
|
|
299
|
+
const consumed = [];
|
|
300
|
+
const phByCode = new Map();
|
|
301
|
+
const tagByCode = new Map();
|
|
302
|
+
const usedNames = new Set();
|
|
303
|
+
let complexIndex = 0;
|
|
304
|
+
let unsafe;
|
|
305
|
+
let voidElement;
|
|
306
|
+
let placeholderCount = 0;
|
|
307
|
+
let tagCount = 0;
|
|
308
|
+
const uniqueName = (base) => {
|
|
309
|
+
let name = base;
|
|
310
|
+
for (let n = 2; usedNames.has(name); n++)
|
|
311
|
+
name = `${base}${n}`;
|
|
312
|
+
usedNames.add(name);
|
|
313
|
+
return name;
|
|
314
|
+
};
|
|
315
|
+
const addPlaceholder = (expr) => {
|
|
316
|
+
const simple = placeholderNameFor(expr);
|
|
317
|
+
const exprCode = code(expr);
|
|
318
|
+
if (simple) {
|
|
319
|
+
const existing = phByCode.get(exprCode);
|
|
320
|
+
if (existing)
|
|
321
|
+
return existing;
|
|
322
|
+
}
|
|
323
|
+
let name;
|
|
324
|
+
if (simple) {
|
|
325
|
+
name = uniqueName(RESERVED_NAMES.has(simple) ? `${simple}Value` : simple);
|
|
326
|
+
phByCode.set(exprCode, name);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
while (usedNames.has(String(complexIndex)))
|
|
330
|
+
complexIndex++;
|
|
331
|
+
name = String(complexIndex++);
|
|
332
|
+
usedNames.add(name);
|
|
333
|
+
}
|
|
334
|
+
entries.push({ kind: 'placeholder', name, expression: expr });
|
|
335
|
+
return name;
|
|
336
|
+
};
|
|
337
|
+
const walk = (children, depth) => {
|
|
338
|
+
const pieces = [];
|
|
339
|
+
for (const child of children) {
|
|
340
|
+
if (t.isJSXText(child)) {
|
|
341
|
+
const text = cleanJsxText(child.value);
|
|
342
|
+
consumed.push(child);
|
|
343
|
+
if (text)
|
|
344
|
+
pieces.push({ type: 'text', value: text });
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
348
|
+
const expr = child.expression;
|
|
349
|
+
if (t.isJSXEmptyExpression(expr)) {
|
|
350
|
+
if (expr.innerComments?.length)
|
|
351
|
+
unsafe ?? (unsafe = 'it contains a {/* comment */}');
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (t.isStringLiteral(expr)) {
|
|
355
|
+
consumed.push(expr);
|
|
356
|
+
pieces.push({ type: 'text', value: expr.value });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (t.isTemplateLiteral(expr) && expr.expressions.length === 0) {
|
|
360
|
+
consumed.push(expr);
|
|
361
|
+
pieces.push({ type: 'text', value: expr.quasis[0].value.cooked ?? expr.quasis[0].value.raw });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const scan = scanExpression(expr);
|
|
365
|
+
if (scan.translated)
|
|
366
|
+
throw new NotASentence();
|
|
367
|
+
placeholderCount++;
|
|
368
|
+
if (scan.sideEffect)
|
|
369
|
+
unsafe ?? (unsafe = `{${code(expr)}} has a side effect (${scan.sideEffect})`);
|
|
370
|
+
else if (scan.jsx)
|
|
371
|
+
unsafe ?? (unsafe = `{${code(expr)}} renders elements`);
|
|
372
|
+
else if (isChildren(expr))
|
|
373
|
+
unsafe ?? (unsafe = `{${code(expr)}} passes children through`);
|
|
374
|
+
else if (!isLikelyPrimitive(expr))
|
|
375
|
+
unsafe ?? (unsafe = `{${code(expr)}} may be an element, not text or a number`);
|
|
376
|
+
if (unsafe)
|
|
377
|
+
continue;
|
|
378
|
+
pieces.push({ type: 'ph', name: addPlaceholder(expr) });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (t.isJSXElement(child)) {
|
|
382
|
+
const info = elementName(child);
|
|
383
|
+
if (!info || (info.intrinsic && !INLINE_TAGS.has(info.name)) || TRANS_NAMES.has(info.name))
|
|
384
|
+
throw new NotASentence();
|
|
385
|
+
if (!hasContent(child.children)) {
|
|
386
|
+
voidElement ?? (voidElement = `<${info.name} />`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const spread = child.openingElement.attributes.find((a) => t.isJSXSpreadAttribute(a));
|
|
390
|
+
if (spread) {
|
|
391
|
+
if (depth === 0)
|
|
392
|
+
tagCount++;
|
|
393
|
+
unsafe ?? (unsafe = `<${info.name}> spreads props`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
for (const attr of child.openingElement.attributes) {
|
|
397
|
+
if (t.isJSXAttribute(attr) && t.isJSXExpressionContainer(attr.value) && !t.isJSXEmptyExpression(attr.value.expression)) {
|
|
398
|
+
const scan = scanExpression(attr.value.expression);
|
|
399
|
+
if (scan.sideEffect)
|
|
400
|
+
unsafe ?? (unsafe = `an attribute of <${info.name}> has a side effect (${scan.sideEffect})`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (depth === 0)
|
|
404
|
+
tagCount++;
|
|
405
|
+
consumed.push(child);
|
|
406
|
+
const inner = walk(child.children, depth + 1);
|
|
407
|
+
const shell = code({ ...child.openingElement, selfClosing: true });
|
|
408
|
+
let name = tagByCode.get(shell);
|
|
409
|
+
if (!name) {
|
|
410
|
+
name = uniqueName(lowerFirst(info.name).replace(/[^A-Za-z0-9]/g, '') || 'tag');
|
|
411
|
+
tagByCode.set(shell, name);
|
|
412
|
+
entries.push({ kind: 'tag', name, element: child });
|
|
413
|
+
}
|
|
414
|
+
pieces.push({ type: 'tag', name, children: inner });
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
// Fragments and {...spread} children: leave the element alone.
|
|
418
|
+
throw new NotASentence();
|
|
419
|
+
}
|
|
420
|
+
return pieces;
|
|
421
|
+
};
|
|
422
|
+
let pieces;
|
|
423
|
+
try {
|
|
424
|
+
pieces = walk(node.children, 0);
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
if (e instanceof NotASentence)
|
|
428
|
+
return { kind: 'none' };
|
|
429
|
+
throw e;
|
|
430
|
+
}
|
|
431
|
+
const literal = collectText(pieces).replace(/\s+/g, ' ').trim();
|
|
432
|
+
if (!(0, string_utils_1.isTranslatableText)(literal) || (0, string_utils_1.containsPII)(literal))
|
|
433
|
+
return { kind: 'none' };
|
|
434
|
+
const meaningful = pieces.filter((p) => p.type !== 'text' || p.value.trim() !== '');
|
|
435
|
+
const isSentence = placeholderCount + tagCount > 0 && (meaningful.length >= 2 || placeholderCount > 0);
|
|
436
|
+
if (!isSentence)
|
|
437
|
+
return { kind: 'none' };
|
|
438
|
+
if (unsafe)
|
|
439
|
+
return { kind: 'unsafe', reason: unsafe };
|
|
440
|
+
if (voidElement)
|
|
441
|
+
return { kind: 'unsafe', reason: `${voidElement} has no text to wrap` };
|
|
442
|
+
if (meaningful.length < 2)
|
|
443
|
+
return { kind: 'none' };
|
|
444
|
+
if (/[{}<>]/.test(collectText(pieces))) {
|
|
445
|
+
return { kind: 'unsafe', reason: 'the text contains { } < or >, which are message syntax' };
|
|
446
|
+
}
|
|
447
|
+
const plural = options.plurals === false ? undefined : makePlural(pieces, entries);
|
|
448
|
+
// JSX text was already cleaned by the JSX rules; collapse leftover runs of
|
|
449
|
+
// ASCII whitespace (HTML renders them as one space) but keep and the
|
|
450
|
+
// spaces inside inline elements. Outer edge spaces are kept by the injector.
|
|
451
|
+
const text = serialize(pieces).replace(/[ \t\r\n]+/g, ' ').replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, '');
|
|
452
|
+
return { kind: 'message', message: { text, entries, consumed, plural } };
|
|
453
|
+
}
|
|
454
|
+
function collectText(pieces) {
|
|
455
|
+
return pieces.map((p) => (p.type === 'text' ? p.value : p.type === 'tag' ? collectText(p.children) : ' ')).join('');
|
|
456
|
+
}
|
|
457
|
+
function serialize(pieces) {
|
|
458
|
+
let out = '';
|
|
459
|
+
pieces.forEach((p, i) => {
|
|
460
|
+
if (p.type === 'text') {
|
|
461
|
+
let value = p.value;
|
|
462
|
+
// An apostrophe right before `{` or `<` would start an ICU quote.
|
|
463
|
+
if (value.endsWith("'") && i + 1 < pieces.length && pieces[i + 1].type !== 'text')
|
|
464
|
+
value += "'";
|
|
465
|
+
out += value;
|
|
466
|
+
}
|
|
467
|
+
else if (p.type === 'ph') {
|
|
468
|
+
out += `{${p.name}}`;
|
|
469
|
+
}
|
|
470
|
+
else if (p.type === 'plural') {
|
|
471
|
+
out += `{${p.name}, plural, one {# ${p.one}} other {# ${p.other}}}`;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
const inner = serialize(p.children);
|
|
475
|
+
out += `<${p.name}>${inner}</${p.name}>`;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
function singular(word) {
|
|
481
|
+
if (word.length < 3 || !/^[a-z]+s$/.test(word) || NOT_PLURAL_NOUNS.has(word))
|
|
482
|
+
return null;
|
|
483
|
+
if (/(?:ss|us|is)$/.test(word))
|
|
484
|
+
return null;
|
|
485
|
+
if (word.endsWith('ies')) {
|
|
486
|
+
if (IES_TO_Y.has(word))
|
|
487
|
+
return word.slice(0, -3) + 'y';
|
|
488
|
+
if (IES_DROP_S.has(word))
|
|
489
|
+
return word.slice(0, -1);
|
|
490
|
+
return null; // unsure: keep a plain placeholder
|
|
491
|
+
}
|
|
492
|
+
if (/(?:sses|shes|ches|xes|zes)$/.test(word))
|
|
493
|
+
return word.slice(0, -2);
|
|
494
|
+
return word.slice(0, -1);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* `{count} items` → `{count, plural, one {# item} other {# items}}`, only when
|
|
498
|
+
* it is unambiguous: one count-like placeholder, used once, directly followed
|
|
499
|
+
* by a regular plural noun that ends the sentence or is followed by a word
|
|
500
|
+
* that doesn't agree with it ("in", "left", "selected", …).
|
|
501
|
+
*/
|
|
502
|
+
function makePlural(pieces, entries) {
|
|
503
|
+
const countLike = entries.filter((e) => e.kind === 'placeholder' && COUNT_NAME.test(e.name));
|
|
504
|
+
if (countLike.length !== 1)
|
|
505
|
+
return undefined;
|
|
506
|
+
// `data?.count` or `n ?? 0` may be undefined at runtime → "NaN items".
|
|
507
|
+
if (!isPlainReference(countLike[0].expression))
|
|
508
|
+
return undefined;
|
|
509
|
+
const name = countLike[0].name;
|
|
510
|
+
const uses = countUses(pieces, name);
|
|
511
|
+
if (uses !== 1)
|
|
512
|
+
return undefined;
|
|
513
|
+
const visit = (list) => {
|
|
514
|
+
for (let i = 0; i < list.length; i++) {
|
|
515
|
+
const p = list[i];
|
|
516
|
+
if (p.type === 'tag' && visit(p.children))
|
|
517
|
+
return true;
|
|
518
|
+
if (p.type !== 'ph' || p.name !== name)
|
|
519
|
+
continue;
|
|
520
|
+
const next = list[i + 1];
|
|
521
|
+
if (!next || next.type !== 'text')
|
|
522
|
+
return false;
|
|
523
|
+
const m = /^ ([a-z]+)(?=$|[\s.,!?;:)])/.exec(next.value);
|
|
524
|
+
if (!m)
|
|
525
|
+
return false;
|
|
526
|
+
const one = singular(m[1]);
|
|
527
|
+
if (!one)
|
|
528
|
+
return false;
|
|
529
|
+
const rest = next.value.slice(m[0].length);
|
|
530
|
+
const after = /^\s*([A-Za-z]+)/.exec(rest);
|
|
531
|
+
const restOfSentence = rest + collectText(list.slice(i + 2));
|
|
532
|
+
if (after && !SAFE_AFTER_NOUN.has(after[1].toLowerCase()))
|
|
533
|
+
return false;
|
|
534
|
+
if (!after && /[A-Za-z]/.test(restOfSentence))
|
|
535
|
+
return false;
|
|
536
|
+
list.splice(i, 2, { type: 'plural', name, one, other: m[1] }, { type: 'text', value: rest });
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
};
|
|
541
|
+
return visit(pieces) ? name : undefined;
|
|
542
|
+
}
|
|
543
|
+
function countUses(pieces, name) {
|
|
544
|
+
let n = 0;
|
|
545
|
+
for (const p of pieces) {
|
|
546
|
+
if (p.type === 'ph' && p.name === name)
|
|
547
|
+
n++;
|
|
548
|
+
if (p.type === 'tag')
|
|
549
|
+
n += countUses(p.children, name);
|
|
550
|
+
}
|
|
551
|
+
return n;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Text used to name the key: the message without arguments and tags, with a
|
|
555
|
+
* plural's `other` branch standing in for it. "Welcome back, {user}" →
|
|
556
|
+
* "Welcome back,"; "{count, plural, one {# item} other {# items}} left" → "items left".
|
|
557
|
+
*/
|
|
558
|
+
function messageKeyText(message) {
|
|
559
|
+
return message
|
|
560
|
+
.replace(/\{\s*\w+\s*,\s*plural\s*,[^{}]*?(?:\{[^{}]*\}[^{}]*?)*\}/g, (m) => {
|
|
561
|
+
const other = /other\s*\{([^{}]*)\}/.exec(m);
|
|
562
|
+
return other ? ` ${other[1].replace(/#/g, '')} ` : ' ';
|
|
563
|
+
})
|
|
564
|
+
.replace(/\{[^{}]*\}/g, ' ')
|
|
565
|
+
.replace(/<\/?[A-Za-z][\w-]*>/g, '')
|
|
566
|
+
.replace(/''/g, "'")
|
|
567
|
+
.replace(/\s+/g, ' ')
|
|
568
|
+
.trim();
|
|
569
|
+
}
|
|
570
|
+
// ── Code generation (shared by the injection strategies) ─────────────────────
|
|
571
|
+
/** A parameter name for rich-text callbacks that the element doesn't already use. */
|
|
572
|
+
function chunksParam(el) {
|
|
573
|
+
const text = code(el);
|
|
574
|
+
for (const candidate of ['chunks', '_chunks', '__chunks']) {
|
|
575
|
+
if (!new RegExp(`\\b${candidate}\\b`).test(text))
|
|
576
|
+
return candidate;
|
|
577
|
+
}
|
|
578
|
+
return '__chunks';
|
|
579
|
+
}
|
|
580
|
+
function propertyKey(name) {
|
|
581
|
+
return /^\d+$/.test(name) ? t.numericLiteral(Number(name)) : t.identifier(name);
|
|
582
|
+
}
|
|
583
|
+
/** `{ user, userName: user.name, 0: format(x) }` — placeholders only. */
|
|
584
|
+
function placeholderValues(message) {
|
|
585
|
+
return message.entries
|
|
586
|
+
.filter((e) => e.kind === 'placeholder')
|
|
587
|
+
.map((e) => {
|
|
588
|
+
const shorthand = t.isIdentifier(e.expression) && e.expression.name === e.name;
|
|
589
|
+
return t.objectProperty(propertyKey(e.name), e.expression, false, shorthand);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
/** next-intl rich values: `b: (chunks) => <b>{chunks}</b>`. Moves the element into the callback. */
|
|
593
|
+
function richTagValue(tag) {
|
|
594
|
+
const param = chunksParam(tag.element);
|
|
595
|
+
const el = tag.element;
|
|
596
|
+
el.children = [t.jsxExpressionContainer(t.identifier(param))];
|
|
597
|
+
if (!el.closingElement) {
|
|
598
|
+
el.openingElement.selfClosing = false;
|
|
599
|
+
el.closingElement = t.jsxClosingElement(el.openingElement.name);
|
|
600
|
+
}
|
|
601
|
+
return t.objectProperty(t.identifier(tag.name), t.arrowFunctionExpression([t.identifier(param)], el));
|
|
602
|
+
}
|
|
603
|
+
/** react-i18next Trans components: `b: <b />`. */
|
|
604
|
+
function transComponent(tag) {
|
|
605
|
+
const el = tag.element;
|
|
606
|
+
el.children = [];
|
|
607
|
+
el.openingElement.selfClosing = true;
|
|
608
|
+
el.closingElement = null;
|
|
609
|
+
return t.objectProperty(t.identifier(tag.name), el);
|
|
610
|
+
}
|
|
611
|
+
function hasTags(message) {
|
|
612
|
+
return message.entries.some((e) => e.kind === 'tag');
|
|
613
|
+
}
|
|
614
|
+
/** `t("key")` or `t("key", { …values })`. */
|
|
615
|
+
function createMessageTCall(key, message, callee = t.identifier('t')) {
|
|
616
|
+
const values = placeholderValues(message);
|
|
617
|
+
const args = [t.stringLiteral(key)];
|
|
618
|
+
if (values.length > 0)
|
|
619
|
+
args.push(t.objectExpression(values));
|
|
620
|
+
return t.callExpression(callee, args);
|
|
621
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { KeyUsageReport, WorkspaceInfo } from '../runtime/types';
|
|
2
|
+
/**
|
|
3
|
+
* Which translation keys the project's code still references.
|
|
4
|
+
*
|
|
5
|
+
* Strings injected by earlier runs are no longer extracted, so the source
|
|
6
|
+
* locale file is the only list of keys — and it only ever grows. Keys whose
|
|
7
|
+
* string was deleted from the code would otherwise be translated into every
|
|
8
|
+
* new locale forever. This scans for the reference forms the injection
|
|
9
|
+
* strategies emit (and common hand-written ones):
|
|
10
|
+
*
|
|
11
|
+
* t('key') / t("key") / t(`key`) react-i18next, next-intl, vue-i18n, vanilla
|
|
12
|
+
* $t('key'), i18n.t('key'), t.rich/markup/raw/has('key')
|
|
13
|
+
* i18nKey="key" react-i18next <Trans>
|
|
14
|
+
* data-i18n="key", data-i18n-<attr>="key" static HTML
|
|
15
|
+
*
|
|
16
|
+
* and next-intl namespaces (`useTranslations('ns')`, `getTranslations('ns')`
|
|
17
|
+
* or `{ namespace: 'ns' }`), counting `t('k')` in such a file as `ns.k` too.
|
|
18
|
+
*
|
|
19
|
+
* Conservative by design: a call with a key it can't read statically
|
|
20
|
+
* (`t(variable)`, `t(\`a.${x}\`)`) marks the scan dynamic, and callers must
|
|
21
|
+
* then treat every key as live.
|
|
22
|
+
*/
|
|
23
|
+
export interface KeyUsage {
|
|
24
|
+
/** Keys referenced literally somewhere in the code. */
|
|
25
|
+
referenced: Set<string>;
|
|
26
|
+
/** Files with a translation call whose key isn't a literal (relative paths). */
|
|
27
|
+
dynamicFiles: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function scanKeyReferences(content: string): {
|
|
30
|
+
keys: string[];
|
|
31
|
+
dynamic: boolean;
|
|
32
|
+
};
|
|
33
|
+
export declare function findKeyUsage(workspace: WorkspaceInfo): Promise<KeyUsage>;
|
|
34
|
+
/** Report-only: source keys with no t() reference found. Never used to skip or drop anything. */
|
|
35
|
+
export declare function reportKeyUsage(workspace: WorkspaceInfo, sourceKeys: Record<string, string>, extractedNow: Set<string>): Promise<KeyUsageReport>;
|