sicua
Version:
A tool for analyzing project structure and dependencies
193 lines (192 loc) • 8.65 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LibraryDetector = exports.TranslationLibrary = void 0;
const typescript_1 = __importDefault(require("typescript"));
/**
* Enum representing supported translation libraries
*/
var TranslationLibrary;
(function (TranslationLibrary) {
TranslationLibrary["NEXT_INTL"] = "next-intl";
TranslationLibrary["REACT_I18NEXT"] = "react-i18next";
TranslationLibrary["UNKNOWN"] = "unknown";
})(TranslationLibrary || (exports.TranslationLibrary = TranslationLibrary = {}));
/**
* Detects which translation library is being used in the project
*/
class LibraryDetector {
/**
* Analyzes source files to determine which translation library is being used
* @param sourceFiles Map of source files to analyze
* @returns Detection result with library type and confidence
*/
detectTranslationLibrary(sourceFiles) {
const nextIntlEvidence = this.scanForNextIntl(sourceFiles);
const reactI18nextEvidence = this.scanForReactI18next(sourceFiles);
// Calculate confidence scores
const nextIntlScore = this.calculateConfidenceScore(nextIntlEvidence);
const reactI18nextScore = this.calculateConfidenceScore(reactI18nextEvidence);
// Determine library based on priority (next-intl > react-i18next)
if (nextIntlScore > 0 && nextIntlScore >= reactI18nextScore) {
return {
library: TranslationLibrary.NEXT_INTL,
confidence: nextIntlScore,
evidence: nextIntlEvidence,
};
}
else if (reactI18nextScore > 0) {
return {
library: TranslationLibrary.REACT_I18NEXT,
confidence: reactI18nextScore,
evidence: reactI18nextEvidence,
};
}
return {
library: TranslationLibrary.UNKNOWN,
confidence: 0,
evidence: {
imports: [],
hooks: [],
patterns: [],
},
};
}
/**
* Scans for next-intl specific patterns
* @param sourceFiles Source files to scan
* @returns Evidence of next-intl usage
*/
scanForNextIntl(sourceFiles) {
const evidence = {
imports: [],
hooks: [],
patterns: [],
};
for (const [filePath, sourceFile] of sourceFiles.entries()) {
this.visitNodes(sourceFile, (node) => {
// Check for next-intl imports
if (typescript_1.default.isImportDeclaration(node) && node.moduleSpecifier) {
if (typescript_1.default.isStringLiteral(node.moduleSpecifier) &&
node.moduleSpecifier.text === "next-intl") {
evidence.imports.push(`${filePath}: ${node.moduleSpecifier.text}`);
}
}
// Check for useTranslations hook calls (plural)
if (typescript_1.default.isCallExpression(node) &&
typescript_1.default.isIdentifier(node.expression) &&
node.expression.text === "useTranslations") {
evidence.hooks.push(`${filePath}: useTranslations()`);
}
// Check for variable declarations with useTranslations
if (typescript_1.default.isVariableDeclaration(node) &&
node.initializer &&
typescript_1.default.isCallExpression(node.initializer) &&
typescript_1.default.isIdentifier(node.initializer.expression) &&
node.initializer.expression.text === "useTranslations") {
evidence.patterns.push(`${filePath}: const t = useTranslations()`);
}
});
}
return evidence;
}
/**
* Scans for react-i18next specific patterns
* @param sourceFiles Source files to scan
* @returns Evidence of react-i18next usage
*/
scanForReactI18next(sourceFiles) {
const evidence = {
imports: [],
hooks: [],
patterns: [],
};
for (const [filePath, sourceFile] of sourceFiles.entries()) {
this.visitNodes(sourceFile, (node) => {
// Check for react-i18next imports
if (typescript_1.default.isImportDeclaration(node) && node.moduleSpecifier) {
if (typescript_1.default.isStringLiteral(node.moduleSpecifier) &&
node.moduleSpecifier.text === "react-i18next") {
evidence.imports.push(`${filePath}: ${node.moduleSpecifier.text}`);
}
}
// Check for useTranslation hook calls (singular)
if (typescript_1.default.isCallExpression(node) &&
typescript_1.default.isIdentifier(node.expression) &&
node.expression.text === "useTranslation") {
evidence.hooks.push(`${filePath}: useTranslation()`);
}
// Check for variable declarations with useTranslation
if (typescript_1.default.isVariableDeclaration(node) &&
node.initializer &&
typescript_1.default.isCallExpression(node.initializer) &&
typescript_1.default.isIdentifier(node.initializer.expression) &&
node.initializer.expression.text === "useTranslation") {
evidence.patterns.push(`${filePath}: const { t } = useTranslation()`);
}
// Check for destructuring patterns common in react-i18next
if (typescript_1.default.isVariableDeclaration(node) &&
typescript_1.default.isObjectBindingPattern(node.name) &&
node.initializer &&
typescript_1.default.isCallExpression(node.initializer) &&
typescript_1.default.isIdentifier(node.initializer.expression) &&
node.initializer.expression.text === "useTranslation") {
// Look for { t, i18n } destructuring pattern
const hasT = node.name.elements.some((element) => typescript_1.default.isBindingElement(element) &&
element.name &&
typescript_1.default.isIdentifier(element.name) &&
element.name.text === "t");
const hasI18n = node.name.elements.some((element) => typescript_1.default.isBindingElement(element) &&
element.name &&
typescript_1.default.isIdentifier(element.name) &&
element.name.text === "i18n");
if (hasT) {
evidence.patterns.push(`${filePath}: destructured 't' from useTranslation`);
}
if (hasI18n) {
evidence.patterns.push(`${filePath}: destructured 'i18n' from useTranslation`);
}
}
});
}
return evidence;
}
/**
* Calculates confidence score based on evidence
* @param evidence Evidence collected for a library
* @returns Confidence score between 0 and 1
*/
calculateConfidenceScore(evidence) {
let score = 0;
// Import evidence is strongest (0.5 points)
if (evidence.imports.length > 0) {
score += 0.5;
}
// Hook usage evidence (0.3 points)
if (evidence.hooks.length > 0) {
score += 0.3;
}
// Pattern evidence (0.2 points)
if (evidence.patterns.length > 0) {
score += 0.2;
}
// Bonus for multiple files using the library
const uniqueFiles = new Set([...evidence.imports, ...evidence.hooks, ...evidence.patterns].map((item) => item.split(":")[0]));
if (uniqueFiles.size > 1) {
score += 0.1 * Math.min(uniqueFiles.size - 1, 3); // Max 0.3 bonus
}
return Math.min(score, 1.0);
}
/**
* Utility method to visit all nodes in a source file
* @param node Starting node
* @param visitor Visitor function
*/
visitNodes(node, visitor) {
visitor(node);
typescript_1.default.forEachChild(node, (child) => this.visitNodes(child, visitor));
}
}
exports.LibraryDetector = LibraryDetector;