Files
FendxPHP/fendx-framework/fendx-i18n/src/Tools/Statistics/TranslationProgressTracker.php

847 lines
26 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Fendx\I18n\Tools\Statistics;
use Fendx\I18n\Tools\Statistics\Analyzer\ProgressAnalyzer;
use Fendx\I18n\Tools\Statistics\Reporter\ProgressReporter;
use Fendx\I18n\Tools\Statistics\Calculator\CompletionCalculator;
class TranslationProgressTracker
{
protected ProgressAnalyzer $analyzer;
protected ProgressReporter $reporter;
protected CompletionCalculator $calculator;
protected array $config = [];
protected array $translationData = [];
protected array $progressData = [];
protected array $history = [];
public function __construct(array $config = [])
{
$this->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'
]);
}
}