i18next-cli 1.56.1 → 1.56.3
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/cjs/cli.js +1 -1
- package/dist/cjs/extractor/parsers/jsx-handler.js +59 -0
- package/dist/cjs/extractor/parsers/scope-manager.js +29 -8
- package/dist/cjs/linter.js +27 -4
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/parsers/jsx-handler.js +59 -0
- package/dist/esm/extractor/parsers/scope-manager.js +29 -8
- package/dist/esm/linter.js +27 -4
- package/package.json +1 -1
- package/types/extractor/parsers/jsx-handler.d.ts +18 -0
- package/types/extractor/parsers/jsx-handler.d.ts.map +1 -1
- package/types/extractor/parsers/scope-manager.d.ts +8 -0
- package/types/extractor/parsers/scope-manager.d.ts.map +1 -1
package/dist/cjs/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ const program = new commander.Command();
|
|
|
32
32
|
program
|
|
33
33
|
.name('i18next-cli')
|
|
34
34
|
.description('A unified, high-performance i18next CLI.')
|
|
35
|
-
.version('1.56.
|
|
35
|
+
.version('1.56.3'); // This string is replaced with the actual version at build time by rollup
|
|
36
36
|
// new: global config override option
|
|
37
37
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
38
38
|
program
|
|
@@ -30,6 +30,64 @@ class JSXHandler {
|
|
|
30
30
|
return undefined;
|
|
31
31
|
return astUtils.lineColumnFromOffset(this.getCurrentCode(), node.span.start);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Warns about `<Trans>Hello <b>{name}</b></Trans>` style children where a
|
|
35
|
+
* bare identifier is used as a React child. react-i18next inlines the value
|
|
36
|
+
* at runtime, producing a key like `"Hello <1>meow</1>"`, but the extractor
|
|
37
|
+
* serializes the identifier name as `"{{name}}"`. The two never match, and
|
|
38
|
+
* even when an `i18nKey` is set, the placeholder `{{name}}` cannot be
|
|
39
|
+
* interpolated without a `values={{ name }}` prop — it renders literally.
|
|
40
|
+
*
|
|
41
|
+
* We keep the existing extraction behaviour so projects that already rely on
|
|
42
|
+
* the `{{name}}` output (with a matching `values` prop) aren't broken, and
|
|
43
|
+
* instead surface a diagnostic pointing users at the runtime mismatch.
|
|
44
|
+
*/
|
|
45
|
+
warnOnBareIdentifierTransChildren(node, elementName) {
|
|
46
|
+
const bareIdentifiers = [];
|
|
47
|
+
const visit = (children) => {
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
50
|
+
const inner = this.unwrapExpression(child.expression);
|
|
51
|
+
if (inner && inner.type === 'Identifier') {
|
|
52
|
+
bareIdentifiers.push({ name: inner.value, span: child.span });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (child.type === 'JSXElement') {
|
|
56
|
+
visit(child.children);
|
|
57
|
+
}
|
|
58
|
+
else if (child.type === 'JSXFragment') {
|
|
59
|
+
visit(child.children);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
visit(node.children);
|
|
64
|
+
if (bareIdentifiers.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const warn = this.pluginContext?.logger?.warn?.bind(this.pluginContext.logger) ??
|
|
67
|
+
console.warn.bind(console);
|
|
68
|
+
for (const { name, span } of bareIdentifiers) {
|
|
69
|
+
const loc = astUtils.lineColumnFromOffset(this.getCurrentCode(), span.start);
|
|
70
|
+
const where = loc
|
|
71
|
+
? `${this.getCurrentFile()}:${loc.line}:${loc.column}`
|
|
72
|
+
: this.getCurrentFile();
|
|
73
|
+
warn(`<${elementName}> child {${name}} at ${where} won't match at runtime — react-i18next inlines the value (e.g. "<1>meow</1>"), but extraction produces "<1>{{${name}}}</1>". Use {{${name}}} (double braces) with values={{ ${name} }} for interpolation, or inline the value if it isn't meant to be translated.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Unwraps TS type-assertion and parenthesis wrappers so we can inspect the
|
|
78
|
+
* underlying expression type (mirrors behaviour in jsx-parser).
|
|
79
|
+
*/
|
|
80
|
+
unwrapExpression(expr) {
|
|
81
|
+
if (!expr)
|
|
82
|
+
return expr;
|
|
83
|
+
if (expr.type === 'TsAsExpression' || expr.type === 'TsSatisfiesExpression') {
|
|
84
|
+
return this.unwrapExpression(expr.expression);
|
|
85
|
+
}
|
|
86
|
+
if (expr.type === 'ParenthesisExpression') {
|
|
87
|
+
return this.unwrapExpression(expr.expression);
|
|
88
|
+
}
|
|
89
|
+
return expr;
|
|
90
|
+
}
|
|
33
91
|
/**
|
|
34
92
|
* Processes JSX elements to extract translation keys from Trans components.
|
|
35
93
|
*
|
|
@@ -42,6 +100,7 @@ class JSXHandler {
|
|
|
42
100
|
handleJSXElement(node, getScopeInfo) {
|
|
43
101
|
const elementName = this.getElementName(node);
|
|
44
102
|
if (elementName && (this.config.extract.transComponents || ['Trans']).includes(elementName)) {
|
|
103
|
+
this.warnOnBareIdentifierTransChildren(node, elementName);
|
|
45
104
|
let extractedAttributes = null;
|
|
46
105
|
try {
|
|
47
106
|
extractedAttributes = jsxParser.extractFromTransComponent(node, this.config);
|
|
@@ -486,14 +486,37 @@ class ScopeManager {
|
|
|
486
486
|
const args = callExpr.arguments;
|
|
487
487
|
// getFixedT(lng, ns, keyPrefix)
|
|
488
488
|
// We ignore the first argument (lng) for key extraction.
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
const defaultNs = (nsArg?.type === 'StringLiteral') ? nsArg.value : undefined;
|
|
492
|
-
const keyPrefix = (keyPrefixArg?.type === 'StringLiteral') ? keyPrefixArg.value : undefined;
|
|
489
|
+
const defaultNs = this.resolveStringArg(args[1]?.expression);
|
|
490
|
+
const keyPrefix = this.resolveStringArg(args[2]?.expression);
|
|
493
491
|
if (defaultNs || keyPrefix) {
|
|
494
492
|
this.setVarInScope(variableName, { defaultNs, keyPrefix });
|
|
495
493
|
}
|
|
496
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Resolves a call argument expression to a string value by handling string
|
|
497
|
+
* literals, previously-declared local/shared constants (Identifier), member
|
|
498
|
+
* expressions referencing constant objects, and simple template literals
|
|
499
|
+
* without interpolation. Returns undefined when the expression cannot be
|
|
500
|
+
* resolved statically.
|
|
501
|
+
*/
|
|
502
|
+
resolveStringArg(node) {
|
|
503
|
+
if (!node)
|
|
504
|
+
return undefined;
|
|
505
|
+
const unwrapped = ScopeManager.unwrapTsExpression(node);
|
|
506
|
+
if (unwrapped.type === 'StringLiteral')
|
|
507
|
+
return unwrapped.value;
|
|
508
|
+
if (unwrapped.type === 'Identifier')
|
|
509
|
+
return this.resolveSimpleStringIdentifier(unwrapped.value);
|
|
510
|
+
if (unwrapped.type === 'MemberExpression')
|
|
511
|
+
return this.resolveSimpleMemberExpression(unwrapped);
|
|
512
|
+
if (unwrapped.type === 'TemplateLiteral') {
|
|
513
|
+
const tpl = unwrapped;
|
|
514
|
+
if ((tpl.expressions || []).length === 0) {
|
|
515
|
+
return tpl.quasis?.[0]?.cooked ?? undefined;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
497
520
|
/**
|
|
498
521
|
* Handles cases where a getFixedT-like function is a variable (from a custom hook)
|
|
499
522
|
* and is invoked to produce a bound `t` function, e.g.:
|
|
@@ -513,10 +536,8 @@ class ScopeManager {
|
|
|
513
536
|
return;
|
|
514
537
|
const args = callExpr.arguments;
|
|
515
538
|
// getFixedT(lng, ns, keyPrefix)
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
const nsFromCall = (nsArg?.type === 'StringLiteral') ? nsArg.value : undefined;
|
|
519
|
-
const keyPrefixFromCall = (keyPrefixArg?.type === 'StringLiteral') ? keyPrefixArg.value : undefined;
|
|
539
|
+
const nsFromCall = this.resolveStringArg(args[1]?.expression);
|
|
540
|
+
const keyPrefixFromCall = this.resolveStringArg(args[2]?.expression);
|
|
520
541
|
// Merge: call args take precedence over source scope values
|
|
521
542
|
const finalNs = nsFromCall ?? sourceScope.defaultNs;
|
|
522
543
|
const finalKeyPrefix = keyPrefixFromCall ?? sourceScope.keyPrefix;
|
package/dist/cjs/linter.js
CHANGED
|
@@ -755,14 +755,22 @@ function findHardcodedStrings(ast, code, config) {
|
|
|
755
755
|
}
|
|
756
756
|
if (node.type === 'StringLiteral') {
|
|
757
757
|
const parent = currentAncestors[currentAncestors.length - 2];
|
|
758
|
+
const grandparent = currentAncestors[currentAncestors.length - 3];
|
|
758
759
|
// Determine whether this attribute is inside any ignored element (handles nested Trans etc.)
|
|
759
760
|
const insideIgnored = isWithinIgnoredElement(currentAncestors);
|
|
760
|
-
|
|
761
|
-
|
|
761
|
+
// A StringLiteral can be an attribute value in two forms:
|
|
762
|
+
// <tag attr="value" /> → parent is JSXAttribute
|
|
763
|
+
// <tag attr={"value"} /> → parent is JSXExpressionContainer, grandparent is JSXAttribute
|
|
764
|
+
const attrNode = parent?.type === 'JSXAttribute'
|
|
765
|
+
? parent
|
|
766
|
+
: (parent?.type === 'JSXExpressionContainer' && grandparent?.type === 'JSXAttribute' ? grandparent : null);
|
|
767
|
+
if (attrNode && !insideIgnored) {
|
|
768
|
+
const rawAttrName = extractAttrName(attrNode.name);
|
|
762
769
|
const attrNameLower = rawAttrName ? String(rawAttrName).toLowerCase() : null;
|
|
763
770
|
// Check tag-level acceptance if acceptedTagsSet provided: attributes should only be considered
|
|
764
|
-
// when the nearest enclosing element is accepted.
|
|
765
|
-
const
|
|
771
|
+
// when the nearest enclosing element is accepted. Use the ancestors above the attrNode.
|
|
772
|
+
const attrNodeIdx = currentAncestors.indexOf(attrNode);
|
|
773
|
+
const parentElement = currentAncestors.slice(0, attrNodeIdx).reverse().find(a => a && typeof a === 'object' && (a.type === 'JSXElement' || a.type === 'JSXOpeningElement' || a.type === 'JSXSelfClosingElement'));
|
|
766
774
|
if (acceptedTagsSet && parentElement) {
|
|
767
775
|
const parentName = extractJSXName(parentElement);
|
|
768
776
|
if (!parentName || !acceptedTagsSet.has(String(parentName).toLowerCase())) {
|
|
@@ -786,6 +794,21 @@ function findHardcodedStrings(ast, code, config) {
|
|
|
786
794
|
}
|
|
787
795
|
}
|
|
788
796
|
}
|
|
797
|
+
// Hardcoded string inside a JSX expression container used as element child:
|
|
798
|
+
// <tag>{"hello"}</tag> → parent is JSXExpressionContainer, grandparent is JSXElement/JSXFragment
|
|
799
|
+
// Apply the same filters used for JSXText so this behaves like raw text.
|
|
800
|
+
const isJsxChildExpression = parent?.type === 'JSXExpressionContainer' &&
|
|
801
|
+
(grandparent?.type === 'JSXElement' || grandparent?.type === 'JSXFragment');
|
|
802
|
+
if (isJsxChildExpression && !insideIgnored) {
|
|
803
|
+
// Respect attribute-only mode: when acceptedAttributes is set without acceptedTags,
|
|
804
|
+
// only attribute strings are linted — skip JSX child text.
|
|
805
|
+
if (!(acceptedAttributesSet && !acceptedTagsSet)) {
|
|
806
|
+
const text = node.value.trim();
|
|
807
|
+
if (text && text.length > 1 && text !== '...' && !isUrlOrPath(text) && isNaN(Number(text)) && !text.startsWith('{{')) {
|
|
808
|
+
nodesToLint.push(node);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
789
812
|
}
|
|
790
813
|
// Recurse into children
|
|
791
814
|
for (const key of Object.keys(node)) {
|
package/dist/esm/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ const program = new Command();
|
|
|
30
30
|
program
|
|
31
31
|
.name('i18next-cli')
|
|
32
32
|
.description('A unified, high-performance i18next CLI.')
|
|
33
|
-
.version('1.56.
|
|
33
|
+
.version('1.56.3'); // This string is replaced with the actual version at build time by rollup
|
|
34
34
|
// new: global config override option
|
|
35
35
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
36
36
|
program
|
|
@@ -28,6 +28,64 @@ class JSXHandler {
|
|
|
28
28
|
return undefined;
|
|
29
29
|
return lineColumnFromOffset(this.getCurrentCode(), node.span.start);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Warns about `<Trans>Hello <b>{name}</b></Trans>` style children where a
|
|
33
|
+
* bare identifier is used as a React child. react-i18next inlines the value
|
|
34
|
+
* at runtime, producing a key like `"Hello <1>meow</1>"`, but the extractor
|
|
35
|
+
* serializes the identifier name as `"{{name}}"`. The two never match, and
|
|
36
|
+
* even when an `i18nKey` is set, the placeholder `{{name}}` cannot be
|
|
37
|
+
* interpolated without a `values={{ name }}` prop — it renders literally.
|
|
38
|
+
*
|
|
39
|
+
* We keep the existing extraction behaviour so projects that already rely on
|
|
40
|
+
* the `{{name}}` output (with a matching `values` prop) aren't broken, and
|
|
41
|
+
* instead surface a diagnostic pointing users at the runtime mismatch.
|
|
42
|
+
*/
|
|
43
|
+
warnOnBareIdentifierTransChildren(node, elementName) {
|
|
44
|
+
const bareIdentifiers = [];
|
|
45
|
+
const visit = (children) => {
|
|
46
|
+
for (const child of children) {
|
|
47
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
48
|
+
const inner = this.unwrapExpression(child.expression);
|
|
49
|
+
if (inner && inner.type === 'Identifier') {
|
|
50
|
+
bareIdentifiers.push({ name: inner.value, span: child.span });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (child.type === 'JSXElement') {
|
|
54
|
+
visit(child.children);
|
|
55
|
+
}
|
|
56
|
+
else if (child.type === 'JSXFragment') {
|
|
57
|
+
visit(child.children);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
visit(node.children);
|
|
62
|
+
if (bareIdentifiers.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
const warn = this.pluginContext?.logger?.warn?.bind(this.pluginContext.logger) ??
|
|
65
|
+
console.warn.bind(console);
|
|
66
|
+
for (const { name, span } of bareIdentifiers) {
|
|
67
|
+
const loc = lineColumnFromOffset(this.getCurrentCode(), span.start);
|
|
68
|
+
const where = loc
|
|
69
|
+
? `${this.getCurrentFile()}:${loc.line}:${loc.column}`
|
|
70
|
+
: this.getCurrentFile();
|
|
71
|
+
warn(`<${elementName}> child {${name}} at ${where} won't match at runtime — react-i18next inlines the value (e.g. "<1>meow</1>"), but extraction produces "<1>{{${name}}}</1>". Use {{${name}}} (double braces) with values={{ ${name} }} for interpolation, or inline the value if it isn't meant to be translated.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Unwraps TS type-assertion and parenthesis wrappers so we can inspect the
|
|
76
|
+
* underlying expression type (mirrors behaviour in jsx-parser).
|
|
77
|
+
*/
|
|
78
|
+
unwrapExpression(expr) {
|
|
79
|
+
if (!expr)
|
|
80
|
+
return expr;
|
|
81
|
+
if (expr.type === 'TsAsExpression' || expr.type === 'TsSatisfiesExpression') {
|
|
82
|
+
return this.unwrapExpression(expr.expression);
|
|
83
|
+
}
|
|
84
|
+
if (expr.type === 'ParenthesisExpression') {
|
|
85
|
+
return this.unwrapExpression(expr.expression);
|
|
86
|
+
}
|
|
87
|
+
return expr;
|
|
88
|
+
}
|
|
31
89
|
/**
|
|
32
90
|
* Processes JSX elements to extract translation keys from Trans components.
|
|
33
91
|
*
|
|
@@ -40,6 +98,7 @@ class JSXHandler {
|
|
|
40
98
|
handleJSXElement(node, getScopeInfo) {
|
|
41
99
|
const elementName = this.getElementName(node);
|
|
42
100
|
if (elementName && (this.config.extract.transComponents || ['Trans']).includes(elementName)) {
|
|
101
|
+
this.warnOnBareIdentifierTransChildren(node, elementName);
|
|
43
102
|
let extractedAttributes = null;
|
|
44
103
|
try {
|
|
45
104
|
extractedAttributes = extractFromTransComponent(node, this.config);
|
|
@@ -484,14 +484,37 @@ class ScopeManager {
|
|
|
484
484
|
const args = callExpr.arguments;
|
|
485
485
|
// getFixedT(lng, ns, keyPrefix)
|
|
486
486
|
// We ignore the first argument (lng) for key extraction.
|
|
487
|
-
const
|
|
488
|
-
const
|
|
489
|
-
const defaultNs = (nsArg?.type === 'StringLiteral') ? nsArg.value : undefined;
|
|
490
|
-
const keyPrefix = (keyPrefixArg?.type === 'StringLiteral') ? keyPrefixArg.value : undefined;
|
|
487
|
+
const defaultNs = this.resolveStringArg(args[1]?.expression);
|
|
488
|
+
const keyPrefix = this.resolveStringArg(args[2]?.expression);
|
|
491
489
|
if (defaultNs || keyPrefix) {
|
|
492
490
|
this.setVarInScope(variableName, { defaultNs, keyPrefix });
|
|
493
491
|
}
|
|
494
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Resolves a call argument expression to a string value by handling string
|
|
495
|
+
* literals, previously-declared local/shared constants (Identifier), member
|
|
496
|
+
* expressions referencing constant objects, and simple template literals
|
|
497
|
+
* without interpolation. Returns undefined when the expression cannot be
|
|
498
|
+
* resolved statically.
|
|
499
|
+
*/
|
|
500
|
+
resolveStringArg(node) {
|
|
501
|
+
if (!node)
|
|
502
|
+
return undefined;
|
|
503
|
+
const unwrapped = ScopeManager.unwrapTsExpression(node);
|
|
504
|
+
if (unwrapped.type === 'StringLiteral')
|
|
505
|
+
return unwrapped.value;
|
|
506
|
+
if (unwrapped.type === 'Identifier')
|
|
507
|
+
return this.resolveSimpleStringIdentifier(unwrapped.value);
|
|
508
|
+
if (unwrapped.type === 'MemberExpression')
|
|
509
|
+
return this.resolveSimpleMemberExpression(unwrapped);
|
|
510
|
+
if (unwrapped.type === 'TemplateLiteral') {
|
|
511
|
+
const tpl = unwrapped;
|
|
512
|
+
if ((tpl.expressions || []).length === 0) {
|
|
513
|
+
return tpl.quasis?.[0]?.cooked ?? undefined;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
495
518
|
/**
|
|
496
519
|
* Handles cases where a getFixedT-like function is a variable (from a custom hook)
|
|
497
520
|
* and is invoked to produce a bound `t` function, e.g.:
|
|
@@ -511,10 +534,8 @@ class ScopeManager {
|
|
|
511
534
|
return;
|
|
512
535
|
const args = callExpr.arguments;
|
|
513
536
|
// getFixedT(lng, ns, keyPrefix)
|
|
514
|
-
const
|
|
515
|
-
const
|
|
516
|
-
const nsFromCall = (nsArg?.type === 'StringLiteral') ? nsArg.value : undefined;
|
|
517
|
-
const keyPrefixFromCall = (keyPrefixArg?.type === 'StringLiteral') ? keyPrefixArg.value : undefined;
|
|
537
|
+
const nsFromCall = this.resolveStringArg(args[1]?.expression);
|
|
538
|
+
const keyPrefixFromCall = this.resolveStringArg(args[2]?.expression);
|
|
518
539
|
// Merge: call args take precedence over source scope values
|
|
519
540
|
const finalNs = nsFromCall ?? sourceScope.defaultNs;
|
|
520
541
|
const finalKeyPrefix = keyPrefixFromCall ?? sourceScope.keyPrefix;
|
package/dist/esm/linter.js
CHANGED
|
@@ -753,14 +753,22 @@ function findHardcodedStrings(ast, code, config) {
|
|
|
753
753
|
}
|
|
754
754
|
if (node.type === 'StringLiteral') {
|
|
755
755
|
const parent = currentAncestors[currentAncestors.length - 2];
|
|
756
|
+
const grandparent = currentAncestors[currentAncestors.length - 3];
|
|
756
757
|
// Determine whether this attribute is inside any ignored element (handles nested Trans etc.)
|
|
757
758
|
const insideIgnored = isWithinIgnoredElement(currentAncestors);
|
|
758
|
-
|
|
759
|
-
|
|
759
|
+
// A StringLiteral can be an attribute value in two forms:
|
|
760
|
+
// <tag attr="value" /> → parent is JSXAttribute
|
|
761
|
+
// <tag attr={"value"} /> → parent is JSXExpressionContainer, grandparent is JSXAttribute
|
|
762
|
+
const attrNode = parent?.type === 'JSXAttribute'
|
|
763
|
+
? parent
|
|
764
|
+
: (parent?.type === 'JSXExpressionContainer' && grandparent?.type === 'JSXAttribute' ? grandparent : null);
|
|
765
|
+
if (attrNode && !insideIgnored) {
|
|
766
|
+
const rawAttrName = extractAttrName(attrNode.name);
|
|
760
767
|
const attrNameLower = rawAttrName ? String(rawAttrName).toLowerCase() : null;
|
|
761
768
|
// Check tag-level acceptance if acceptedTagsSet provided: attributes should only be considered
|
|
762
|
-
// when the nearest enclosing element is accepted.
|
|
763
|
-
const
|
|
769
|
+
// when the nearest enclosing element is accepted. Use the ancestors above the attrNode.
|
|
770
|
+
const attrNodeIdx = currentAncestors.indexOf(attrNode);
|
|
771
|
+
const parentElement = currentAncestors.slice(0, attrNodeIdx).reverse().find(a => a && typeof a === 'object' && (a.type === 'JSXElement' || a.type === 'JSXOpeningElement' || a.type === 'JSXSelfClosingElement'));
|
|
764
772
|
if (acceptedTagsSet && parentElement) {
|
|
765
773
|
const parentName = extractJSXName(parentElement);
|
|
766
774
|
if (!parentName || !acceptedTagsSet.has(String(parentName).toLowerCase())) {
|
|
@@ -784,6 +792,21 @@ function findHardcodedStrings(ast, code, config) {
|
|
|
784
792
|
}
|
|
785
793
|
}
|
|
786
794
|
}
|
|
795
|
+
// Hardcoded string inside a JSX expression container used as element child:
|
|
796
|
+
// <tag>{"hello"}</tag> → parent is JSXExpressionContainer, grandparent is JSXElement/JSXFragment
|
|
797
|
+
// Apply the same filters used for JSXText so this behaves like raw text.
|
|
798
|
+
const isJsxChildExpression = parent?.type === 'JSXExpressionContainer' &&
|
|
799
|
+
(grandparent?.type === 'JSXElement' || grandparent?.type === 'JSXFragment');
|
|
800
|
+
if (isJsxChildExpression && !insideIgnored) {
|
|
801
|
+
// Respect attribute-only mode: when acceptedAttributes is set without acceptedTags,
|
|
802
|
+
// only attribute strings are linted — skip JSX child text.
|
|
803
|
+
if (!(acceptedAttributesSet && !acceptedTagsSet)) {
|
|
804
|
+
const text = node.value.trim();
|
|
805
|
+
if (text && text.length > 1 && text !== '...' && !isUrlOrPath(text) && isNaN(Number(text)) && !text.startsWith('{{')) {
|
|
806
|
+
nodesToLint.push(node);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
787
810
|
}
|
|
788
811
|
// Recurse into children
|
|
789
812
|
for (const key of Object.keys(node)) {
|
package/package.json
CHANGED
|
@@ -14,6 +14,24 @@ export declare class JSXHandler {
|
|
|
14
14
|
* so we can use them directly.
|
|
15
15
|
*/
|
|
16
16
|
private getLocationFromNode;
|
|
17
|
+
/**
|
|
18
|
+
* Warns about `<Trans>Hello <b>{name}</b></Trans>` style children where a
|
|
19
|
+
* bare identifier is used as a React child. react-i18next inlines the value
|
|
20
|
+
* at runtime, producing a key like `"Hello <1>meow</1>"`, but the extractor
|
|
21
|
+
* serializes the identifier name as `"{{name}}"`. The two never match, and
|
|
22
|
+
* even when an `i18nKey` is set, the placeholder `{{name}}` cannot be
|
|
23
|
+
* interpolated without a `values={{ name }}` prop — it renders literally.
|
|
24
|
+
*
|
|
25
|
+
* We keep the existing extraction behaviour so projects that already rely on
|
|
26
|
+
* the `{{name}}` output (with a matching `values` prop) aren't broken, and
|
|
27
|
+
* instead surface a diagnostic pointing users at the runtime mismatch.
|
|
28
|
+
*/
|
|
29
|
+
private warnOnBareIdentifierTransChildren;
|
|
30
|
+
/**
|
|
31
|
+
* Unwraps TS type-assertion and parenthesis wrappers so we can inspect the
|
|
32
|
+
* underlying expression type (mirrors behaviour in jsx-parser).
|
|
33
|
+
*/
|
|
34
|
+
private unwrapExpression;
|
|
17
35
|
/**
|
|
18
36
|
* Processes JSX elements to extract translation keys from Trans components.
|
|
19
37
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsx-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/jsx-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"jsx-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/jsx-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,UAAU,EAAqC,MAAM,WAAW,CAAA;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAgB,MAAM,gBAAgB,CAAA;AACvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAS7D,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAAoB;IAC9C,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;gBAGlC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM;IAS9B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAK3B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,iCAAiC;IAgCzC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;;;;;;;OAQG;IACH,gBAAgB,CAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,GAAG,IAAI;IA2UjI;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,0BAA0B;IAkIlC;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;CAevB"}
|
|
@@ -112,6 +112,14 @@ export declare class ScopeManager {
|
|
|
112
112
|
* @param callExpr - The CallExpression node representing the getFixedT invocation
|
|
113
113
|
*/
|
|
114
114
|
private handleGetFixedTDeclarator;
|
|
115
|
+
/**
|
|
116
|
+
* Resolves a call argument expression to a string value by handling string
|
|
117
|
+
* literals, previously-declared local/shared constants (Identifier), member
|
|
118
|
+
* expressions referencing constant objects, and simple template literals
|
|
119
|
+
* without interpolation. Returns undefined when the expression cannot be
|
|
120
|
+
* resolved statically.
|
|
121
|
+
*/
|
|
122
|
+
private resolveStringArg;
|
|
115
123
|
/**
|
|
116
124
|
* Handles cases where a getFixedT-like function is a variable (from a custom hook)
|
|
117
125
|
* and is invoked to produce a bound `t` function, e.g.:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/scope-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,kBAAkB,EAMnB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,SAAS,EAA4B,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAG/F,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,KAAK,CAAqE;IAGlF,OAAO,CAAC,eAAe,CAAiC;IAGxD,OAAO,CAAC,qBAAqB,CAAiD;IAI9E,OAAO,CAAC,eAAe,CAAiC;IACxD,OAAO,CAAC,qBAAqB,CAAiD;gBAEjE,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC;IAI1D;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAcjC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,+BAA+B;IAgB9C;;;;;;OAMG;IACI,KAAK,IAAK,IAAI;IAOrB;;;OAGG;IACH,UAAU,IAAK,IAAI;IAInB;;;OAGG;IACH,SAAS,IAAK,IAAI;IAIlB;;;;;;OAMG;IACH,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAUnD;;;;;;OAMG;IACH,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAkBrD,OAAO,CAAC,uBAAuB;IAoB/B;;OAEG;IACI,6BAA6B,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIvE;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IAqBrC;;;;;;;;;;OAUG;IACH,wBAAwB,CAAE,IAAI,EAAE,kBAAkB,GAAG,IAAI;IA0FzD;;;;;;;;OAQG;IACH,OAAO,CAAC,+BAA+B;IA0GvC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,8BAA8B;IAmFtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,yBAAyB;
|
|
1
|
+
{"version":3,"file":"scope-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/scope-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,kBAAkB,EAMnB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,SAAS,EAA4B,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAG/F,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,KAAK,CAAqE;IAGlF,OAAO,CAAC,eAAe,CAAiC;IAGxD,OAAO,CAAC,qBAAqB,CAAiD;IAI9E,OAAO,CAAC,eAAe,CAAiC;IACxD,OAAO,CAAC,qBAAqB,CAAiD;gBAEjE,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC;IAI1D;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAcjC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,+BAA+B;IAgB9C;;;;;;OAMG;IACI,KAAK,IAAK,IAAI;IAOrB;;;OAGG;IACH,UAAU,IAAK,IAAI;IAInB;;;OAGG;IACH,SAAS,IAAK,IAAI;IAIlB;;;;;;OAMG;IACH,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAUnD;;;;;;OAMG;IACH,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAkBrD,OAAO,CAAC,uBAAuB;IAoB/B;;OAEG;IACI,6BAA6B,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIvE;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IAqBrC;;;;;;;;;;OAUG;IACH,wBAAwB,CAAE,IAAI,EAAE,kBAAkB,GAAG,IAAI;IA0FzD;;;;;;;;OAQG;IACH,OAAO,CAAC,+BAA+B;IA0GvC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,8BAA8B;IAmFtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,yBAAyB;IAiBjC;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAexB;;;;;;;;;OASG;IACH,OAAO,CAAC,qCAAqC;CAoB9C"}
|