UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

193 lines (192 loc) 8.65 kB
"use strict"; 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;