@generaltranslation/gt-next-lint
Version:
ESLint plugin for General Translation Next.js integration
186 lines • 8.74 kB
JavaScript
;
/**
* ESLint rule: no-dynamic-string
*
* Ensures translation functions (t, useGT, getGT) only accept string literals
* as their first argument. Dynamic content like template literals or string
* concatenation is prohibited to ensure consistent translation keys.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.noDynamicString = void 0;
const GT_MODULES = ['gt-next', 'gt-next/client', 'gt-next/server'];
const TRANSLATION_FUNCTIONS = ['useGT', 'getGT'];
function isGTModule(source) {
return GT_MODULES.includes(source);
}
function isTranslationFunction(name) {
return TRANSLATION_FUNCTIONS.includes(name);
}
exports.noDynamicString = {
meta: {
type: 'problem',
docs: {
description: 'Translation functions must use string literals as the first argument',
category: 'Best Practices',
recommended: true,
url: 'https://github.com/generaltranslation/gt/tree/main/packages/next-lint#no-dynamic-string',
},
fixable: undefined,
schema: [],
messages: {
dynamicString: "Translation function must use a constant string literal as the first argument. Use t('Hello, {name}!', { name: value }) instead of template literals or string concatenation.",
},
},
create(context) {
// Track imported GT functions and their aliases
const trackedFunctions = new Map();
// Track variables assigned from translation function calls
const translationVariables = new Set();
function trackImport(localName, importedName, source) {
if (isGTModule(source) && isTranslationFunction(importedName)) {
trackedFunctions.set(localName, {
name: importedName,
isTranslationFunction: true,
});
}
}
function trackNamespaceImport(localName, source) {
if (isGTModule(source)) {
trackedFunctions.set(localName, {
name: localName,
isTranslationFunction: false, // It's a namespace, not directly callable
});
}
}
function isStringLiteral(node) {
return ((node.type === 'Literal' && typeof node.value === 'string') ||
(node.type === 'TemplateLiteral' && node.expressions.length === 0) // Template literal with no interpolation
);
}
function validateTranslationCall(node) {
const firstArg = node.arguments[0];
if (!firstArg)
return; // No arguments
if (!isStringLiteral(firstArg)) {
context.report({
node: firstArg,
messageId: 'dynamicString',
});
}
}
function handleCallExpression(node) {
if (node.callee.type === 'Identifier') {
// Direct function calls: t(), useGT(), etc.
const functionName = node.callee.name;
if (trackedFunctions.has(functionName)) {
const tracked = trackedFunctions.get(functionName);
if (tracked.isTranslationFunction) {
validateTranslationCall(node);
}
}
else if (translationVariables.has(functionName)) {
// Variable assigned from translation function
validateTranslationCall(node);
}
}
else if (node.callee.type === 'MemberExpression') {
// Member expressions: GT.tx(), namespace.function()
const objectName = node.callee.object.name;
const propertyName = node.callee.property.name;
if (trackedFunctions.has(objectName) &&
isTranslationFunction(propertyName)) {
validateTranslationCall(node);
}
}
}
function handleAssignment(node) {
// Track variables assigned from translation function calls
if (node.type === 'VariableDeclarator' &&
node.id.type === 'Identifier' &&
node.init) {
if (node.init.type === 'CallExpression') {
const callExpression = node.init;
if (callExpression.callee.type === 'Identifier') {
const functionName = callExpression.callee.name;
if (trackedFunctions.has(functionName)) {
const tracked = trackedFunctions.get(functionName);
if (tracked.isTranslationFunction) {
// This variable now holds a translation function
translationVariables.add(node.id.name);
}
}
}
else if (callExpression.callee.type === 'MemberExpression') {
// Handle namespace calls like: const t = GT.useGT();
const objectName = callExpression.callee.object.name;
const propertyName = callExpression.callee.property.name;
if (trackedFunctions.has(objectName) &&
isTranslationFunction(propertyName)) {
// This variable now holds a translation function
translationVariables.add(node.id.name);
}
}
}
else if (node.init.type === 'AwaitExpression' &&
node.init.argument.type === 'CallExpression') {
// Handle await getGT() case
const callExpression = node.init.argument;
if (callExpression.callee.type === 'Identifier') {
const functionName = callExpression.callee.name;
if (trackedFunctions.has(functionName)) {
const tracked = trackedFunctions.get(functionName);
if (tracked.isTranslationFunction) {
// This variable now holds a translation function (from awaited promise)
translationVariables.add(node.id.name);
}
}
}
else if (callExpression.callee.type === 'MemberExpression') {
// Handle namespace calls like: const t = await GT.getGT();
const objectName = callExpression.callee.object.name;
const propertyName = callExpression.callee.property.name;
if (trackedFunctions.has(objectName) &&
isTranslationFunction(propertyName)) {
// This variable now holds a translation function (from awaited promise)
translationVariables.add(node.id.name);
}
}
}
else if (node.init.type === 'Identifier') {
// Handle reassignment: const t = getTranslation;
const assignedFrom = node.init.name;
if (translationVariables.has(assignedFrom)) {
translationVariables.add(node.id.name);
}
}
}
}
return {
ImportDeclaration(node) {
const source = node.source.value;
if (!isGTModule(source))
return;
for (const specifier of node.specifiers) {
if (specifier.type === 'ImportSpecifier') {
// import { useGT, tx as serverTx } from 'gt-next'
const importedName = specifier.imported.name;
const localName = specifier.local.name;
trackImport(localName, importedName, source);
}
else if (specifier.type === 'ImportNamespaceSpecifier') {
// import * as GT from 'gt-next'
const localName = specifier.local.name;
trackNamespaceImport(localName, source);
}
}
},
VariableDeclarator(node) {
handleAssignment(node);
},
CallExpression(node) {
handleCallExpression(node);
},
};
},
};
//# sourceMappingURL=no-dynamic-string.js.map