UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

237 lines (236 loc) 9.48 kB
"use strict"; 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;