feat(eslint): add no-jsx-text-literals rule + tests (fixes #6472)

Signed-off-by: sharvandeep <2400032427@kluniversity.in>
This commit is contained in:
sharvandeep 2025-10-30 21:27:43 +05:30
parent f8bedbd0a0
commit 2306941eda
No known key found for this signature in database
GPG Key ID: 75A43C246E1F30C0
4 changed files with 142 additions and 0 deletions

View File

@ -0,0 +1,5 @@
module.exports = {
rules: {
'no-jsx-text-literals': require('./rules/no-jsx-text-literals'),
},
};

View File

@ -0,0 +1,93 @@
/**
* @fileoverview Disallow plain text literals in JSX to encourage use of i18n/Translate
*/
'use strict';
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow literal text in JSX (encourage <Translate> or i18n usage)',
category: 'Best Practices',
recommended: false,
},
schema: [], // no options
messages: {
noJsxText: 'Avoid using raw text in JSX — wrap translatable text with <Translate> or use i18n APIs.',
},
},
create(context) {
// helper: find nearest ancestor JSX element name (if any)
function findAncestorElementName(node) {
let p = node.parent;
while (p) {
if (p.type === 'JSXElement' && p.openingElement && p.openingElement.name) {
const nameNode = p.openingElement.name;
// handle simple identifier names only (e.g., Translate, div)
if (nameNode.type === 'JSXIdentifier' && nameNode.name) {
return nameNode.name;
}
// handle MemberExpressions like Some.Namespace.Component (rare in JSX)
if (nameNode.type === 'JSXMemberExpression') {
let parts = [];
let curr = nameNode;
while (curr) {
if (curr.property && curr.property.name) parts.unshift(curr.property.name);
if (curr.object && curr.object.name) { parts.unshift(curr.object.name); break; }
curr = curr.object;
}
return parts.join('.');
}
}
p = p.parent;
}
return null;
}
const ignoreTags = new Set(['code', 'pre', 'script', 'style', 'Translate']);
return {
JSXText(node) {
if (!node || !node.value) {
return;
}
const text = node.value.trim();
if (!text) {
return;
}
// If inside an ignored tag (including Translate), skip
const ancestorName = findAncestorElementName(node);
if (ancestorName && ignoreTags.has(ancestorName)) {
return;
}
context.report({
node,
messageId: 'noJsxText',
});
},
Literal(node) {
// Flag string literals used as children: e.g. { 'Hello' }
if (typeof node.value === 'string' && node.parent && node.parent.type === 'JSXExpressionContainer') {
const text = node.value.trim();
if (!text) return;
// If inside ignored tag (including Translate), skip
const ancestorName = findAncestorElementName(node);
if (ancestorName && ignoreTags.has(ancestorName)) {
return;
}
context.report({
node,
messageId: 'noJsxText',
});
}
},
};
},
};

View File

@ -0,0 +1,13 @@
{
"name": "eslint-plugin-docusaurus-local",
"version": "0.0.0",
"private": true,
"main": "lib/index.js",
"scripts": {
"test": "mocha tests/*.test.js --timeout 5000"
},
"devDependencies": {
"eslint": "^8.57.1",
"mocha": "^10.8.2"
}
}

View File

@ -0,0 +1,31 @@
'use strict';
const RuleTester = require('eslint').RuleTester;
const rule = require('../lib/rules/no-jsx-text-literals');
const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true } },
});
ruleTester.run('no-jsx-text-literals', rule, {
valid: [
// using Translate component
{ code: "const a = <Translate>Hello</Translate>;" },
// whitespace-only
{ code: "const a = <div> </div>;" },
// code/pre tags
{ code: "const a = <pre>some code</pre>;" },
// dynamic expression
{ code: "const a = <div>{message}</div>;" },
],
invalid: [
{
code: "const a = <div>Hello world</div>;",
errors: [{ messageId: 'noJsxText' }],
},
{
code: "const a = <p>{'Plain text'}</p>;",
errors: [{ messageId: 'noJsxText' }],
},
],
});