sicua
Version:
A tool for analyzing project structure and dependencies
237 lines (236 loc) • 9.48 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReactI18nextKeyFinder = void 0;
const astUtils_1 = require("./astUtils");
const contextExtractor_1 = require("../extractors/contextExtractor");
/**
* Finds react-i18next translation keys used in source code with enhanced context
*/
class ReactI18nextKeyFinder {
constructor() {
this.translationKeys = [];
this.analyzedFiles = new Set();
this.contextExtractor = new contextExtractor_1.ContextExtractor();
}
/**
* Finds all react-i18next translation keys in source files
* @param sourceFiles Map of source files to analyze
* @returns Promise resolving when analysis is complete
*/
async findAllTranslationKeys(sourceFiles) {
// Analyze each source file
for (const [filePath, sourceFile] of sourceFiles.entries()) {
// Skip files that we've already analyzed
if (this.analyzedFiles.has(filePath)) {
continue;
}
// Skip non-TypeScript/JavaScript files
if (!(0, astUtils_1.isTypeScriptFile)(filePath)) {
continue;
}
const results = this.analyzeSourceFile(sourceFile, filePath);
// Add the keys to our collection
this.translationKeys.push(...results.translationKeys);
// Mark as analyzed
this.analyzedFiles.add(filePath);
}
}
/**
* Gets the collected translation keys
* @returns Array of translation keys
*/
getTranslationKeys() {
return this.translationKeys;
}
/**
* Analyzes a source file for react-i18next hooks and calls
* @param sourceFile Source file to analyze
* @param filePath Path to the source file
* @returns Analysis results with hooks and keys
*/
analyzeSourceFile(sourceFile, filePath) {
// Find all react-i18next hooks in the file (e.g., useTranslation)
const hooks = (0, astUtils_1.findReactI18nextHooksInFile)(sourceFile, filePath);
// Process each hook to find translation keys
const foundKeys = [];
for (const hook of hooks) {
const translationCalls = this.processReactI18nextHook(hook, sourceFile, filePath);
// Add the keys to our result
foundKeys.push(...translationCalls);
}
return {
hooks,
translationKeys: foundKeys,
};
}
/**
* Processes a react-i18next hook to find translation keys
* @param hook React-i18next hook to process
* @param sourceFile Source file
* @param filePath File path
* @returns Array of translation keys
*/
processReactI18nextHook(hook, sourceFile, filePath) {
const { varName, namespace, componentName } = hook;
// Find all translation calls using this hook
const translationCalls = (0, astUtils_1.findReactI18nextCalls)(sourceFile, varName);
// Process each translation call
return translationCalls.map((call) => this.processReactI18nextCall(call, namespace, componentName, sourceFile, filePath));
}
/**
* Processes a react-i18next translation call to create a translation key with usage context
* @param call React-i18next translation call
* @param hookNamespace Optional namespace from hook
* @param componentName Component name
* @param sourceFile Source file
* @param filePath File path
* @returns Translation key with usage context
*/
processReactI18nextCall(call, hookNamespace, componentName, sourceFile, filePath) {
// Create the basic translation key
const basicKey = (0, astUtils_1.processReactI18nextKey)(call, hookNamespace, componentName, sourceFile, filePath);
// Enhance it with additional context
return this.contextExtractor.enhanceKeyWithContext(basicKey, sourceFile);
}
/**
* Groups translation keys by file path
* @returns Map of file paths to arrays of translation keys
*/
groupKeysByFile() {
const keysByFile = new Map();
for (const key of this.translationKeys) {
const existing = keysByFile.get(key.filePath) || [];
existing.push(key);
keysByFile.set(key.filePath, existing);
}
return keysByFile;
}
/**
* Groups translation keys by namespace
* @returns Map of namespaces to arrays of translation keys
*/
groupKeysByNamespace() {
const keysByNamespace = new Map();
for (const key of this.translationKeys) {
const namespace = key.namespace || "translation"; // Default namespace for react-i18next
const existing = keysByNamespace.get(namespace) || [];
existing.push(key);
keysByNamespace.set(namespace, existing);
}
return keysByNamespace;
}
/**
* Finds keys that use namespace prefix pattern (namespace:key)
* @returns Array of keys using namespace prefix
*/
findKeysWithNamespacePrefix() {
return this.translationKeys.filter((key) => {
// Check if the original key contained a colon (indicating namespace:key pattern)
return key.key !== key.fullKey.split(".").pop();
});
}
/**
* Finds keys that use namespace from options object
* @returns Array of keys using options namespace
*/
findKeysWithOptionsNamespace() {
// This would require storing additional metadata about how the namespace was determined
// For now, we'll identify them by checking if they have a namespace but don't use prefix pattern
return this.translationKeys.filter((key) => {
const hasNamespace = key.namespace && key.namespace !== "translation";
const usesPrefix = key.key !== key.fullKey.split(".").pop();
return hasNamespace && !usesPrefix;
});
}
/**
* Finds files with the most translation keys
* @param limit Maximum number of files to return
* @returns Array of files sorted by key count
*/
findFilesWithMostKeys(limit = 10) {
const keysByFile = this.groupKeysByFile();
return Array.from(keysByFile.entries())
.map(([filePath, keys]) => ({
filePath,
keyCount: keys.length,
keys,
}))
.sort((a, b) => b.keyCount - a.keyCount)
.slice(0, limit);
}
/**
* Finds translation keys in JSX context
* @returns Array of keys used in JSX
*/
findKeysInJSX() {
return this.translationKeys.filter((key) => key.usageContext?.isInJSX);
}
/**
* Finds translation keys in conditional rendering
* @returns Array of keys used in conditional rendering
*/
findKeysInConditionals() {
return this.translationKeys.filter((key) => key.usageContext?.isInConditional);
}
/**
* Finds translation keys in event handlers
* @returns Array of keys used in event handlers
*/
findKeysInEventHandlers() {
return this.translationKeys.filter((key) => key.usageContext?.isInEventHandler);
}
/**
* Groups translation keys by component
* @returns Map of component names to arrays of translation keys
*/
groupKeysByComponent() {
const keysByComponent = new Map();
for (const key of this.translationKeys) {
// Use the parent component from context if available
const componentName = key.usageContext?.parentComponent || key.componentName;
const existing = keysByComponent.get(componentName) || [];
existing.push(key);
keysByComponent.set(componentName, existing);
}
return keysByComponent;
}
/**
* Finds components that use multiple namespaces
* @returns Array of components with multiple namespaces
*/
findComponentsWithMultipleNamespaces() {
const componentMap = this.groupKeysByComponent();
const result = [];
for (const [componentName, keys] of componentMap.entries()) {
const namespaces = Array.from(new Set(keys.map((key) => key.namespace || "translation")));
if (namespaces.length > 1) {
result.push({
componentName,
namespaces,
keyCount: keys.length,
});
}
}
return result.sort((a, b) => b.namespaces.length - a.namespaces.length);
}
/**
* Gets statistics about key usage patterns
* @returns Object with usage statistics
*/
getUsageStatistics() {
const uniqueKeys = new Set(this.translationKeys.map((key) => key.fullKey));
const namespaces = new Set(this.translationKeys.map((key) => key.namespace || "translation"));
const components = new Set(this.translationKeys.map((key) => key.componentName));
return {
totalKeys: this.translationKeys.length,
uniqueKeys: uniqueKeys.size,
namespacesUsed: namespaces.size,
filesAnalyzed: this.analyzedFiles.size,
componentsAnalyzed: components.size,
jsxUsage: this.findKeysInJSX().length,
conditionalUsage: this.findKeysInConditionals().length,
eventHandlerUsage: this.findKeysInEventHandlers().length,
};
}
}
exports.ReactI18nextKeyFinder = ReactI18nextKeyFinder;