config = array_merge($this->getDefaultConfig(), $config); $this->phpParser = new PhpParser($this->config); $this->jsParser = new JsParser($this->config); $this->htmlParser = new HtmlParser($this->config); $this->twigParser = new TwigParser($this->config); $this->vueParser = new VueParser($this->config); } /** * Extract translation keys from directory. */ public function extractFromDirectory(string $directory, array $options = []): array { $this->extractedKeys = []; $this->fileScanned = []; $recursive = $options['recursive'] ?? true; $patterns = $options['patterns'] ?? $this->config['file_patterns']; $excludePatterns = $options['exclude_patterns'] ?? $this->config['exclude_patterns']; $this->scanDirectory($directory, $recursive, $patterns, $excludePatterns); return $this->getExtractedKeys(); } /** * Extract translation keys from file. */ public function extractFromFile(string $filepath): array { $this->extractedKeys = []; $this->fileScanned = []; if (!file_exists($filepath)) { throw new \InvalidArgumentException("File not found: {$filepath}"); } $this->scanFile($filepath); return $this->getExtractedKeys(); } /** * Extract translation keys from string content. */ public function extractFromString(string $content, string $filename = 'unknown'): array { $this->extractedKeys = []; $this->fileScanned = []; $this->parseContent($content, $filename); return $this->getExtractedKeys(); } /** * Scan directory for files. */ protected function scanDirectory(string $directory, bool $recursive, array $patterns, array $excludePatterns): void { $iterator = new \RecursiveIteratorIterator( $recursive ? new \RecursiveDirectoryIterator($directory) : new \DirectoryIterator($directory), \RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isDir()) { continue; } $filepath = $file->getPathname(); // Check exclude patterns if ($this->matchesExcludePatterns($filepath, $excludePatterns)) { continue; } // Check include patterns if ($this->matchesPatterns($filepath, $patterns)) { $this->scanFile($filepath); } } } /** * Scan single file. */ protected function scanFile(string $filepath): void { if (in_array($filepath, $this->fileScanned)) { return; } $content = file_get_contents($filepath); if ($content === false) { return; } $this->fileScanned[] = $filepath; $this->parseContent($content, $filepath); } /** * Parse content based on file type. */ protected function parseContent(string $content, string $filename): void { $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); switch ($extension) { case 'php': $this->extractFromPhp($content, $filename); break; case 'js': case 'jsx': case 'ts': case 'tsx': $this->extractFromJs($content, $filename); break; case 'html': case 'htm': $this->extractFromHtml($content, $filename); break; case 'twig': $this->extractFromTwig($content, $filename); break; case 'vue': $this->extractFromVue($content, $filename); break; default: // Try to detect content type if ($this->isPhpContent($content)) { $this->extractFromPhp($content, $filename); } elseif ($this->isJsContent($content)) { $this->extractFromJs($content, $filename); } elseif ($this->isHtmlContent($content)) { $this->extractFromHtml($content, $filename); } elseif ($this->isTwigContent($content)) { $this->extractFromTwig($content, $filename); } break; } } /** * Extract from PHP content. */ protected function extractFromPhp(string $content, string $filename): void { $keys = $this->phpParser->extract($content); $this->addExtractedKeys($keys, $filename, 'php'); } /** * Extract from JavaScript content. */ protected function extractFromJs(string $content, string $filename): void { $keys = $this->jsParser->extract($content); $this->addExtractedKeys($keys, $filename, 'js'); } /** * Extract from HTML content. */ protected function extractFromHtml(string $content, string $filename): void { $keys = $this->htmlParser->extract($content); $this->addExtractedKeys($keys, $filename, 'html'); } /** * Extract from Twig content. */ protected function extractFromTwig(string $content, string $filename): void { $keys = $this->twigParser->extract($content); $this->addExtractedKeys($keys, $filename, 'twig'); } /** * Extract from Vue content. */ protected function extractFromVue(string $content, string $filename): void { $keys = $this->vueParser->extract($content); $this->addExtractedKeys($keys, $filename, 'vue'); } /** * Add extracted keys. */ protected function addExtractedKeys(array $keys, string $filename, string $type): void { foreach ($keys as $key => $info) { if (!isset($this->extractedKeys[$key])) { $this->extractedKeys[$key] = [ 'key' => $key, 'files' => [], 'contexts' => [], 'parameters' => [], 'types' => [], 'line_numbers' => [] ]; } $this->extractedKeys[$key]['files'][] = $filename; $this->extractedKeys[$key]['types'][] = $type; if (isset($info['context'])) { $this->extractedKeys[$key]['contexts'][] = $info['context']; } if (isset($info['parameters'])) { $this->extractedKeys[$key]['parameters'] = array_merge( $this->extractedKeys[$key]['parameters'], $info['parameters'] ); } if (isset($info['line'])) { $this->extractedKeys[$key]['line_numbers'][] = $filename . ':' . $info['line']; } } } /** * Get extracted keys. */ public function getExtractedKeys(): array { // Remove duplicates and sort $result = []; foreach ($this->extractedKeys as $key => $info) { $result[$key] = [ 'key' => $key, 'files' => array_unique($info['files']), 'contexts' => array_unique($info['contexts']), 'parameters' => array_unique($info['parameters']), 'types' => array_unique($info['types']), 'line_numbers' => array_unique($info['line_numbers']), 'usage_count' => count($info['files']) ]; } ksort($result); return $result; } /** * Get keys by file. */ public function getKeysByFile(): array { $keysByFile = []; foreach ($this->extractedKeys as $key => $info) { foreach ($info['files'] as $file) { if (!isset($keysByFile[$file])) { $keysByFile[$file] = []; } $keysByFile[$file][] = $key; } } return $keysByFile; } /** * Get keys by type. */ public function getKeysByType(): array { $keysByType = []; foreach ($this->extractedKeys as $key => $info) { foreach ($info['types'] as $type) { if (!isset($keysByType[$type])) { $keysByType[$type] = []; } $keysByType[$type][] = $key; } } return $keysByType; } /** * Get unused keys (keys not found in any file). */ public function getUnusedKeys(array $existingKeys): array { $extractedKeySet = array_keys($this->extractedKeys); $unusedKeys = array_diff($existingKeys, $extractedKeySet); return array_values($unusedKeys); } /** * Get missing keys (keys found but not in existing translations). */ public function getMissingKeys(array $existingKeys): array { $extractedKeySet = array_keys($this->extractedKeys); $missingKeys = array_diff($extractedKeySet, $existingKeys); return array_values($missingKeys); } /** * Get keys with parameters. */ public function getKeysWithParameters(): array { $keysWithParams = []; foreach ($this->extractedKeys as $key => $info) { if (!empty($info['parameters'])) { $keysWithParams[$key] = array_unique($info['parameters']); } } return $keysWithParams; } /** * Get duplicate keys (keys found in multiple contexts). */ public function getDuplicateKeys(): array { $duplicates = []; foreach ($this->extractedKeys as $key => $info) { if (count($info['contexts']) > 1) { $duplicates[$key] = [ 'contexts' => array_unique($info['contexts']), 'files' => array_unique($info['files']) ]; } } return $duplicates; } /** * Generate translation template. */ public function generateTemplate(array $existingKeys = null, string $language = 'en'): array { $template = []; $extractedKeys = array_keys($this->extractedKeys); if ($existingKeys) { // Merge with existing keys $allKeys = array_unique(array_merge($extractedKeys, $existingKeys)); } else { $allKeys = $extractedKeys; } foreach ($allKeys as $key) { $template[$key] = $this->generatePlaceholder($key, $language); } return $template; } /** * Generate placeholder for translation key. */ protected function generatePlaceholder(string $key, string $language): string { $parts = explode('.', $key); $lastPart = end($parts); // Convert to human readable format $placeholder = str_replace(['_', '-'], ' ', $lastPart); $placeholder = ucwords($placeholder); // Add language-specific prefix/suffix if needed switch ($language) { case 'zh-CN': case 'zh-TW': return $placeholder; // Chinese doesn't need articles case 'ja': return $placeholder; // Japanese doesn't need articles case 'ko': return $placeholder; // Korean doesn't need articles case 'fr': return "La {$placeholder}"; // French article case 'de': return "Der {$placeholder}"; // German article case 'es': return "El {$placeholder}"; // Spanish article case 'it': return "Il {$placeholder}"; // Italian article case 'ru': return "{$placeholder} (русский)"; // Russian suffix case 'ar': return "{$placeholder} (عربي)"; // Arabic suffix default: return "The {$placeholder}"; // English article } } /** * Export extracted keys. */ public function export(string $format = 'json'): string { $data = [ 'extracted_keys' => $this->getExtractedKeys(), 'statistics' => $this->getStatistics(), 'generated_at' => date('Y-m-d H:i:s'), 'config' => $this->config ]; return match ($format) { 'json' => json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), 'csv' => $this->exportToCsv(), 'xlsx' => $this->exportToXlsx(), 'php' => 'getExtractedKeys(), true) . ';', default => throw new \InvalidArgumentException("Unsupported export format: {$format}") }; } /** * Export to CSV format. */ protected function exportToCsv(): string { $csv = "Key,Files,Types,Contexts,Parameters,Usage Count\n"; foreach ($this->getExtractedKeys() as $key => $info) { $csv .= '"' . $key . '",'; $csv .= '"' . implode('; ', $info['files']) . '",'; $csv .= '"' . implode('; ', $info['types']) . '",'; $csv .= '"' . implode('; ', $info['contexts']) . '",'; $csv .= '"' . implode('; ', $info['parameters']) . '",'; $csv .= $info['usage_count'] . "\n"; } return $csv; } /** * Export to XLSX format (basic implementation). */ protected function exportToXlsx(): string { // This would require a proper XLSX library // For now, return CSV as placeholder return $this->exportToCsv(); } /** * Get extraction statistics. */ public function getStatistics(): array { $keysByType = $this->getKeysByType(); $keysByFile = $this->getKeysByFile(); return [ 'total_keys' => count($this->extractedKeys), 'total_files_scanned' => count($this->fileScanned), 'keys_by_type' => array_map('count', $keysByType), 'keys_by_file' => array_map('count', $keysByFile), 'keys_with_parameters' => count($this->getKeysWithParameters()), 'duplicate_keys' => count($this->getDuplicateKeys()), 'most_used_keys' => $this->getMostUsedKeys(10), 'file_types' => array_unique(array_merge(...array_values($keysByType))) ]; } /** * Get most used keys. */ protected function getMostUsedKeys(int $limit = 10): array { $keys = $this->getExtractedKeys(); uasort($keys, fn($a, $b) => $b['usage_count'] - $a['usage_count']); return array_slice($keys, 0, $limit, true); } /** * Check if file matches patterns. */ protected function matchesPatterns(string $filepath, array $patterns): bool { foreach ($patterns as $pattern) { if (fnmatch($pattern, basename($filepath)) || fnmatch($pattern, $filepath)) { return true; } } return false; } /** * Check if file matches exclude patterns. */ protected function matchesExcludePatterns(string $filepath, array $patterns): bool { foreach ($patterns as $pattern) { if (fnmatch($pattern, basename($filepath)) || fnmatch($pattern, $filepath)) { return true; } } return false; } /** * Check if content is PHP. */ protected function isPhpContent(string $content): bool { return str_contains($content, '') || str_contains($content, 'React'); } /** * Check if content is HTML. */ protected function isHtmlContent(string $content): bool { return preg_match('/<[^>]+>/', $content) && (str_contains($content, 'customParsers[$extension] = $parser; } /** * Set custom patterns. */ public function setPatterns(array $patterns): void { $this->config['file_patterns'] = $patterns; } /** * Set exclude patterns. */ public function setExcludePatterns(array $patterns): void { $this->config['exclude_patterns'] = $patterns; } /** * Reset extractor state. */ public function reset(): void { $this->extractedKeys = []; $this->fileScanned = []; } /** * Get default configuration. */ protected function getDefaultConfig(): array { return [ 'file_patterns' => [ '*.php', '*.js', '*.jsx', '*.ts', '*.tsx', '*.html', '*.htm', '*.twig', '*.vue' ], 'exclude_patterns' => [ 'vendor/*', 'node_modules/*', '.git/*', 'storage/*', 'bootstrap/*', 'config/*', 'routes/*', 'tests/*', '*.min.js', '*.min.css', '*.cache' ], 'php_patterns' => [ '/trans\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/__\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/t\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/translate\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/Lang::get\s*\(\s*[\'"]([^\'"]+)[\'"]/' ], 'js_patterns' => [ '/trans\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/t\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/\$t\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/translate\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/i18n\.t\s*\(\s*[\'"]([^\'"]+)[\'"]/' ], 'html_patterns' => [ '/data-i18n\s*=\s*[\'"]([^\'"]+)[\'"]/', '/data-translate\s*=\s*[\'"]([^\'"]+)[\'"]/', '/data-trans\s*=\s*[\'"]([^\'"]+)[\'"]/' ], 'twig_patterns' => [ '/\{\{\s*trans\s*\([\'"]([^\'"]+)[\'"]/', '/\{\{\s*t\s*\([\'"]([^\'"]+)[\'"]/', '/\{\{\s*__\s*\([\'"]([^\'"]+)[\'"]/' ], 'vue_patterns' => [ '/\$t\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/t\s*\(\s*[\'"]([^\'"]+)[\'"]/', '/v-t\s*=\s*[\'"]([^\'"]+)[\'"]/' ] ]; } /** * Get configuration. */ public function getConfig(): array { return $this->config; } /** * Set configuration. */ public function setConfig(array $config): void { $this->config = array_merge($this->config, $config); } /** * Create extractor instance. */ public static function create(array $config = []): self { return new self($config); } /** * Create for PHP project. */ public static function forPhp(): self { return new self([ 'file_patterns' => ['*.php', '*.twig'], 'exclude_patterns' => ['vendor/*', 'node_modules/*', 'storage/*'] ]); } /** * Create for JavaScript project. */ public static function forJs(): self { return new self([ 'file_patterns' => ['*.js', '*.jsx', '*.ts', '*.tsx', '*.vue', '*.html'], 'exclude_patterns' => ['node_modules/*', 'dist/*', 'build/*'] ]); } /** * Create for full-stack project. */ public static function forFullStack(): self { return new self([ 'file_patterns' => ['*.php', '*.js', '*.jsx', '*.ts', '*.tsx', '*.vue', '*.html', '*.twig'], 'exclude_patterns' => ['vendor/*', 'node_modules/*', 'storage/*', 'dist/*', 'build/*'] ]); } }