config = array_merge($this->getDefaultConfig(), $config); $this->analyzer = new ProgressAnalyzer($this->config); $this->reporter = new ProgressReporter($this->config); $this->calculator = new CompletionCalculator($this->config); } /** * Track translation progress for project. */ public function trackProject(string $projectPath, array $options = []): array { $this->reset(); $translationPaths = $options['translation_paths'] ?? $this->config['translation_paths']; $languages = $options['languages'] ?? $this->config['languages']; $referenceLanguage = $options['reference_language'] ?? $this->config['reference_language']; $groups = $options['groups'] ?? $this->config['groups']; // Load translation data $this->loadTranslationData($projectPath, $translationPaths, $languages, $groups); // Analyze progress $this->analyzeProgress($referenceLanguage); // Calculate completion rates $this->calculateCompletion(); // Generate statistics $this->generateStatistics(); return $this->getProgressResults(); } /** * Track translation progress from data. */ public function trackData(array $translationData, array $options = []): array { $this->reset(); $referenceLanguage = $options['reference_language'] ?? $this->config['reference_language']; $this->translationData = $translationData; // Analyze progress $this->analyzeProgress($referenceLanguage); // Calculate completion rates $this->calculateCompletion(); // Generate statistics $this->generateStatistics(); return $this->getProgressResults(); } /** * Track progress for specific language. */ public function trackLanguage(string $language, string $referenceLanguage = null): array { $referenceLanguage = $referenceLanguage ?? $this->config['reference_language']; if (!isset($this->translationData[$referenceLanguage])) { throw new \InvalidArgumentException("Reference language '{$referenceLanguage}' not found"); } if (!isset($this->translationData[$language])) { throw new \InvalidArgumentException("Language '{$language}' not found"); } $referenceData = $this->translationData[$referenceLanguage]; $languageData = $this->translationData[$language]; $progress = $this->analyzer->analyzeLanguage($languageData, $referenceData); $completion = $this->calculator->calculateLanguage($progress); return [ 'language' => $language, 'reference_language' => $referenceLanguage, 'progress' => $progress, 'completion' => $completion, 'statistics' => $this->generateLanguageStatistics($progress, $completion) ]; } /** * Track progress for specific group. */ public function trackGroup(string $group, array $options = []): array { $referenceLanguage = $options['reference_language'] ?? $this->config['reference_language']; $languages = $options['languages'] ?? array_keys($this->translationData); $groupProgress = []; foreach ($languages as $language) { if ($language === $referenceLanguage) { continue; } if (isset($this->translationData[$language][$group])) { $groupData = $this->translationData[$language][$group]; $referenceData = $this->translationData[$referenceLanguage][$group] ?? []; $progress = $this->analyzer->analyzeGroup($groupData, $referenceData); $completion = $this->calculator->calculateGroup($progress); $groupProgress[$language] = [ 'progress' => $progress, 'completion' => $completion ]; } } return [ 'group' => $group, 'reference_language' => $referenceLanguage, 'languages' => $groupProgress, 'overall_completion' => $this->calculateGroupOverallCompletion($groupProgress) ]; } /** * Load translation data. */ protected function loadTranslationData(string $projectPath, array $translationPaths, array $languages, array $groups): void { foreach ($languages as $language) { $this->translationData[$language] = []; foreach ($translationPaths as $path) { $languagePath = $projectPath . '/' . $path . '/' . $language; if (is_dir($languagePath)) { $this->loadLanguageDirectory($languagePath, $language, $groups); } } } } /** * Load language directory. */ protected function loadLanguageDirectory(string $directory, string $language, array $groups): void { $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory) ); foreach ($iterator as $file) { if ($file->isFile() && $this->isTranslationFile($file->getPathname())) { $group = $this->getGroupFromPath($file->getPathname(), $directory); if (empty($groups) || in_array($group, $groups)) { $content = $this->loadTranslationFile($file->getPathname()); if ($content) { $this->translationData[$language][$group] = array_merge( $this->translationData[$language][$group] ?? [], $content ); } } } } } /** * Load translation file. */ protected function loadTranslationFile(string $filepath): ?array { $extension = strtolower(pathinfo($filepath, PATHINFO_EXTENSION)); switch ($extension) { case 'php': return include $filepath; case 'json': $content = file_get_contents($filepath); return json_decode($content, true) ?: []; case 'yaml': case 'yml': return $this->loadYamlFile($filepath); case 'po': return $this->loadPoFile($filepath); default: return null; } } /** * Load YAML file. */ protected function loadYamlFile(string $filepath): array { $content = file_get_contents($filepath); $lines = explode("\n", $content); $data = []; $currentPath = []; foreach ($lines as $line) { $line = trim($line); if (empty($line) || str_starts_with($line, '#')) { continue; } if (preg_match('/^(\w+):\s*(.*)$/', $line, $matches)) { $key = $matches[1]; $value = $matches[2]; if (empty($value)) { $currentPath[] = $key; } else { $path = array_merge($currentPath, [$key]); $this->setNestedValue($data, $path, trim($value, '"\'')); } } } return $data; } /** * Load PO file. */ protected function loadPoFile(string $filepath): array { $content = file_get_contents($filepath); $lines = explode("\n", $content); $data = []; $currentMsgid = null; foreach ($lines as $line) { $line = trim($line); if (str_starts_with($line, 'msgid ')) { $currentMsgid = trim(substr($line, 6), '"'); } elseif (str_starts_with($line, 'msgstr ') && $currentMsgid) { $msgstr = trim(substr($line, 7), '"'); if ($currentMsgid !== '""') { $data[$currentMsgid] = $msgstr; } $currentMsgid = null; } } return $data; } /** * Set nested value. */ protected function setNestedValue(array &$array, array $path, $value): void { $current = &$array; foreach ($path as $key) { if (!isset($current[$key])) { $current[$key] = []; } $current = &$current[$key]; } $current = $value; } /** * Get group from path. */ protected function getGroupFromPath(string $filepath, string $baseDirectory): string { $relativePath = str_replace($baseDirectory . '/', '', $filepath); $parts = explode('/', $relativePath); $filename = array_pop($parts); $group = pathinfo($filename, PATHINFO_FILENAME); return empty($parts) ? $group : implode('/', $parts) . '/' . $group; } /** * Check if file is translation file. */ protected function isTranslationFile(string $filepath): bool { $extension = strtolower(pathinfo($filepath, PATHINFO_EXTENSION)); return in_array($extension, $this->config['translation_extensions']); } /** * Analyze progress. */ protected function analyzeProgress(string $referenceLanguage): void { $this->progressData = $this->analyzer->analyze($this->translationData, $referenceLanguage); } /** * Calculate completion. */ protected function calculateCompletion(): void { $this->progressData['completion'] = $this->calculator->calculate($this->progressData); } /** * Generate statistics. */ protected function generateStatistics(): void { $this->progressData['statistics'] = $this->generateOverallStatistics(); } /** * Generate overall statistics. */ protected function generateOverallStatistics(): array { $stats = []; // Language statistics foreach ($this->progressData['languages'] as $language => $progress) { $completion = $this->progressData['completion']['languages'][$language] ?? []; $stats['languages'][$language] = $this->generateLanguageStatistics($progress, $completion); } // Group statistics foreach ($this->progressData['groups'] as $group => $progress) { $completion = $this->progressData['completion']['groups'][$group] ?? []; $stats['groups'][$group] = $this->generateGroupStatistics($progress, $completion); } // Overall statistics $stats['overall'] = [ 'total_languages' => count($this->translationData), 'total_groups' => count($this->progressData['groups']), 'total_keys' => $this->progressData['total_keys'], 'average_completion' => $this->calculateAverageCompletion(), 'best_language' => $this->findBestLanguage(), 'worst_language' => $this->findWorstLanguage(), 'best_group' => $this->findBestGroup(), 'worst_group' => $this->findWorstGroup(), 'completion_distribution' => $this->getCompletionDistribution() ]; return $stats; } /** * Generate language statistics. */ protected function generateLanguageStatistics(array $progress, array $completion): array { return [ 'total_keys' => $progress['total_keys'] ?? 0, 'translated_keys' => $progress['translated_keys'] ?? 0, 'missing_keys' => $progress['missing_keys'] ?? 0, 'empty_keys' => $progress['empty_keys'] ?? 0, 'completion_rate' => $completion['rate'] ?? 0, 'quality_score' => $completion['quality_score'] ?? 0, 'status' => $this->getLanguageStatus($completion['rate'] ?? 0), 'needs_review' => $progress['needs_review'] ?? 0, 'placeholders_mismatch' => $progress['placeholders_mismatch'] ?? 0 ]; } /** * Generate group statistics. */ protected function generateGroupStatistics(array $progress, array $completion): array { return [ 'total_keys' => $progress['total_keys'] ?? 0, 'average_completion' => $completion['average_rate'] ?? 0, 'best_language' => $completion['best_language'] ?? null, 'worst_language' => $completion['worst_language'] ?? null, 'completion_range' => [ 'min' => $completion['min_rate'] ?? 0, 'max' => $completion['max_rate'] ?? 0 ], 'status' => $this->getGroupStatus($completion['average_rate'] ?? 0) ]; } /** * Calculate average completion. */ protected function calculateAverageCompletion(): float { if (empty($this->progressData['completion']['languages'])) { return 0.0; } $total = array_sum(array_column($this->progressData['completion']['languages'], 'rate')); $count = count($this->progressData['completion']['languages']); return $total / $count; } /** * Find best language. */ protected function findBestLanguage(): ?string { $bestRate = 0; $bestLanguage = null; foreach ($this->progressData['completion']['languages'] as $language => $completion) { if ($completion['rate'] > $bestRate) { $bestRate = $completion['rate']; $bestLanguage = $language; } } return $bestLanguage; } /** * Find worst language. */ protected function findWorstLanguage(): ?string { $worstRate = 100; $worstLanguage = null; foreach ($this->progressData['completion']['languages'] as $language => $completion) { if ($completion['rate'] < $worstRate) { $worstRate = $completion['rate']; $worstLanguage = $language; } } return $worstLanguage; } /** * Find best group. */ protected function findBestGroup(): ?string { $bestRate = 0; $bestGroup = null; foreach ($this->progressData['completion']['groups'] as $group => $completion) { if ($completion['average_rate'] > $bestRate) { $bestRate = $completion['average_rate']; $bestGroup = $group; } } return $bestGroup; } /** * Find worst group. */ protected function findWorstGroup(): ?string { $worstRate = 100; $worstGroup = null; foreach ($this->progressData['completion']['groups'] as $group => $completion) { if ($completion['average_rate'] < $worstRate) { $worstRate = $completion['average_rate']; $worstGroup = $group; } } return $worstGroup; } /** * Get completion distribution. */ protected function getCompletionDistribution(): array { $distribution = [ '0-25%' => 0, '26-50%' => 0, '51-75%' => 0, '76-99%' => 0, '100%' => 0 ]; foreach ($this->progressData['completion']['languages'] as $completion) { $rate = $completion['rate']; if ($rate == 100) { $distribution['100%']++; } elseif ($rate >= 76) { $distribution['76-99%']++; } elseif ($rate >= 51) { $distribution['51-75%']++; } elseif ($rate >= 26) { $distribution['26-50%']++; } else { $distribution['0-25%']++; } } return $distribution; } /** * Get language status. */ protected function getLanguageStatus(float $completionRate): string { if ($completionRate == 100) { return 'complete'; } elseif ($completionRate >= 90) { return 'nearly_complete'; } elseif ($completionRate >= 75) { return 'good_progress'; } elseif ($completionRate >= 50) { return 'in_progress'; } elseif ($completionRate >= 25) { return 'started'; } else { return 'not_started'; } } /** * Get group status. */ protected function getGroupStatus(float $completionRate): string { if ($completionRate == 100) { return 'complete'; } elseif ($completionRate >= 90) { return 'nearly_complete'; } elseif ($completionRate >= 75) { return 'good_progress'; } elseif ($completionRate >= 50) { return 'in_progress'; } elseif ($completionRate >= 25) { return 'started'; } else { return 'not_started'; } } /** * Calculate group overall completion. */ protected function calculateGroupOverallCompletion(array $groupProgress): float { if (empty($groupProgress)) { return 0.0; } $total = array_sum(array_column($groupProgress, 'completion')); $count = count($groupProgress); return $total / $count; } /** * Get progress results. */ public function getProgressResults(): array { return [ 'progress' => $this->progressData, 'translation_data' => $this->translationData, 'history' => $this->history, 'generated_at' => date('Y-m-d H:i:s'), 'config' => $this->config ]; } /** * Generate progress report. */ public function generateReport(string $format = 'text'): string { $results = $this->getProgressResults(); return $this->reporter->generate($results, $format); } /** * Save progress to history. */ public function saveToHistory(string $label = null): void { $snapshot = [ 'timestamp' => time(), 'label' => $label ?? date('Y-m-d H:i:s'), 'progress' => $this->progressData, 'completion' => $this->progressData['completion'] ?? [] ]; $this->history[] = $snapshot; // Keep only last N snapshots $maxHistory = $this->config['max_history'] ?? 100; if (count($this->history) > $maxHistory) { $this->history = array_slice($this->history, -$maxHistory); } } /** * Get progress history. */ public function getHistory(): array { return $this->history; } /** * Compare with previous snapshot. */ public function compareWithPrevious(): array { if (count($this->history) < 2) { return []; } $current = end($this->history); $previous = $this->history[count($this->history) - 2]; return $this->calculateProgressChange($previous, $current); } /** * Calculate progress change. */ protected function calculateProgressChange(array $previous, array $current): array { $changes = []; foreach ($current['completion']['languages'] as $language => $completion) { $previousCompletion = $previous['completion']['languages'][$language] ?? ['rate' => 0]; $changes['languages'][$language] = [ 'rate_change' => $completion['rate'] - $previousCompletion['rate'], 'translated_change' => ($completion['translated'] ?? 0) - ($previousCompletion['translated'] ?? 0), 'missing_change' => ($completion['missing'] ?? 0) - ($previousCompletion['missing'] ?? 0) ]; } return $changes; } /** * Export progress data. */ public function export(string $format = 'json'): string { $data = $this->getProgressResults(); switch ($format) { case 'json': return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); case 'csv': return $this->exportToCsv(); case 'xlsx': return $this->exportToXlsx(); default: throw new \InvalidArgumentException("Unsupported export format: {$format}"); } } /** * Export to CSV. */ protected function exportToCsv(): string { $csv = "Language,Total Keys,Translated,Missing,Empty,Completion Rate,Status\n"; if (isset($this->progressData['statistics']['languages'])) { foreach ($this->progressData['statistics']['languages'] as $language => $stats) { $csv .= "{$language},{$stats['total_keys']},{$stats['translated_keys']},"; $csv .= "{$stats['missing_keys']},{$stats['empty_keys']},"; $csv .= number_format($stats['completion_rate'], 2) . "%,{$stats['status']}\n"; } } return $csv; } /** * Export to XLSX (basic implementation). */ protected function exportToXlsx(): string { // This would require a proper XLSX library // For now, return CSV as placeholder return $this->exportToCsv(); } /** * Get completion trends. */ public function getCompletionTrends(): array { if (count($this->history) < 2) { return []; } $trends = []; foreach ($this->history as $snapshot) { $trends[] = [ 'timestamp' => $snapshot['timestamp'], 'label' => $snapshot['label'], 'overall_completion' => $this->calculateSnapshotOverallCompletion($snapshot) ]; } return $trends; } /** * Calculate snapshot overall completion. */ protected function calculateSnapshotOverallCompletion(array $snapshot): float { if (empty($snapshot['completion']['languages'])) { return 0.0; } $total = array_sum(array_column($snapshot['completion']['languages'], 'rate')); $count = count($snapshot['completion']['languages']); return $total / $count; } /** * Reset tracker state. */ protected function reset(): void { $this->translationData = []; $this->progressData = []; } /** * Get default configuration. */ protected function getDefaultConfig(): array { return [ 'translation_paths' => ['resources/lang', 'lang'], 'languages' => ['en', 'es', 'fr', 'de', 'zh-CN', 'ja', 'ko', 'pt', 'it', 'ru'], 'reference_language' => 'en', 'groups' => [], // Empty means all groups 'translation_extensions' => ['php', 'json', 'yaml', 'yml', 'po'], 'max_history' => 100, 'quality_weights' => [ 'completion_rate' => 0.6, 'placeholder_consistency' => 0.2, 'review_status' => 0.2 ], 'completion_thresholds' => [ 'complete' => 100, 'nearly_complete' => 90, 'good_progress' => 75, 'in_progress' => 50, 'started' => 25 ] ]; } /** * 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 tracker instance. */ public static function create(array $config = []): self { return new self($config); } /** * Create for Laravel project. */ public static function forLaravel(): self { return new self([ 'translation_paths' => ['resources/lang', 'lang'], 'languages' => ['en', 'es', 'fr', 'de', 'pt', 'it', 'ru', 'zh-CN', 'ja', 'ko', 'ar'], 'reference_language' => 'en' ]); } /** * Create for Symfony project. */ public static function forSymfony(): self { return new self([ 'translation_paths' => ['translations'], 'languages' => ['en', 'fr', 'de', 'es', 'it', 'pt', 'ru', 'zh-CN', 'ja'], 'reference_language' => 'en' ]); } /** * Create for Vue.js project. */ public static function forVue(): self { return new self([ 'translation_paths' => ['src/locales', 'locales'], 'languages' => ['en', 'es', 'fr', 'de', 'pt', 'it', 'ru', 'zh-CN', 'ja', 'ko'], 'reference_language' => 'en' ]); } }