Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 104 additions & 199 deletions ProcessMaker/Console/Commands/SyncTranslations.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use ProcessMaker\Helpers\SyncJsonTranslations;
use ProcessMaker\Helpers\SyncPhpTranslations;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator;

class SyncTranslations extends Command
{
Expand All @@ -31,231 +34,133 @@ class SyncTranslations extends Command
*/
public function handle()
{
// Check if exists resources core
$translationsCore = base_path() . '/resources-core';
$existsLangOrig = $this->fileExists(lang_path() . '.orig');
$this->info('Starting translation synchronization...');
$this->newLine();

if (!$this->fileExists($translationsCore)) {
$this->error('The folder resources-core not exists.');
$jsonResults = (new SyncJsonTranslations())->sync();
// dump("JSON Results: ", $jsonResults);
$phpResults = (new SyncPhpTranslations())->sync();

return;
}
//Search files
$this->listFiles($translationsCore . '/lang');

// updating languages by default
foreach ($this->files as $pathFile) {
if (!(str_contains($pathFile, '.json') || str_contains($pathFile, '.php')) || str_contains($pathFile, '.bak.')) {
continue;
}
// updating resources/lang
$this->syncFile(str_replace('/resources-core/', '/resources/', $pathFile), $pathFile);
if ($existsLangOrig) {
// updating resources/lang.orig
$this->syncFile(str_replace(['/resources-core/', '/lang/'], ['/resources/', '/lang.orig/'], $pathFile), $pathFile);
}
}
$this->displayJsonResults($jsonResults);
$this->newLine();
$this->displayPhpResults($phpResults);

// updating all languages with new labels
$this->files = [];
$translationsCore = lang_path();
$this->listFiles($translationsCore);
foreach ($this->files as $pathFile) {
if (!(str_contains($pathFile, '.json') || str_contains($pathFile, '.php')) || str_contains($pathFile, '.bak.')) {
continue;
}
// updating resources/lang
$backup = str_replace('/resources/', '/resources-core/', preg_replace('/(?<=lang).+?(?=json)/', '/en.', $pathFile));
$path1 = explode('/lang/', $backup);
$path2 = explode('/', $path1[1]);
if (is_array($path2)) {
$backup = str_replace('/' . $path2[0] . '/', '/en/', $backup);
}
$this->newLine();
$this->info('Translation synchronization completed!');

$this->syncFile($pathFile, $backup);
if ($existsLangOrig) {
// updating resources/lang.orig
$this->syncFile(str_replace('/lang/', '/lang.orig/', $pathFile), $backup);
}
}
return 0;
}

private function listFiles($dir)
/**
* Display JSON translation results in a table format
*
* @param array $results
* @return void
*/
private function displayJsonResults(array $results): void
{
$files = scandir($dir);
$this->info('JSON Translation Files:');

foreach ($files as $value) {
$path = $dir . '/' . $value;
if (!is_dir($path)) {
$this->files[] = $path;
} elseif ($value != '.' && $value != '..') {
$this->listFiles($path);
$table = new Table($this->output);
$table->setHeaders(['Language', 'File', 'Action', 'New Keys', 'Total Keys', 'Backup', 'Status']);
$additionalChanges = Arr::get($results, 'en.otherLanguageResults');

foreach ($results as $language => $result) {
if (isset($additionalChanges[$language])) {
if ($result['action'] !== 'no_changes') {
$this->addRowToTable($table, $result, $language);
}
$this->addRowToTable($table, $additionalChanges[$language], $language);
} else {
$this->addRowToTable($table, $result, $language);
}
}
}

private function fileExists($path)
{
return File::exists($path);
$table->render();
}

private function parseFile($path)
private function addRowToTable(Table $table, array $result, string $language)
{
$pathInfo = pathinfo($path);

$lines = [];

try {
if ($pathInfo['extension'] === 'json') {
$lines = json_decode(file_get_contents($path), true);
} elseif ($pathInfo['extension'] === 'php') {
$lines = include $path;
}
} catch (\Exception $e) {
$lines = [];
$this->error($path . '. Not found.');
$status = $result['error'] ? '<error>Error</error>' : '<info>Success</info>';
$action = $this->formatAction($result['action']);
$backup = $result['backup_created'] ? '<comment>Yes</comment>' : 'No';

$table->addRow([
$language,
$result['filename'],
$action,
$result['new_keys'],
$result['total_keys'],
$backup,
$status,
]);

if ($result['error']) {
$table->addRow(new TableSeparator());
$table->addRow(['', '', '', '', '', '', '<error>' . $result['error'] . '</error>']);
}

$lines = Arr::dot($lines);

return collect($lines);
}

/**
* Synchronize translations between target and backup files
* Display PHP translation results in a table format
*
* @param string $target Path to target file
* @param string $backup Path to backup file
* @return bool
* @throws \Exception
* @param array $results
* @return void
*/
private function syncFile($target, $backup)
private function displayPhpResults(array $results): void
{
if (str_contains($target, '.bak.')) {
// Clean up old backup if everything succeeded
if (file_exists($target)) {
unlink($target);
$this->info('Removed bak: ' . $target);
}
$this->info("Skipping backup file: {$target}");

return true;
}
// Create backup before modifications
$backupPath = $target . '.bak.' . date('Y-m-d-His');
try {
if (!copy($target, $backupPath)) {
$this->error("Failed to create backup file: {$backupPath}");

return false;
$this->info('PHP Translation Files:');

$table = new Table($this->output);
$table->setHeaders(['Language', 'Files Processed', 'Copied', 'Merged', 'No Changes', 'Errors', 'Status']);

foreach ($results as $language => $result) {
$errorCount = count($result['errors']);
$status = $errorCount > 0 ? '<error>Errors</error>' : '<info>Success</info>';

$table->addRow([
$language,
$result['files_processed'],
$result['files_copied'],
$result['files_merged'],
$result['files_no_changes'],
$errorCount,
$status,
]);

// Add error details if any
if ($errorCount > 0) {
$table->addRow(new TableSeparator());
foreach ($result['errors'] as $error) {
$table->addRow(['', '', '', '', '', '', '<error>' . $error . '</error>']);
}
}
$this->info("Backup created: {$backupPath}");
} catch (\Exception $e) {
$this->error('Error creating backup: ' . $e->getMessage());

return false;
}

$pathInfo = pathinfo($target);

try {
$targetTranslations = $this->parseFile($target);
$origin = $this->parseFile($backup);
} catch (\Exception $e) {
$this->error('Error parsing files: ' . $e->getMessage());

return false;
}

// Get keys that are in origin but not in target
$diff = $origin->diffKeys($targetTranslations);

if ($diff->isNotEmpty()) {
$this->info('Found ' . $diff->count() . " new translations to add in {$target}");

// only files en.json to en.json have translations others are empty
$clear = true;
if (str_contains($target, 'en.json') && str_contains($backup, 'en.json')) {
$clear = false;
}

// Add new keys to targetTranslations
foreach ($diff as $key => $value) {
$targetTranslations[$key] = $clear ? '' : $value;
}
}

try {
$contents = $this->generateFile($targetTranslations, $pathInfo['extension']);

// Validate content before saving
if (empty($contents)) {
throw new \Exception('Generated content is empty');
}

// Use atomic file writing
$tempFile = $target . '.tmp';
if (file_put_contents($tempFile, $contents) === false) {
throw new \Exception('Failed to write temporary file');
}

if (!rename($tempFile, $target)) {
unlink($tempFile);
throw new \Exception('Failed to move temporary file');
}

$this->info("Successfully updated: {$target}");

// Clean up old backup if everything succeeded
if (file_exists($backupPath)) {
unlink($backupPath);
$this->info('Removed backup file after successful update');
}

if ($pathInfo['extension'] == 'php') {
$this->clearCache();
}

return true;
} catch (\Exception $e) {
// Restore from backup if something went wrong
if (file_exists($backupPath)) {
copy($backupPath, $target);
$this->info('Restored from backup due to error');
}
$this->error('Error saving file: ' . $e->getMessage());

return false;
}
$table->render();
}

private function generateFile($lines, $type)
{
$array = [];

foreach ($lines as $key => $line) {
$array[$key] = $line;
}

if ($type === 'json') {
return json_encode($array, JSON_PRETTY_PRINT);
} elseif ($type === 'php') {
$contents = "<?php\n\nreturn [\n\n";
foreach ($array as $key => $value) {
$key = addslashes($key);
$value = addslashes($value);
$contents .= "\t'$key' => '$value',\n";
}
$contents .= '];';

return $contents;
}
}

private function clearCache()
/**
* Format action for display
*
* @param string $action
* @return string
*/
private function formatAction(string $action): string
{
if (function_exists('opcache_reset')) {
return opcache_reset();
switch ($action) {
case 'copied':
return '<comment>Copied</comment>';
case 'merged':
return '<info>Merged</info>';
case 'updated':
return '<fg=cyan>Updated</fg=cyan>';
case 'no_changes':
return '<fg=gray>No Changes</fg=gray>';
case 'error':
return '<error>Error</error>';
default:
return $action;
}
}
}
Loading
Loading