UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

281 lines (280 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReactI18nextAnalyzer = void 0; const fileFinder_1 = require("./fileFinder"); const keyFinder_1 = require("./keyFinder"); const missingTranslationAnalyzer_1 = require("../analyzers/missingTranslationAnalyzer"); const duplicateTranslationAnalyzer_1 = require("../analyzers/duplicateTranslationAnalyzer"); const coverageAnalyzer_1 = require("../analyzers/coverageAnalyzer"); /** * Analyzer for detecting react-i18next translation issues in a React project */ class ReactI18nextAnalyzer { /** * Constructor for the react-i18next analyzer * @param projectPath Path to the project root * @param sourceFiles Map of source files to analyze */ constructor(projectPath, sourceFiles) { this.sourceFiles = sourceFiles; // Create context object this.context = { projectPath, debugMode: false, log: () => { }, // No-op logger }; // Initialize analyzers this.fileFinder = new fileFinder_1.ReactI18nextFileFinder(this.context); this.keyFinder = new keyFinder_1.ReactI18nextKeyFinder(); this.missingTranslationAnalyzer = new missingTranslationAnalyzer_1.MissingTranslationAnalyzer(); this.duplicateTranslationAnalyzer = new duplicateTranslationAnalyzer_1.DuplicateTranslationAnalyzer(); this.coverageAnalyzer = new coverageAnalyzer_1.CoverageAnalyzer(); } /** * Main analysis method for react-i18next * @returns Promise that resolves to analysis results */ async analyze() { // Step 1: Find react-i18next translation files await this.fileFinder.findAllTranslationFiles(); const translationFiles = this.fileFinder.getTranslationFiles(); const mainTranslationFile = this.fileFinder.getMainTranslationFile(); // Step 2: Find react-i18next translation keys in code await this.keyFinder.findAllTranslationKeys(this.sourceFiles); const translationKeys = this.keyFinder.getTranslationKeys(); // Step 3: Analyze missing translations const missingTranslations = this.missingTranslationAnalyzer.detectMissingTranslations(translationKeys, translationFiles); // Step 4: Analyze duplicate translations (per namespace) const duplicateTranslations = await this.analyzeDuplicateTranslations(translationKeys, translationFiles); // Step 5: Analyze translation file coverage const translationFilesCoverage = this.coverageAnalyzer.analyzeTranslationFilesCoverage(translationKeys, translationFiles); // Step 6: Generate react-i18next specific statistics const statistics = this.generateReactI18nextStatistics(translationKeys, missingTranslations, duplicateTranslations); return { missingTranslations, duplicateTranslations, translationFilesCoverage, statistics, }; } /** * Analyzes duplicate translations across react-i18next namespaces * @param translationKeys Translation keys found in code * @param translationFiles Translation files to analyze * @returns Array of duplicate translations */ async analyzeDuplicateTranslations(translationKeys, translationFiles) { const allDuplicates = []; // Analyze duplicates per namespace/file for (const file of translationFiles) { const duplicatesInFile = this.duplicateTranslationAnalyzer.detectDuplicateTranslations(translationKeys, file); allDuplicates.push(...duplicatesInFile); } // Additional react-i18next specific duplicate analysis const crossNamespaceDuplicates = this.findCrossNamespaceDuplicates(translationFiles); allDuplicates.push(...crossNamespaceDuplicates); return allDuplicates; } /** * Finds duplicate translations across different namespaces * @param translationFiles Translation files to analyze * @returns Array of cross-namespace duplicates */ findCrossNamespaceDuplicates(translationFiles) { const crossNamespaceDuplicates = []; const namespaceFiles = this.fileFinder.getNamespaceFiles(); // Group all translation values across namespaces const valueToNamespaceMap = new Map(); for (const [namespace, files] of namespaceFiles.entries()) { for (const file of files) { this.flattenTranslationsWithNamespace(file.content, "", namespace, file.path, valueToNamespaceMap); } } // Find values that appear in multiple namespaces for (const [value, occurrences] of valueToNamespaceMap.entries()) { if (occurrences.length > 1 && this.isSignificantTextForDuplication(value)) { const uniqueNamespaces = new Set(occurrences.map((occ) => occ.namespace)); if (uniqueNamespaces.size > 1) { crossNamespaceDuplicates.push({ value, keys: occurrences.map((occ) => ({ fullKey: `${occ.namespace}.${occ.key}`, filePath: occ.filePath, })), usages: [], // Would need to cross-reference with translation keys type: "cross-namespace", }); } } } return crossNamespaceDuplicates; } /** * Flattens translations with namespace information * @param obj Translation object * @param prefix Current key prefix * @param namespace Namespace name * @param filePath File path * @param valueMap Map to populate with values and their locations */ flattenTranslationsWithNamespace(obj, prefix, namespace, filePath, valueMap) { for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; if (typeof value === "object" && value !== null) { this.flattenTranslationsWithNamespace(value, fullKey, namespace, filePath, valueMap); } else if (typeof value === "string") { const existing = valueMap.get(value) || []; existing.push({ namespace, key: fullKey, filePath }); valueMap.set(value, existing); } } } /** * Checks if text is significant enough to warn about duplication * @param value The text value to check * @returns Boolean indicating if the text is significant */ isSignificantTextForDuplication(value) { // Ignore very short strings if (value.length < 5) return false; // Ignore common short words/values const lowered = value.toLowerCase(); if ([ "yes", "no", "true", "false", "ok", "cancel", "save", "edit", "delete", ].includes(lowered)) return false; // Ignore strings that are just numbers if (/^\d+$/.test(value)) return false; // Ignore react-i18next interpolation-only strings if (/^\{\{[^}]+\}\}$/.test(value)) return false; return true; } /** * Generate react-i18next specific statistics * @param translationKeys Translation keys found * @param missingTranslations Missing translations * @param duplicateTranslations Duplicate translations * @returns Statistics object */ generateReactI18nextStatistics(translationKeys, missingTranslations, duplicateTranslations) { // Get basic usage statistics const usageStats = this.keyFinder.getUsageStatistics(); // Calculate namespace-specific statistics const keysByNamespace = this.keyFinder.groupKeysByNamespace(); const keysPerNamespace = {}; for (const [namespace, keys] of keysByNamespace.entries()) { keysPerNamespace[namespace] = keys.length; } // Count cross-namespace duplicates const crossNamespaceDuplicates = duplicateTranslations.filter((dup) => dup.type === "cross-namespace").length; // Analyze react-i18next specific features const interpolationUsage = this.countInterpolationUsage(translationKeys); const pluralizationUsage = this.countPluralizationUsage(); // Group missing translations by file (from base statistics) const filesMissingCount = {}; for (const translation of missingTranslations) { const filePath = translation.key.filePath; if (!filesMissingCount[filePath]) { filesMissingCount[filePath] = []; } filesMissingCount[filePath].push(translation); } const filesWithMostMissingTranslations = Object.entries(filesMissingCount) .map(([filePath, translations]) => ({ filePath, count: translations.length, })) .sort((a, b) => b.count - a.count) .slice(0, 5); return { totalTranslationKeysUsed: usageStats.totalKeys, totalMissingTranslations: missingTranslations.length, totalDuplicateValues: duplicateTranslations.length, filesWithMostMissingTranslations, namespacesUsed: usageStats.namespacesUsed, keysPerNamespace, crossNamespaceDuplicates, interpolationUsage, pluralizationUsage, }; } /** * Counts usage of interpolation in translation keys * @param translationKeys Translation keys to analyze * @returns Number of keys using interpolation */ countInterpolationUsage(translationKeys) { // This would require analyzing the actual usage context or translation values // For now, return 0 as placeholder - could be enhanced to detect interpolation patterns return 0; } /** * Counts usage of pluralization in translation files * @returns Number of plural keys found */ countPluralizationUsage() { const translationFiles = this.fileFinder.getTranslationFiles(); let pluralCount = 0; for (const file of translationFiles) { const analysis = this.fileFinder.analyzeReactI18nextFile(file); if (analysis.hasPlurals) { pluralCount += this.countPluralKeys(file.content); } } return pluralCount; } /** * Counts plural keys in a translation object * @param obj Translation object * @returns Number of plural keys */ countPluralKeys(obj) { let count = 0; const countInObject = (object) => { for (const [key, value] of Object.entries(object)) { // Check for react-i18next plural suffixes if (key.endsWith("_zero") || key.endsWith("_one") || key.endsWith("_two") || key.endsWith("_few") || key.endsWith("_many") || key.endsWith("_other")) { count++; } if (typeof value === "object" && value !== null) { countInObject(value); } } }; countInObject(obj); return count; } /** * Gets react-i18next specific analysis information * @returns React-i18next setup information */ getReactI18nextInfo() { const setup = this.fileFinder.detectReactI18nextSetup(); const keyStatistics = this.keyFinder.getUsageStatistics(); const namespaceAnalysis = this.keyFinder.findComponentsWithMultipleNamespaces(); return { setup, keyStatistics, namespaceAnalysis, }; } } exports.ReactI18nextAnalyzer = ReactI18nextAnalyzer;