sicua
Version:
A tool for analyzing project structure and dependencies
281 lines (280 loc) • 12 kB
JavaScript
"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;