mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
847 lines
26 KiB
PHP
847 lines
26 KiB
PHP
|
|
<?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'
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|