@generaltranslation/gt-next-lint
Version:
ESLint plugin for General Translation Next.js integration
209 lines • 9.49 kB
JavaScript
;
/**
* ESLint rule: no-dynamic-jsx
*
* Detects unwrapped dynamic content in GT-Next translation components.
* Equivalent to the SWC plugin functionality but with proper ESLint error reporting.
*
* This rule checks for JSX expressions ({dynamic content}) inside <T> components
* that are not wrapped in variable components (<Var>, <DateTime>, <Num>, <Currency>).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.noDynamicJsx = void 0;
const GT_MODULES = ['gt-next', 'gt-next/client', 'gt-next/server'];
const TRANSLATION_COMPONENTS = ['T'];
const VARIABLE_COMPONENTS = ['Var', 'DateTime', 'Num', 'Currency'];
function isGTModule(source) {
return GT_MODULES.includes(source);
}
function isTranslationComponent(name) {
return TRANSLATION_COMPONENTS.includes(name);
}
function isVariableComponent(name) {
return VARIABLE_COMPONENTS.includes(name);
}
exports.noDynamicJsx = {
meta: {
type: 'problem',
docs: {
description: 'Detect unwrapped dynamic content in GT-Next translation components',
category: 'Best Practices',
recommended: true,
url: 'https://github.com/generaltranslation/gt/tree/main/packages/next-lint#no-dynamic-jsx',
},
fixable: undefined,
schema: [],
messages: {
dynamicJsx: 'Dynamic content in <T> component should be wrapped in a variable component (<Var>, <DateTime>, <Num>, or <Currency>)',
},
},
create(context) {
const state = {
translationStack: 0,
variableStack: 0,
imports: {
translationComponents: new Set(),
variableComponents: new Set(),
namespaceImports: new Set(),
assignedTranslationComponents: new Set(),
assignedVariableComponents: new Set(),
},
};
function getElementName(node) {
const name = node?.openingElement?.name;
if (!name)
return null;
if (name.type === 'JSXIdentifier') {
return name.name;
}
if (name.type === 'JSXMemberExpression') {
const obj = name.object;
const prop = name.property;
if (obj?.type === 'JSXIdentifier' && prop?.type === 'JSXIdentifier') {
return `${obj.name}.${prop.name}`;
}
}
return null;
}
function isNamespaceTranslationComponent(elementName) {
const parts = elementName.split('.');
if (parts.length === 2) {
const [namespace, component] = parts;
return (state.imports.namespaceImports.has(namespace) &&
isTranslationComponent(component));
}
return false;
}
function isNamespaceVariableComponent(elementName) {
const parts = elementName.split('.');
if (parts.length === 2) {
const [namespace, component] = parts;
return (state.imports.namespaceImports.has(namespace) &&
isVariableComponent(component));
}
return false;
}
function isInsideJSXAttribute(node) {
// Walk up the AST to check if this expression is inside a JSX attribute
let currentNode = node.parent;
while (currentNode) {
if (currentNode.type === 'JSXAttribute') {
return true;
}
// If we reach a JSX element, we're in element content, not attributes
if (currentNode.type === 'JSXElement') {
return false;
}
currentNode = currentNode.parent;
}
return false;
}
// Check if the node is a string literal or template literal
function isStringLiteral(expression) {
// For JSXExpressionContainer, check the expression inside
return ((expression.type === 'Literal' &&
typeof expression.value === 'string') ||
(expression.type === 'TemplateLiteral' &&
expression.expressions.length === 0) // No interpolation
);
}
// Check if the node is a comment
function isComment(expression) {
return expression.type === 'Block' || expression.type === 'Line';
}
// Check if empty
function isEmpty(expression) {
return expression.type === 'JSXEmptyExpression';
}
return {
// Track imports from GT-Next modules
ImportDeclaration(node) {
if (node.source?.type === 'Literal' &&
typeof node.source.value === 'string') {
const source = node.source.value;
if (isGTModule(source)) {
for (const specifier of node.specifiers || []) {
if (specifier.type === 'ImportSpecifier') {
const importedName = specifier.imported?.name || '';
const localName = specifier.local?.name || '';
if (isTranslationComponent(importedName)) {
state.imports.translationComponents.add(localName);
}
else if (isVariableComponent(importedName)) {
state.imports.variableComponents.add(localName);
}
}
else if (specifier.type === 'ImportNamespaceSpecifier') {
const localName = specifier.local?.name || '';
state.imports.namespaceImports.add(localName);
}
}
}
}
},
// Track variable assignments from GT components
VariableDeclarator(node) {
if (node.id?.type === 'Identifier' &&
node.init?.type === 'Identifier') {
const varName = node.id.name;
const assignedFrom = node.init.name;
if (state.imports.translationComponents.has(assignedFrom)) {
state.imports.assignedTranslationComponents.add(varName);
}
else if (state.imports.variableComponents.has(assignedFrom)) {
state.imports.assignedVariableComponents.add(varName);
}
}
},
// Detect unwrapped dynamic content
JSXExpressionContainer(node) {
// Skip expressions inside JSX attributes (e.g., <Image width={16} />)
if (isInsideJSXAttribute(node)) {
return;
}
// Skip expressions with just a string literal (e.g., {'Hello'})
// Skip expressions with just a comment
// Skip expressions with just an empty statement
if ((node.expression && isStringLiteral(node.expression)) ||
isComment(node.expression) ||
isEmpty(node.expression)) {
return;
}
// Check if this expression is inside a translation component but not inside a variable component
let inTranslationComponent = false;
let inVariableComponent = false;
// Walk up the AST to find parent JSX elements
let currentNode = node.parent;
while (currentNode) {
if (currentNode.type === 'JSXElement') {
const elementName = getElementName(currentNode);
if (elementName) {
// Check if this is a variable component
if (state.imports.variableComponents.has(elementName) ||
state.imports.assignedVariableComponents.has(elementName) ||
isNamespaceVariableComponent(elementName)) {
inVariableComponent = true;
break; // If we find a variable component, we don't need to check further
}
// Check if this is a translation component
else if (state.imports.translationComponents.has(elementName) ||
state.imports.assignedTranslationComponents.has(elementName) ||
isNamespaceTranslationComponent(elementName)) {
inTranslationComponent = true;
}
}
}
currentNode = currentNode.parent;
}
// Report if we're inside a translation component but not inside a variable component
if (inTranslationComponent && !inVariableComponent) {
context.report({
node,
messageId: 'dynamicJsx',
});
}
},
};
},
};
//# sourceMappingURL=no-dynamic-jsx.js.map