@babelize/engine 0.1.3 → 0.2.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/dist/config.d.ts +3 -3
- package/dist/detection/stack.d.ts +3 -1
- package/dist/detection/stack.js +13 -6
- package/dist/injection/StrategyFactory.d.ts +5 -1
- package/dist/injection/StrategyFactory.js +7 -2
- package/dist/injection/strategies/VanillaStrategy.d.ts +8 -0
- package/dist/injection/strategies/VanillaStrategy.js +10 -3
- package/dist/local/translate-core.d.ts +1 -1
- package/dist/local/translate-core.js +1 -1
- package/dist/modules/extractor/extract.js +67 -10
- package/dist/modules/finalization/dependencies.d.ts +10 -0
- package/dist/modules/finalization/dependencies.js +23 -0
- package/dist/modules/finalization/finalize.js +41 -39
- package/dist/modules/finalization/nextjs-finalize.js +2 -1
- package/dist/modules/finalization/react-native-finalize.d.ts +45 -0
- package/dist/modules/finalization/react-native-finalize.js +324 -0
- package/dist/modules/format-injection/index.js +38 -9
- package/dist/modules/format-injection/inject-formats.d.ts +3 -2
- package/dist/modules/format-injection/inject-formats.js +4 -3
- package/dist/modules/injection/inject.d.ts +14 -1
- package/dist/modules/injection/inject.js +307 -154
- package/dist/modules/sync-detector/sync-detector.js +11 -4
- package/dist/modules/translator/line-breaks.d.ts +8 -0
- package/dist/modules/translator/line-breaks.js +63 -0
- package/dist/modules/translator/translate.js +5 -2
- package/dist/modules/workspace/validate.d.ts +13 -1
- package/dist/modules/workspace/validate.js +95 -58
- package/dist/runtime/types.d.ts +22 -0
- package/dist/runtime/types.js +33 -0
- package/dist/utils/ast-helpers.d.ts +30 -2
- package/dist/utils/ast-helpers.js +355 -42
- package/dist/utils/extraction-options.js +1 -1
- package/dist/utils/file-utils.js +4 -1
- package/dist/utils/i18n-config-path.d.ts +18 -0
- package/dist/utils/i18n-config-path.js +64 -0
- package/dist/utils/i18next-icu.js +1 -1
- package/dist/utils/jsx-message.d.ts +16 -1
- package/dist/utils/jsx-message.js +89 -9
- package/dist/utils/key-usage.js +1 -1
- package/dist/utils/metadata.d.ts +5 -1
- package/dist/utils/metadata.js +19 -1
- package/dist/utils/module-constants.d.ts +21 -3
- package/dist/utils/module-constants.js +123 -25
- package/dist/utils/react-native.d.ts +17 -0
- package/dist/utils/react-native.js +108 -0
- package/dist/utils/string-utils.d.ts +8 -0
- package/dist/utils/string-utils.js +29 -1
- package/dist/utils/ui-copy.d.ts +5 -4
- package/dist/utils/ui-copy.js +10 -5
- package/package.json +3 -2
|
@@ -46,6 +46,8 @@ const html_injector_1 = require("../../injection/html/html-injector");
|
|
|
46
46
|
const inject_file_1 = require("../../django/inject-file");
|
|
47
47
|
const types_2 = require("../../runtime/types");
|
|
48
48
|
const file_utils_1 = require("../../utils/file-utils");
|
|
49
|
+
const i18n_config_path_1 = require("../../utils/i18n-config-path");
|
|
50
|
+
const i18next_icu_1 = require("../../utils/i18next-icu");
|
|
49
51
|
const string_utils_1 = require("../../utils/string-utils");
|
|
50
52
|
const ast_helpers_1 = require("../../utils/ast-helpers");
|
|
51
53
|
/**
|
|
@@ -119,14 +121,21 @@ function transLocalName(ast, content) {
|
|
|
119
121
|
}
|
|
120
122
|
/**
|
|
121
123
|
* `<p> {t(…)} </p>`: keep the spaces at the edges of the children that JSX
|
|
122
|
-
* renders (no line break), as the fragment path does.
|
|
124
|
+
* renders (no line break), as the fragment path does. Edges with a line break
|
|
125
|
+
* render nothing and are kept as written, so the element keeps its layout.
|
|
123
126
|
*/
|
|
124
127
|
function withEdgeSpaces(children, node) {
|
|
125
128
|
const first = children[0];
|
|
126
129
|
const last = children[children.length - 1];
|
|
127
130
|
const lead = ast_helpers_1.t.isJSXText(first) && /^[ \t]+/.test(first.value) && !/^[ \t]*[\r\n]/.test(first.value);
|
|
128
131
|
const trail = ast_helpers_1.t.isJSXText(last) && /[ \t]+$/.test(last.value) && !/[\r\n][ \t]*$/.test(last.value);
|
|
129
|
-
|
|
132
|
+
const leadLayout = !lead && ast_helpers_1.t.isJSXText(first) ? (first.value.match(/^\s*[\r\n]\s*/)?.[0] ?? '') : '';
|
|
133
|
+
const trailLayout = !trail && ast_helpers_1.t.isJSXText(last) ? (last.value.match(/\s*[\r\n]\s*$/)?.[0] ?? '') : '';
|
|
134
|
+
return [
|
|
135
|
+
...(lead ? [ast_helpers_1.t.jsxText(' ')] : leadLayout ? [ast_helpers_1.t.jsxText(leadLayout)] : []),
|
|
136
|
+
node,
|
|
137
|
+
...(trail ? [ast_helpers_1.t.jsxText(' ')] : trailLayout ? [ast_helpers_1.t.jsxText(trailLayout)] : []),
|
|
138
|
+
];
|
|
130
139
|
}
|
|
131
140
|
const HOOK_CALLEES = new Set(['useTranslations', 'getTranslations', 'useTranslation']);
|
|
132
141
|
/** `const t = useTranslations()`, `const t = await getTranslations()`, `const { t } = useTranslation()`. */
|
|
@@ -242,18 +251,97 @@ function replaceJSXTextKeepingSpaces(path, tCall) {
|
|
|
242
251
|
const trailing = raw.match(/\s*$/)?.[0] ?? '';
|
|
243
252
|
const keepLeading = leading.length > 0 && !leading.includes('\n');
|
|
244
253
|
const keepTrailing = trailing.length > 0 && !trailing.includes('\n') && trailing.length < raw.length;
|
|
245
|
-
|
|
254
|
+
// A line break with its indentation renders nothing, so it stays as written:
|
|
255
|
+
// `>\n Decline\n</Text>` becomes `>\n {t("…")}\n</Text>`, not `>{t("…")}</Text>`.
|
|
256
|
+
const layoutLeading = !keepLeading && leading.includes('\n') ? leading : '';
|
|
257
|
+
const layoutTrailing = !keepTrailing && trailing.includes('\n') && trailing.length < raw.length ? trailing : '';
|
|
258
|
+
if (!keepLeading && !keepTrailing && !layoutLeading && !layoutTrailing) {
|
|
246
259
|
path.replaceWith(tCall);
|
|
247
260
|
return;
|
|
248
261
|
}
|
|
249
262
|
const nodes = [];
|
|
250
263
|
if (keepLeading)
|
|
251
264
|
nodes.push(ast_helpers_1.t.jsxText(' '));
|
|
265
|
+
else if (layoutLeading)
|
|
266
|
+
nodes.push(ast_helpers_1.t.jsxText(layoutLeading));
|
|
252
267
|
nodes.push(tCall);
|
|
253
268
|
if (keepTrailing)
|
|
254
269
|
nodes.push(ast_helpers_1.t.jsxText(' '));
|
|
270
|
+
else if (layoutTrailing)
|
|
271
|
+
nodes.push(ast_helpers_1.t.jsxText(layoutTrailing));
|
|
255
272
|
path.replaceWithMultiple(nodes);
|
|
256
273
|
}
|
|
274
|
+
/** `NICHES` → `NICHES_KEYS`, `PLANS` + `label` → `PLANS_LABEL_KEYS`, `tabs` + `label` → `tabsLabelKeys`. */
|
|
275
|
+
function labelMapName(constant, field) {
|
|
276
|
+
if (/^[A-Z0-9_]+$/.test(constant))
|
|
277
|
+
return `${constant}${field ? `_${field.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}` : ''}_KEYS`;
|
|
278
|
+
const cap = (x) => x.charAt(0).toUpperCase() + x.slice(1);
|
|
279
|
+
return `${constant}${cap(field)}Keys`;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The value → key map of each rendered field of a constant that is also data,
|
|
283
|
+
* declared right after it (`const NICHES_KEYS: Record<string, string> = { "Fashion": "…" }`).
|
|
284
|
+
* A map an earlier run added is reused, with any new values added to it.
|
|
285
|
+
*/
|
|
286
|
+
function addLabelMaps(plan, keys, isTypeScript, usedNames) {
|
|
287
|
+
const entries = new Map();
|
|
288
|
+
plan.literals.forEach((lit, i) => {
|
|
289
|
+
const field = plan.literalFields[i];
|
|
290
|
+
const values = entries.get(field) ?? new Map();
|
|
291
|
+
if (!values.has(lit.node.value))
|
|
292
|
+
values.set(lit.node.value, keys[i]);
|
|
293
|
+
entries.set(field, values);
|
|
294
|
+
});
|
|
295
|
+
const maps = new Map(plan.labelMaps);
|
|
296
|
+
const scope = plan.anchor.scope;
|
|
297
|
+
const added = [];
|
|
298
|
+
for (const [field, values] of entries) {
|
|
299
|
+
const existing = maps.get(field);
|
|
300
|
+
if (existing) {
|
|
301
|
+
const init = scope.getBinding(existing)?.path.node;
|
|
302
|
+
if (ast_helpers_1.t.isVariableDeclarator(init) && ast_helpers_1.t.isObjectExpression(init.init)) {
|
|
303
|
+
const obj = init.init;
|
|
304
|
+
const has = new Set(obj.properties.flatMap((p) => (ast_helpers_1.t.isObjectProperty(p) && ast_helpers_1.t.isStringLiteral(p.key) ? [p.key.value] : [])));
|
|
305
|
+
for (const [value, key] of values) {
|
|
306
|
+
if (!has.has(value))
|
|
307
|
+
obj.properties.push(ast_helpers_1.t.objectProperty(ast_helpers_1.t.stringLiteral(value), ast_helpers_1.t.stringLiteral(key)));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
// Maps added for earlier lists in this file aren't scope bindings yet.
|
|
313
|
+
const taken = (x) => scope.hasBinding(x) || usedNames.has(x) || [...maps.values()].includes(x);
|
|
314
|
+
let name = labelMapName(plan.name, field);
|
|
315
|
+
for (let n = 2; taken(name); n++)
|
|
316
|
+
name = `${labelMapName(plan.name, field)}${n}`;
|
|
317
|
+
const id = ast_helpers_1.t.identifier(name);
|
|
318
|
+
if (isTypeScript) {
|
|
319
|
+
id.typeAnnotation = ast_helpers_1.t.tsTypeAnnotation(ast_helpers_1.t.tsTypeReference(ast_helpers_1.t.identifier('Record'), ast_helpers_1.t.tsTypeParameterInstantiation([ast_helpers_1.t.tsStringKeyword(), ast_helpers_1.t.tsStringKeyword()])));
|
|
320
|
+
}
|
|
321
|
+
const object = ast_helpers_1.t.objectExpression([...values].map(([value, key]) => ast_helpers_1.t.objectProperty(ast_helpers_1.t.stringLiteral(value), ast_helpers_1.t.stringLiteral(key))));
|
|
322
|
+
added.push(ast_helpers_1.t.variableDeclaration('const', [ast_helpers_1.t.variableDeclarator(id, object)]));
|
|
323
|
+
maps.set(field, name);
|
|
324
|
+
usedNames.add(name);
|
|
325
|
+
}
|
|
326
|
+
if (added.length > 0) {
|
|
327
|
+
if (plan.insert === 'after')
|
|
328
|
+
plan.anchor.insertAfter(added);
|
|
329
|
+
else
|
|
330
|
+
plan.anchor.insertBefore(added);
|
|
331
|
+
}
|
|
332
|
+
return maps;
|
|
333
|
+
}
|
|
334
|
+
/** Whether `p` sits in the parameters (a default value) of the function whose body is `body`. */
|
|
335
|
+
function isInComponentParams(p, body) {
|
|
336
|
+
const fn = body.parentPath;
|
|
337
|
+
if (!fn?.isFunction())
|
|
338
|
+
return false;
|
|
339
|
+
for (let cur = p; cur && cur !== fn; cur = cur.parentPath) {
|
|
340
|
+
if (cur.listKey === 'params' && cur.parentPath === fn)
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
257
345
|
/**
|
|
258
346
|
* Phase 5: Injection
|
|
259
347
|
*
|
|
@@ -273,9 +361,23 @@ positions) {
|
|
|
273
361
|
const sourceFiles = [];
|
|
274
362
|
for (const sourceDir of workspace.sourceDirs) {
|
|
275
363
|
const dirPath = path.join(workspace.path, sourceDir);
|
|
276
|
-
const files = await (0, file_utils_1.findFiles)(dirPath, extensions, types_2.
|
|
364
|
+
const files = await (0, file_utils_1.findFiles)(dirPath, extensions, (0, types_2.workspaceExcludes)(workspace));
|
|
277
365
|
sourceFiles.push(...files);
|
|
278
366
|
}
|
|
367
|
+
// react-i18next projects get Babelize's config, which exports a module-level
|
|
368
|
+
// `t`: strings outside any component the hook can go in use it. A config
|
|
369
|
+
// the user owns may not export `t`, so those strings stay as they are.
|
|
370
|
+
const deps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
|
|
371
|
+
const library = StrategyFactory_1.StrategyFactory.getStrategy(workspace.framework, deps).id;
|
|
372
|
+
let babelizeOwnsConfig = false;
|
|
373
|
+
// `t(key, { total })` fills "{total}" only where messages are ICU:
|
|
374
|
+
// next-intl always, react-i18next with i18next-icu (Babelize's configs load it).
|
|
375
|
+
let placeholders = library === 'next-intl';
|
|
376
|
+
if (library === 'react-i18next') {
|
|
377
|
+
const status = await (0, i18next_icu_1.detectI18nextIcu)(workspace).catch(() => null);
|
|
378
|
+
babelizeOwnsConfig = status ? !status.userOwnsInit : false;
|
|
379
|
+
placeholders = status?.available ?? false;
|
|
380
|
+
}
|
|
279
381
|
// Collect keys by file for efficient lookup
|
|
280
382
|
const keysByFile = new Map();
|
|
281
383
|
Array.from(keyMapping.entries()).forEach(([extractionId, key]) => {
|
|
@@ -315,7 +417,8 @@ positions) {
|
|
|
315
417
|
}
|
|
316
418
|
else {
|
|
317
419
|
const existingDeps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
|
|
318
|
-
|
|
420
|
+
const moduleTranslator = babelizeOwnsConfig ? (0, i18n_config_path_1.i18nImportPath)(workspace, relativePath) : undefined;
|
|
421
|
+
result = injectIntoFile(originalContent, relativePath, fileKeys, confidenceThreshold, workspace.framework, existingDeps, undefined, positions, { moduleTranslator, placeholders });
|
|
319
422
|
}
|
|
320
423
|
if (result.injections.length > 0) {
|
|
321
424
|
modifiedFiles.push(result);
|
|
@@ -342,6 +445,7 @@ positions) {
|
|
|
342
445
|
}
|
|
343
446
|
const ALLOWED_PARENT_TYPES = [
|
|
344
447
|
'ObjectProperty',
|
|
448
|
+
'AssignmentPattern',
|
|
345
449
|
'ArrayExpression',
|
|
346
450
|
'VariableDeclarator',
|
|
347
451
|
'AssignmentExpression',
|
|
@@ -357,7 +461,8 @@ const ALLOWED_PARENT_TYPES = [
|
|
|
357
461
|
*/
|
|
358
462
|
function injectIntoFile(content, filePath, keyMapping, confidenceThreshold, framework, existingDeps, forceTypeScript,
|
|
359
463
|
/** extractionId → source offsets of the nodes extraction selected (see ExtractedString.offsets). */
|
|
360
|
-
positions) {
|
|
464
|
+
positions, options = {}) {
|
|
465
|
+
const { moduleTranslator, placeholders = false } = options;
|
|
361
466
|
const injections = [];
|
|
362
467
|
let importAdded = false;
|
|
363
468
|
let hookAdded = false;
|
|
@@ -371,11 +476,13 @@ positions) {
|
|
|
371
476
|
}
|
|
372
477
|
}
|
|
373
478
|
const isTypeScript = forceTypeScript || filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
374
|
-
const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript });
|
|
479
|
+
const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript, preserveFormat: true });
|
|
375
480
|
// Track components that need the hook
|
|
376
481
|
const componentsNeedingHook = new Set();
|
|
377
|
-
let activeStrategy = StrategyFactory_1.StrategyFactory.getStrategy(framework, existingDeps);
|
|
482
|
+
let activeStrategy = StrategyFactory_1.StrategyFactory.getStrategy(framework, existingDeps, moduleTranslator);
|
|
378
483
|
let hasNonComponentInjections = false;
|
|
484
|
+
// Plain t() for code outside any component the hook can go in.
|
|
485
|
+
const moduleStrategy = StrategyFactory_1.StrategyFactory.getStrategy('vanilla', undefined, moduleTranslator);
|
|
379
486
|
// Concise-body arrow components (`const Button = () => <button …/>`) have no
|
|
380
487
|
// body to put the translation hook in. Give the ones that hold a string to
|
|
381
488
|
// inject a block body (`=> { return …; }`) so they can be wired like any
|
|
@@ -384,7 +491,11 @@ positions) {
|
|
|
384
491
|
// another node that happens to have the same text on the same line.
|
|
385
492
|
const selected = (id, node) => {
|
|
386
493
|
const starts = positions?.get(id);
|
|
387
|
-
|
|
494
|
+
if (!starts)
|
|
495
|
+
return true;
|
|
496
|
+
// Where the node starts in the file as written, which is what extraction recorded.
|
|
497
|
+
const start = (0, ast_helpers_1.sourceOffset)(ast, node);
|
|
498
|
+
return typeof start === 'number' && starts.has(start);
|
|
388
499
|
};
|
|
389
500
|
const hasKey = (text, node) => {
|
|
390
501
|
const id = `${filePath}:${node.loc?.start.line || 0}:${(0, string_utils_1.hashString)(text)}`;
|
|
@@ -445,10 +556,31 @@ positions) {
|
|
|
445
556
|
// each render becomes t(…). Their literals are left to this, not to the
|
|
446
557
|
// StringLiteral visitors (which would put a module-level t() call there).
|
|
447
558
|
const moduleLiterals = new WeakSet();
|
|
559
|
+
/**
|
|
560
|
+
* The function body the translation hook goes in. Lowercase render helpers
|
|
561
|
+
* get it only when there is no module-level t to use instead (next-intl, a
|
|
562
|
+
* config the user owns): a hook there runs at render time, while skipping
|
|
563
|
+
* would leave a string that was extracted and translated hardcoded.
|
|
564
|
+
*/
|
|
565
|
+
const hookBody = (p) => (0, ast_helpers_1.findComponentBody)(p) ?? (moduleTranslator ? null : (0, ast_helpers_1.findComponentBody)(p, { renderHelpers: true }));
|
|
566
|
+
/** Pre-scan: where a string the main pass will replace gets its t() from. */
|
|
567
|
+
const noteSite = (p) => {
|
|
568
|
+
const isComponent = (0, ast_helpers_1.isInsideComponent)(p);
|
|
569
|
+
const body = isComponent ? hookBody(p) : null;
|
|
570
|
+
if (body && !isInComponentParams(p, body))
|
|
571
|
+
componentsNeedingHook.add(body);
|
|
572
|
+
else if (!isComponent || moduleTranslator)
|
|
573
|
+
hasNonComponentInjections = true;
|
|
574
|
+
};
|
|
448
575
|
const moduleRewrites = [];
|
|
576
|
+
const labelRewrites = [];
|
|
449
577
|
if (activeStrategy.id !== 'vanilla') {
|
|
450
578
|
for (const plan of (0, module_constants_1.analyzeModuleConstants)(ast)) {
|
|
579
|
+
// Values that are also data are never translated in place, wired or not.
|
|
451
580
|
if (!plan.provable)
|
|
581
|
+
for (const lit of plan.literals)
|
|
582
|
+
moduleLiterals.add(lit.node);
|
|
583
|
+
if (!plan.provable && !plan.labels)
|
|
452
584
|
continue;
|
|
453
585
|
const keys = plan.literals.map((lit) => {
|
|
454
586
|
const id = `${filePath}:${lit.node.loc?.start.line || 0}:${(0, string_utils_1.hashString)(lit.node.value.trim())}`;
|
|
@@ -457,30 +589,72 @@ positions) {
|
|
|
457
589
|
});
|
|
458
590
|
if (keys.some((k) => !k) || confidenceThreshold > 100)
|
|
459
591
|
continue;
|
|
592
|
+
// Every render site needs a t(): its component's hook, or the config's module-level t.
|
|
593
|
+
if (!moduleTranslator && plan.renderSites.some((site) => !hookBody(site)))
|
|
594
|
+
continue;
|
|
460
595
|
for (const lit of plan.literals)
|
|
461
596
|
moduleLiterals.add(lit.node);
|
|
462
597
|
for (const site of plan.renderSites) {
|
|
463
|
-
const body = (
|
|
598
|
+
const body = hookBody(site);
|
|
464
599
|
if (body)
|
|
465
600
|
componentsNeedingHook.add(body);
|
|
466
601
|
}
|
|
467
|
-
moduleRewrites.push({ plan, keys: keys });
|
|
602
|
+
(plan.provable ? moduleRewrites : labelRewrites).push({ plan, keys: keys });
|
|
468
603
|
}
|
|
469
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* A template literal or `+` concatenation extraction selected:
|
|
607
|
+
* `\`Charged ${total}\`` → key + `{ total }`. Null when it isn't one, the
|
|
608
|
+
* runtime can't fill placeholders, or two placeholders share a name.
|
|
609
|
+
*/
|
|
610
|
+
const placeholderMessage = (path) => {
|
|
611
|
+
if (!placeholders)
|
|
612
|
+
return null;
|
|
613
|
+
if (path.isBinaryExpression() && path.node.operator !== '+')
|
|
614
|
+
return null;
|
|
615
|
+
if (!path.isTemplateLiteral() && !path.isBinaryExpression())
|
|
616
|
+
return null;
|
|
617
|
+
const names = [];
|
|
618
|
+
const exprs = [];
|
|
619
|
+
const text = (0, ast_helpers_1.extractExpressionContent)(path.node, names, exprs)?.trim();
|
|
620
|
+
if (!text)
|
|
621
|
+
return null;
|
|
622
|
+
const extractionId = `${filePath}:${path.node.loc?.start.line || 0}:${(0, string_utils_1.hashString)(text)}`;
|
|
623
|
+
const key = keyMapping.get(extractionId);
|
|
624
|
+
if (!key || !selected(extractionId, path.node) || confidenceThreshold > 100)
|
|
625
|
+
return null;
|
|
626
|
+
if ((0, ast_helpers_1.isNonTranslatableContext)(path))
|
|
627
|
+
return null;
|
|
628
|
+
const byName = new Map();
|
|
629
|
+
for (let i = 0; i < names.length; i++) {
|
|
630
|
+
const prev = byName.get(names[i]);
|
|
631
|
+
if (prev && !ast_helpers_1.t.isNodesEquivalent(prev, exprs[i]))
|
|
632
|
+
return null;
|
|
633
|
+
byName.set(names[i], exprs[i]);
|
|
634
|
+
}
|
|
635
|
+
if (byName.size === 0)
|
|
636
|
+
return { key, values: null };
|
|
637
|
+
const props = [...byName].map(([name, expr]) => {
|
|
638
|
+
const shorthand = ast_helpers_1.t.isIdentifier(expr) && expr.name === name;
|
|
639
|
+
const keyNode = ast_helpers_1.t.isValidIdentifier(name) ? ast_helpers_1.t.identifier(name) : ast_helpers_1.t.stringLiteral(name);
|
|
640
|
+
// The original node, not a clone: a clone loses its source offset, so a
|
|
641
|
+
// fallback string inside (`name || 'your business'`) couldn't be wired.
|
|
642
|
+
return ast_helpers_1.t.objectProperty(keyNode, expr, false, shorthand);
|
|
643
|
+
});
|
|
644
|
+
return { key, values: ast_helpers_1.t.objectExpression(props) };
|
|
645
|
+
};
|
|
470
646
|
// Pre-scan to determine if we should fall back to vanilla strategy and identify hooks needed
|
|
471
647
|
(0, ast_helpers_1.traverse)(ast, {
|
|
648
|
+
'TemplateLiteral|BinaryExpression'(path) {
|
|
649
|
+
if (!placeholderMessage(path))
|
|
650
|
+
return;
|
|
651
|
+
noteSite(path);
|
|
652
|
+
},
|
|
472
653
|
'JSXElement|JSXFragment'(visited) {
|
|
473
654
|
const path = visited;
|
|
474
655
|
if (!sentenceFor(path) || confidenceThreshold > 100)
|
|
475
656
|
return;
|
|
476
|
-
|
|
477
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(path);
|
|
478
|
-
if (componentPath)
|
|
479
|
-
componentsNeedingHook.add(componentPath);
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
hasNonComponentInjections = true;
|
|
483
|
-
}
|
|
657
|
+
noteSite(path);
|
|
484
658
|
},
|
|
485
659
|
JSXText(path) {
|
|
486
660
|
const text = (0, ast_helpers_1.extractJSXTextContent)(path.node);
|
|
@@ -500,24 +674,7 @@ positions) {
|
|
|
500
674
|
return;
|
|
501
675
|
if (confidenceThreshold > 100)
|
|
502
676
|
return;
|
|
503
|
-
|
|
504
|
-
if (isComponent) {
|
|
505
|
-
if (activeStrategy.id !== 'vanilla') {
|
|
506
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(path);
|
|
507
|
-
if (!componentPath)
|
|
508
|
-
return;
|
|
509
|
-
componentsNeedingHook.add(componentPath);
|
|
510
|
-
}
|
|
511
|
-
else {
|
|
512
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(path);
|
|
513
|
-
if (componentPath) {
|
|
514
|
-
componentsNeedingHook.add(componentPath);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
hasNonComponentInjections = true;
|
|
520
|
-
}
|
|
677
|
+
noteSite(path);
|
|
521
678
|
},
|
|
522
679
|
JSXAttribute(attributes) {
|
|
523
680
|
const name = attributes.node.name;
|
|
@@ -543,24 +700,7 @@ positions) {
|
|
|
543
700
|
return;
|
|
544
701
|
if (confidenceThreshold > 100)
|
|
545
702
|
return;
|
|
546
|
-
|
|
547
|
-
if (isComponent) {
|
|
548
|
-
if (activeStrategy.id !== 'vanilla') {
|
|
549
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(attributes);
|
|
550
|
-
if (!componentPath)
|
|
551
|
-
return;
|
|
552
|
-
componentsNeedingHook.add(componentPath);
|
|
553
|
-
}
|
|
554
|
-
else {
|
|
555
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(attributes);
|
|
556
|
-
if (componentPath) {
|
|
557
|
-
componentsNeedingHook.add(componentPath);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
else {
|
|
562
|
-
hasNonComponentInjections = true;
|
|
563
|
-
}
|
|
703
|
+
noteSite(attributes);
|
|
564
704
|
},
|
|
565
705
|
StringLiteral(path) {
|
|
566
706
|
if (moduleLiterals.has(path.node))
|
|
@@ -597,31 +737,16 @@ positions) {
|
|
|
597
737
|
return;
|
|
598
738
|
if (confidenceThreshold > 100)
|
|
599
739
|
return;
|
|
600
|
-
|
|
601
|
-
if (isComponent) {
|
|
602
|
-
if (activeStrategy.id !== 'vanilla') {
|
|
603
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(path);
|
|
604
|
-
if (!componentPath)
|
|
605
|
-
return;
|
|
606
|
-
componentsNeedingHook.add(componentPath);
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
609
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(path);
|
|
610
|
-
if (componentPath) {
|
|
611
|
-
componentsNeedingHook.add(componentPath);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
hasNonComponentInjections = true;
|
|
617
|
-
}
|
|
740
|
+
noteSite(path);
|
|
618
741
|
}
|
|
619
742
|
});
|
|
620
743
|
// Determine active strategy based on pre-scan results.
|
|
621
744
|
// If there are ONLY non-component injections (no components needing hooks),
|
|
622
745
|
// fall back to vanilla strategy which doesn't require React hooks.
|
|
623
|
-
|
|
624
|
-
|
|
746
|
+
// Only with a real module-level t: without one (next-intl, a config the user
|
|
747
|
+
// owns) such strings are left as they are rather than importing `@/i18n`.
|
|
748
|
+
if (hasNonComponentInjections && componentsNeedingHook.size === 0 && activeStrategy.id !== 'vanilla' && moduleTranslator) {
|
|
749
|
+
activeStrategy = moduleStrategy;
|
|
625
750
|
}
|
|
626
751
|
// Compute hasUseClient AFTER strategy selection — the strategy may have fallen back
|
|
627
752
|
// to vanilla, which doesn't care about use client directives.
|
|
@@ -634,13 +759,33 @@ positions) {
|
|
|
634
759
|
const nameFor = (p) => {
|
|
635
760
|
if (activeStrategy.id === 'vanilla')
|
|
636
761
|
return 't';
|
|
637
|
-
const body = (
|
|
762
|
+
const body = hookBody(p);
|
|
638
763
|
if (!body)
|
|
639
764
|
return 't';
|
|
640
765
|
if (!translatorNames.has(body.node))
|
|
641
766
|
translatorNames.set(body.node, translatorNameFor(body));
|
|
642
767
|
return translatorNames.get(body.node);
|
|
643
768
|
};
|
|
769
|
+
const translatorFor = (p) => {
|
|
770
|
+
if (activeStrategy.id === 'vanilla')
|
|
771
|
+
return 'module';
|
|
772
|
+
const body = (0, ast_helpers_1.isInsideComponent)(p) ? hookBody(p) : null;
|
|
773
|
+
// A default value (`({ title = 'Untitled' }) =>`) runs before the body's hook.
|
|
774
|
+
if (body && !isInComponentParams(p, body))
|
|
775
|
+
return 'hook';
|
|
776
|
+
return moduleTranslator ? 'module' : null;
|
|
777
|
+
};
|
|
778
|
+
let moduleTranslatorUsed = false;
|
|
779
|
+
let hookTranslatorUsed = false;
|
|
780
|
+
/** The t() expression for a replacement, recording which import it needs. */
|
|
781
|
+
const pickTranslator = (p, source) => {
|
|
782
|
+
if (source === 'module') {
|
|
783
|
+
moduleTranslatorUsed = true;
|
|
784
|
+
return 't';
|
|
785
|
+
}
|
|
786
|
+
hookTranslatorUsed = true;
|
|
787
|
+
return nameFor(p);
|
|
788
|
+
};
|
|
644
789
|
for (const { plan, keys } of moduleRewrites) {
|
|
645
790
|
plan.literals.forEach((lit, i) => {
|
|
646
791
|
injections.push({
|
|
@@ -655,10 +800,37 @@ positions) {
|
|
|
655
800
|
lit.replaceWith(ast_helpers_1.t.stringLiteral(keys[i]));
|
|
656
801
|
});
|
|
657
802
|
for (const site of plan.renderSites) {
|
|
658
|
-
|
|
803
|
+
const name = pickTranslator(site, hookBody(site) ? 'hook' : 'module');
|
|
804
|
+
site.replaceWith(ast_helpers_1.t.callExpression(ast_helpers_1.t.identifier(name), [site.node]));
|
|
659
805
|
}
|
|
660
806
|
needsHook = true;
|
|
661
807
|
}
|
|
808
|
+
const labelMapNames = new Set();
|
|
809
|
+
for (const { plan, keys } of labelRewrites) {
|
|
810
|
+
const maps = addLabelMaps(plan, keys, isTypeScript, labelMapNames);
|
|
811
|
+
plan.literals.forEach((lit, i) => {
|
|
812
|
+
injections.push({
|
|
813
|
+
file: filePath,
|
|
814
|
+
line: lit.node.loc?.start.line || 0,
|
|
815
|
+
column: lit.node.loc?.start.column || 0,
|
|
816
|
+
original: lit.node.value,
|
|
817
|
+
replacement: `${maps.get(plan.literalFields[i])}[…]`,
|
|
818
|
+
key: keys[i],
|
|
819
|
+
confidence: 100,
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
plan.renderSites.forEach((site, i) => {
|
|
823
|
+
const name = pickTranslator(site, hookBody(site) ? 'hook' : 'module');
|
|
824
|
+
const field = plan.renderFields[i];
|
|
825
|
+
const lookup = () => ast_helpers_1.t.memberExpression(ast_helpers_1.t.identifier(maps.get(field)), ast_helpers_1.t.cloneNode(site.node, true, true), true);
|
|
826
|
+
// Values not in the map (not copy) are shown as they are, never looked up as keys.
|
|
827
|
+
site.replaceWith(plan.partialFields.has(field)
|
|
828
|
+
? ast_helpers_1.t.conditionalExpression(lookup(), ast_helpers_1.t.callExpression(ast_helpers_1.t.identifier(name), [lookup()]), site.node)
|
|
829
|
+
: ast_helpers_1.t.callExpression(ast_helpers_1.t.identifier(name), [ast_helpers_1.t.logicalExpression('??', lookup(), site.node)]));
|
|
830
|
+
});
|
|
831
|
+
if (plan.renderSites.length > 0)
|
|
832
|
+
needsHook = true;
|
|
833
|
+
}
|
|
662
834
|
const messageOptions = { transName: transLocalName(ast, content) };
|
|
663
835
|
// Second pass: identify and replace strings
|
|
664
836
|
(0, ast_helpers_1.traverse)(ast, {
|
|
@@ -671,15 +843,17 @@ positions) {
|
|
|
671
843
|
const confidence = 100;
|
|
672
844
|
if (confidence < confidenceThreshold)
|
|
673
845
|
return;
|
|
674
|
-
const
|
|
675
|
-
if (
|
|
846
|
+
const source = translatorFor(path);
|
|
847
|
+
if (!source)
|
|
676
848
|
return;
|
|
677
|
-
// Outside a component there is no hook:
|
|
678
|
-
|
|
849
|
+
// Outside a component there is no hook: the plain t() call, or with
|
|
850
|
+
// react-i18next <Trans t={t}> on the config's module-level t.
|
|
851
|
+
const strategy = source === 'hook' || activeStrategy.id === 'react-i18next' ? activeStrategy : moduleStrategy;
|
|
679
852
|
const rendering = renderMessage(strategy, sentence.key, sentence.message, messageOptions);
|
|
680
853
|
if (!rendering)
|
|
681
854
|
return;
|
|
682
|
-
|
|
855
|
+
const name = pickTranslator(path, source);
|
|
856
|
+
path.node.children = withEdgeSpaces(path.node.children, name === 't' ? rendering.node : useTranslatorName(rendering.node, name));
|
|
683
857
|
extraImports.push(...(rendering.imports ?? []));
|
|
684
858
|
injections.push({
|
|
685
859
|
file: filePath,
|
|
@@ -713,31 +887,10 @@ positions) {
|
|
|
713
887
|
const confidence = 100;
|
|
714
888
|
if (confidence < confidenceThreshold)
|
|
715
889
|
return;
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
// Skip injection entirely when isComponent is true but componentPath is null and the activeStrategy requires hooks
|
|
719
|
-
if (isComponent && activeStrategy.id !== 'vanilla') {
|
|
720
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(path);
|
|
721
|
-
if (!componentPath)
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
// Fallback for non-component JSX text: use vanilla t() call instead of silently skipping
|
|
725
|
-
if (!isSetupSafe) {
|
|
726
|
-
const tCall = (0, ast_helpers_1.createJSXTCall)(key);
|
|
727
|
-
replaceJSXTextKeepingSpaces(path, tCall);
|
|
728
|
-
injections.push({
|
|
729
|
-
file: filePath,
|
|
730
|
-
line,
|
|
731
|
-
column,
|
|
732
|
-
original: text,
|
|
733
|
-
replacement: `{t('${key}')}`,
|
|
734
|
-
key,
|
|
735
|
-
confidence,
|
|
736
|
-
});
|
|
737
|
-
needsHook = true;
|
|
890
|
+
const source = translatorFor(path);
|
|
891
|
+
if (!source)
|
|
738
892
|
return;
|
|
739
|
-
|
|
740
|
-
const tFunctionName = nameFor(path);
|
|
893
|
+
const tFunctionName = pickTranslator(path, source);
|
|
741
894
|
const tCall = useTranslatorName((0, ast_helpers_1.createJSXTCall)(key), tFunctionName);
|
|
742
895
|
replaceJSXTextKeepingSpaces(path, tCall);
|
|
743
896
|
injections.push({
|
|
@@ -777,31 +930,10 @@ positions) {
|
|
|
777
930
|
const confidence = 100;
|
|
778
931
|
if (confidence < confidenceThreshold)
|
|
779
932
|
return;
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
// Skip injection entirely when isComponent is true but componentPath is null and the activeStrategy requires hooks
|
|
783
|
-
if (isComponent && activeStrategy.id !== 'vanilla') {
|
|
784
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(attributes);
|
|
785
|
-
if (!componentPath)
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
// Fallback for non-component JSX attributes: use vanilla t() call
|
|
789
|
-
if (!isSetupSafe) {
|
|
790
|
-
const tFunctionName = 't';
|
|
791
|
-
attributes.node.value = ast_helpers_1.t.jsxExpressionContainer((0, ast_helpers_1.createTCall)(key));
|
|
792
|
-
injections.push({
|
|
793
|
-
file: filePath,
|
|
794
|
-
line,
|
|
795
|
-
column: attributes.node.loc?.start.column || 0,
|
|
796
|
-
original: text,
|
|
797
|
-
replacement: `{${tFunctionName}('${key}')}`,
|
|
798
|
-
key,
|
|
799
|
-
confidence,
|
|
800
|
-
});
|
|
801
|
-
needsHook = true;
|
|
933
|
+
const source = translatorFor(attributes);
|
|
934
|
+
if (!source)
|
|
802
935
|
return;
|
|
803
|
-
|
|
804
|
-
const tFunctionName = nameFor(attributes);
|
|
936
|
+
const tFunctionName = pickTranslator(attributes, source);
|
|
805
937
|
attributes.node.value = ast_helpers_1.t.jsxExpressionContainer(useTranslatorName((0, ast_helpers_1.createTCall)(key), tFunctionName));
|
|
806
938
|
injections.push({
|
|
807
939
|
file: filePath,
|
|
@@ -814,6 +946,36 @@ positions) {
|
|
|
814
946
|
});
|
|
815
947
|
needsHook = true;
|
|
816
948
|
},
|
|
949
|
+
// `\`Charged ${total}\`` / `'Hi ' + name` → t(key, { total }). Visited
|
|
950
|
+
// before the string literals inside, which are then part of the message.
|
|
951
|
+
'TemplateLiteral|BinaryExpression'(path) {
|
|
952
|
+
const message = placeholderMessage(path);
|
|
953
|
+
if (!message)
|
|
954
|
+
return;
|
|
955
|
+
const source = translatorFor(path);
|
|
956
|
+
if (!source)
|
|
957
|
+
return;
|
|
958
|
+
const name = pickTranslator(path, source);
|
|
959
|
+
const args = [ast_helpers_1.t.stringLiteral(message.key)];
|
|
960
|
+
if (message.values)
|
|
961
|
+
args.push(message.values);
|
|
962
|
+
const call = ast_helpers_1.t.callExpression(ast_helpers_1.t.identifier(name), args);
|
|
963
|
+
const line = path.node.loc?.start.line || 0;
|
|
964
|
+
const column = path.node.loc?.start.column || 0;
|
|
965
|
+
const original = (0, ast_helpers_1.generate)(path.node).code;
|
|
966
|
+
path.replaceWith(call);
|
|
967
|
+
// Not skipped: the values are visited next, for copy inside them.
|
|
968
|
+
injections.push({
|
|
969
|
+
file: filePath,
|
|
970
|
+
line,
|
|
971
|
+
column,
|
|
972
|
+
original,
|
|
973
|
+
replacement: (0, ast_helpers_1.generate)(call).code,
|
|
974
|
+
key: message.key,
|
|
975
|
+
confidence: 100,
|
|
976
|
+
});
|
|
977
|
+
needsHook = true;
|
|
978
|
+
},
|
|
817
979
|
// Handle string literals in safe contexts (ObjectProperty, ArrayExpression, VariableDeclarator, etc.)
|
|
818
980
|
StringLiteral(path) {
|
|
819
981
|
if (moduleLiterals.has(path.node))
|
|
@@ -830,6 +992,8 @@ positions) {
|
|
|
830
992
|
// Specific checks to ensure the string literal is in a value/translatable position
|
|
831
993
|
if (parent.isObjectProperty() && parent.node.key === path.node)
|
|
832
994
|
return;
|
|
995
|
+
if (parent.isAssignmentPattern() && parent.node.right !== path.node)
|
|
996
|
+
return;
|
|
833
997
|
if (parent.isConditionalExpression() && parent.node.test === path.node)
|
|
834
998
|
return;
|
|
835
999
|
if (parent.isCallExpression() && parent.node.callee === path.node)
|
|
@@ -853,30 +1017,10 @@ positions) {
|
|
|
853
1017
|
return;
|
|
854
1018
|
if (confidenceThreshold > 100)
|
|
855
1019
|
return;
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
// Skip injection entirely when isComponent is true but componentPath is null and the activeStrategy requires hooks
|
|
859
|
-
if (isComponent && activeStrategy.id !== 'vanilla') {
|
|
860
|
-
const componentPath = (0, ast_helpers_1.findComponentBody)(path);
|
|
861
|
-
if (!componentPath)
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
// Fallback for non-component string literals: use vanilla t() call directly
|
|
865
|
-
if (!isSetupSafe) {
|
|
866
|
-
path.replaceWith((0, ast_helpers_1.createTCall)(key));
|
|
867
|
-
injections.push({
|
|
868
|
-
file: filePath,
|
|
869
|
-
line,
|
|
870
|
-
column: path.node.loc?.start.column || 0,
|
|
871
|
-
original: text,
|
|
872
|
-
replacement: `t('${key}')`,
|
|
873
|
-
key,
|
|
874
|
-
confidence: 100,
|
|
875
|
-
});
|
|
876
|
-
needsHook = true;
|
|
1020
|
+
const source = translatorFor(path);
|
|
1021
|
+
if (!source)
|
|
877
1022
|
return;
|
|
878
|
-
|
|
879
|
-
const tFunctionName = nameFor(path);
|
|
1023
|
+
const tFunctionName = pickTranslator(path, source);
|
|
880
1024
|
path.replaceWith(useTranslatorName((0, ast_helpers_1.createTCall)(key), tFunctionName));
|
|
881
1025
|
injections.push({
|
|
882
1026
|
file: filePath,
|
|
@@ -928,10 +1072,19 @@ positions) {
|
|
|
928
1072
|
const decl = activeStrategy.createImport(componentContext);
|
|
929
1073
|
neededImports.set(importSignature(decl), decl);
|
|
930
1074
|
});
|
|
931
|
-
|
|
1075
|
+
// A hook already in place (no new hook) still needs its import; vanilla
|
|
1076
|
+
// files need the config's `t`. Module-level t() in a hook-strategy file
|
|
1077
|
+
// imports `t` from the config next to the hook's import.
|
|
1078
|
+
const strategyUsed = activeStrategy.id === 'vanilla' ? moduleTranslatorUsed : hookTranslatorUsed;
|
|
1079
|
+
// A file that already binds `t` (an earlier run's `import { t } from '@/i18n'`) keeps it.
|
|
1080
|
+
const tBound = (0, ast_helpers_1.bindsTopLevel)(ast, 't');
|
|
1081
|
+
if (neededImports.size === 0 && strategyUsed && !activeStrategy.hasImport(ast) && !(activeStrategy.id === 'vanilla' && tBound)) {
|
|
932
1082
|
const decl = activeStrategy.createImport(fileContext);
|
|
933
1083
|
neededImports.set(importSignature(decl), decl);
|
|
934
1084
|
}
|
|
1085
|
+
if (moduleTranslatorUsed && activeStrategy.id !== 'vanilla' && moduleTranslator && !tBound) {
|
|
1086
|
+
extraImports.push({ source: moduleTranslator, name: 't' });
|
|
1087
|
+
}
|
|
935
1088
|
for (const decl of [...neededImports.values()].reverse()) {
|
|
936
1089
|
if (hasImportSpecifiers(ast, decl))
|
|
937
1090
|
continue;
|