UNPKG

@logistically/i18n-cli

Version:

Enterprise-grade CLI tool for extracting and managing translations in Logistically microservices

296 lines 13.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TranslationExtractor = void 0; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const glob_1 = require("glob"); const logger_1 = require("./logger"); const validator_1 = require("./validator"); const performance_1 = require("./performance"); const config_1 = require("./config"); class TranslationExtractor { constructor() { this.logger = logger_1.Logger.getInstance(); this.defaultPatterns = [ { regex: /throw new \w+Exception\(['"`]([^'"`]+)['"`](?:,\s*\w+\.\w+)?\)/g, type: 'EXCEPTION', description: 'Exception messages', priority: 1 }, { regex: /return\s*\{[^}]*message\s*:\s*['"`]([^'"`]+)['"`][^}]*\}/g, type: 'RETURN_MESSAGE', description: 'Return object messages', priority: 2 }, { regex: /errors\.push\(`([^`]+)`\)/g, type: 'TEMPLATE_LITERAL', description: 'Template literals in error arrays', priority: 3 }, { regex: /message\s*:\s*['"`]([^'"`]+)['"`]/g, type: 'MESSAGE_PROPERTY', description: 'Message properties in objects', priority: 4 }, { regex: /['"`]([^'"`]{10,})['"`]\s*\+\s*['"`]([^'"`]+)['"`]/g, type: 'CONCATENATED_STRING', description: 'Concatenated strings', priority: 5 } ]; this.configManager = config_1.ConfigManager.getInstance(); const config = this.configManager.getConfig(); const securityContext = { inputValidation: config.security.validateInputs, outputSanitization: config.security.sanitizeOutputs, sanitizeOutputs: config.security.sanitizeOutputs, fileAccessControl: true, maxFileSize: config.performance.maxFileSize, maxKeyLength: config.security.maxKeyLength, allowedFileTypes: ['.ts', '.js', '.tsx', '.jsx'] }; this.validator = new validator_1.Validator(securityContext); this.performanceMonitor = new performance_1.PerformanceMonitor(); } async extract(options) { const config = this.configManager.getEnvironmentConfig(); const { path: searchPath = '.', ignore = ['**/node_modules/**', '**/dist/**', '**/coverage/**', '**/*.spec.ts', '**/*.test.ts'], patterns = this.defaultPatterns, verbose = false, validate = config.features.enableValidation, maxFileSize = config.performance.maxFileSize, concurrency = config.performance.maxConcurrency, includeComments = false, excludePatterns = [] } = options; // Configure logger this.logger.configure(config.logging); this.logger.setVerbose(verbose); this.logger.header('Translation Extraction'); this.logger.info(`Searching in: ${searchPath}`, 'extraction'); this.logger.debug(`Ignore patterns: ${ignore.join(', ')}`, 'extraction'); this.logger.debug(`Concurrency: ${concurrency}`, 'extraction'); this.logger.debug(`Max file size: ${maxFileSize}MB`, 'extraction'); // Start performance monitoring this.performanceMonitor.startMonitoring(); // Validate search path if (validate) { const pathValidation = this.validator.validatePath(searchPath); if (!pathValidation.isValid) { this.logger.logValidationErrors(pathValidation.errors, 'extraction'); throw new Error('Invalid search path'); } this.logger.logValidationWarnings(pathValidation.warnings, 'extraction'); } // Find TypeScript files const searchPattern = path_1.default.resolve(searchPath, '**/*.ts'); const allIgnorePatterns = [...ignore, ...excludePatterns]; const tsFiles = await (0, glob_1.glob)(searchPattern, { ignore: allIgnorePatterns }); this.logger.info(`Found ${tsFiles.length} TypeScript files`, 'extraction'); // Filter files by size const validFiles = await this.filterFilesBySize(tsFiles, maxFileSize); this.logger.info(`Processing ${validFiles.length} files (${tsFiles.length - validFiles.length} skipped due to size)`, 'extraction'); // Sort patterns by priority const sortedPatterns = [...patterns].sort((a, b) => (a.priority || 0) - (b.priority || 0)); // Extract strings with concurrency control const allStrings = []; const batchSize = concurrency; for (let i = 0; i < validFiles.length; i += batchSize) { const batch = validFiles.slice(i, i + batchSize); const batchPromises = batch.map((file) => this.extractFromFile(file, sortedPatterns, includeComments)); const batchResults = await Promise.allSettled(batchPromises); batchResults.forEach((result, index) => { if (result.status === 'fulfilled') { allStrings.push(...result.value); } else { this.logger.warning(`Error processing ${batch[index]}: ${result.reason}`, 'extraction'); } }); // Update progress this.performanceMonitor.updateProgress(i + batch.length, validFiles.length); } // Remove duplicates const uniqueStrings = this.removeDuplicates(allStrings); // Validate extracted strings if (validate) { const validation = this.validator.validateTranslationKeys(uniqueStrings); this.logger.logValidationErrors(validation.errors, 'extraction'); this.logger.logValidationWarnings(validation.warnings, 'extraction'); if (!validation.isValid) { this.logger.error('Extraction completed with validation errors', 'extraction'); } } // Update final metrics this.performanceMonitor.updateKeysProcessed(uniqueStrings.length); const metrics = this.performanceMonitor.endMonitoring(); this.logger.success(`Extracted ${uniqueStrings.length} unique translation keys`, 'extraction'); this.logger.logPerformanceMetrics(metrics, 'extraction'); // Group by service for summary const serviceSummary = this.groupByService(uniqueStrings); this.logger.section('Extraction Summary'); this.logger.table(serviceSummary); return uniqueStrings; } async extractFromFile(filePath, patterns, includeComments = false) { const content = await fs_extra_1.default.readFile(filePath, 'utf8'); const results = []; // Extract comments if requested let comments = []; if (includeComments) { comments = this.extractComments(content); } patterns.forEach(pattern => { let match; while ((match = pattern.regex.exec(content)) !== null) { const text = match[1] || match[2] || match[0]; // Validate text length if (text.length < 3 || text.length > 500) { continue; // Skip very short or very long strings } const key = this.generateKey(text, pattern.type, filePath); const line = content.substring(0, match.index).split('\n').length; // Find nearby comment for context const context = includeComments ? this.findNearbyComment(comments, line) : undefined; results.push({ key, text: this.validator.sanitizeOutput(text), type: pattern.type, file: filePath, line, context, priority: this.determinePriority(text, pattern.type), category: this.determineCategory(filePath, pattern.type) }); } }); return results; } extractComments(content) { const comments = []; // Extract single-line comments const singleLineComments = content.match(/\/\/.*$/gm) || []; comments.push(...singleLineComments.map(c => c.trim())); // Extract multi-line comments const multiLineComments = content.match(/\/\*[\s\S]*?\*\//gm) || []; comments.push(...multiLineComments.map(c => c.trim())); return comments; } findNearbyComment(comments, line) { // Find comment within 5 lines for (let i = Math.max(0, line - 5); i <= line + 5; i++) { const comment = comments.find(c => c.includes(`line ${i}`)); if (comment) { return comment.replace(/\/\/\s*|\/\*\s*|\s*\*\//g, '').trim(); } } return undefined; } determinePriority(text, type) { // High priority: error messages, exceptions if (type === 'EXCEPTION' || text.toLowerCase().includes('error')) { return 'high'; } // Medium priority: return messages, validation messages if (type === 'RETURN_MESSAGE' || text.toLowerCase().includes('validation')) { return 'medium'; } // Low priority: everything else return 'low'; } determineCategory(filePath, type) { const pathParts = filePath.split(path_1.default.sep); // Extract service name const serviceIndex = pathParts.findIndex(part => ['auth', 'profile', 'notification', 'location', 'bff'].includes(part)); if (serviceIndex !== -1) { return pathParts[serviceIndex]; } // Extract directory structure const srcIndex = pathParts.findIndex(part => part === 'src'); if (srcIndex > 0) { return pathParts[srcIndex - 1]; } return 'unknown'; } async filterFilesBySize(files, maxFileSizeMB) { const validFiles = []; for (const file of files) { try { const stats = await fs_extra_1.default.stat(file); const fileSizeMB = stats.size / (1024 * 1024); if (fileSizeMB <= maxFileSizeMB) { validFiles.push(file); } else { this.logger.debug(`Skipping ${file} (${fileSizeMB.toFixed(2)}MB > ${maxFileSizeMB}MB)`, 'extraction'); } } catch (error) { this.logger.warning(`Cannot access file ${file}: ${error}`, 'extraction'); } } return validFiles; } generateKey(text, type, filePath) { // Clean the text let cleanText = text .replace(/[^\w\s]/g, '') // Remove special chars .replace(/\s+/g, '_') // Replace spaces with underscores .toUpperCase(); // Extract service name from file path const pathParts = filePath.split(path_1.default.sep); // Find the service name by looking for the service directory in the path let serviceName = ''; for (let i = 0; i < pathParts.length; i++) { if (pathParts[i] === 'notification' || pathParts[i] === 'auth' || pathParts[i] === 'profile' || pathParts[i] === 'location') { serviceName = pathParts[i]; break; } } // If no service name found, use the directory before 'src' if (!serviceName) { const srcIndex = pathParts.findIndex(part => part === 'src'); if (srcIndex > 0) { serviceName = pathParts[srcIndex - 1]; } else { serviceName = 'UNKNOWN'; } } const fileName = path_1.default.basename(filePath, '.ts').replace('.', '_'); return `${serviceName.toUpperCase()}.${fileName.toUpperCase()}.${type}.${cleanText}`; } removeDuplicates(strings) { const uniqueStrings = []; const seen = new Set(); strings.forEach(item => { const key = `${item.key}:${item.text}`; if (!seen.has(key)) { seen.add(key); uniqueStrings.push(item); } }); return uniqueStrings; } groupByService(strings) { const serviceMap = new Map(); strings.forEach(item => { const serviceName = item.file.split(path_1.default.sep)[0]; if (!serviceMap.has(serviceName)) { serviceMap.set(serviceName, { total: 0, types: {} }); } const service = serviceMap.get(serviceName); service.total++; service.types[item.type] = (service.types[item.type] || 0) + 1; }); return Array.from(serviceMap.entries()).map(([service, data]) => ({ Service: service, 'Total Keys': data.total, 'Exception Keys': data.types.EXCEPTION || 0, 'Template Keys': data.types.TEMPLATE_LITERAL || 0, 'Other Keys': data.total - (data.types.EXCEPTION || 0) - (data.types.TEMPLATE_LITERAL || 0) })); } } exports.TranslationExtractor = TranslationExtractor; //# sourceMappingURL=extractor.js.map