first commit
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Commands\Translation;
|
||||
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use CodeIgniter\Helpers\Array\ArrayHelper;
|
||||
use Config\App;
|
||||
use Locale;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* @see \CodeIgniter\Commands\Translation\LocalizationFinderTest
|
||||
*/
|
||||
class LocalizationFinder extends BaseCommand
|
||||
{
|
||||
protected $group = 'Translation';
|
||||
protected $name = 'lang:find';
|
||||
protected $description = 'Find and save available phrases to translate.';
|
||||
protected $usage = 'lang:find [options]';
|
||||
protected $arguments = [];
|
||||
protected $options = [
|
||||
'--locale' => 'Specify locale (en, ru, etc.) to save files.',
|
||||
'--dir' => 'Directory to search for translations relative to APPPATH.',
|
||||
'--show-new' => 'Show only new translations in table. Does not write to files.',
|
||||
'--verbose' => 'Output detailed information.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Flag for output detailed information
|
||||
*/
|
||||
private bool $verbose = false;
|
||||
|
||||
/**
|
||||
* Flag for showing only translations, without saving
|
||||
*/
|
||||
private bool $showNew = false;
|
||||
|
||||
private string $languagePath;
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
$this->verbose = array_key_exists('verbose', $params);
|
||||
$this->showNew = array_key_exists('show-new', $params);
|
||||
$optionLocale = $params['locale'] ?? null;
|
||||
$optionDir = $params['dir'] ?? null;
|
||||
$currentLocale = Locale::getDefault();
|
||||
$currentDir = APPPATH;
|
||||
$this->languagePath = $currentDir . 'Language';
|
||||
|
||||
if (ENVIRONMENT === 'testing') {
|
||||
$currentDir = SUPPORTPATH . 'Services' . DIRECTORY_SEPARATOR;
|
||||
$this->languagePath = SUPPORTPATH . 'Language';
|
||||
}
|
||||
|
||||
if (is_string($optionLocale)) {
|
||||
if (! in_array($optionLocale, config(App::class)->supportedLocales, true)) {
|
||||
CLI::error(
|
||||
'Error: "' . $optionLocale . '" is not supported. Supported locales: '
|
||||
. implode(', ', config(App::class)->supportedLocales),
|
||||
);
|
||||
|
||||
return EXIT_USER_INPUT;
|
||||
}
|
||||
|
||||
$currentLocale = $optionLocale;
|
||||
}
|
||||
|
||||
if (is_string($optionDir)) {
|
||||
$tempCurrentDir = realpath($currentDir . $optionDir);
|
||||
|
||||
if ($tempCurrentDir === false) {
|
||||
CLI::error('Error: Directory must be located in "' . $currentDir . '"');
|
||||
|
||||
return EXIT_USER_INPUT;
|
||||
}
|
||||
|
||||
if ($this->isSubDirectory($tempCurrentDir, $this->languagePath)) {
|
||||
CLI::error('Error: Directory "' . $this->languagePath . '" restricted to scan.');
|
||||
|
||||
return EXIT_USER_INPUT;
|
||||
}
|
||||
|
||||
$currentDir = $tempCurrentDir;
|
||||
}
|
||||
|
||||
$this->process($currentDir, $currentLocale);
|
||||
|
||||
CLI::write('All operations done!');
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function process(string $currentDir, string $currentLocale): void
|
||||
{
|
||||
$tableRows = [];
|
||||
$countNewKeys = 0;
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($currentDir));
|
||||
$files = iterator_to_array($iterator, true);
|
||||
ksort($files);
|
||||
|
||||
[
|
||||
'foundLanguageKeys' => $foundLanguageKeys,
|
||||
'badLanguageKeys' => $badLanguageKeys,
|
||||
'countFiles' => $countFiles,
|
||||
] = $this->findLanguageKeysInFiles($files);
|
||||
|
||||
ksort($foundLanguageKeys);
|
||||
|
||||
$languageDiff = [];
|
||||
$languageFoundGroups = array_unique(array_keys($foundLanguageKeys));
|
||||
|
||||
foreach ($languageFoundGroups as $langFileName) {
|
||||
$languageStoredKeys = [];
|
||||
$languageFilePath = $this->languagePath . DIRECTORY_SEPARATOR . $currentLocale . DIRECTORY_SEPARATOR . $langFileName . '.php';
|
||||
|
||||
if (is_file($languageFilePath)) {
|
||||
// Load old localization
|
||||
$languageStoredKeys = require $languageFilePath;
|
||||
}
|
||||
|
||||
$languageDiff = ArrayHelper::recursiveDiff($foundLanguageKeys[$langFileName], $languageStoredKeys);
|
||||
$countNewKeys += ArrayHelper::recursiveCount($languageDiff);
|
||||
|
||||
if ($this->showNew) {
|
||||
$tableRows = array_merge($this->arrayToTableRows($langFileName, $languageDiff), $tableRows);
|
||||
} else {
|
||||
$newLanguageKeys = array_replace_recursive($foundLanguageKeys[$langFileName], $languageStoredKeys);
|
||||
|
||||
if ($languageDiff !== []) {
|
||||
if (file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys)) === false) {
|
||||
$this->writeIsVerbose('Lang file ' . $langFileName . ' (error write).', 'red');
|
||||
} else {
|
||||
$this->writeIsVerbose('Lang file "' . $langFileName . '" successful updated!', 'green');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->showNew && $tableRows !== []) {
|
||||
sort($tableRows);
|
||||
CLI::table($tableRows, ['File', 'Key']);
|
||||
}
|
||||
|
||||
if (! $this->showNew && $countNewKeys > 0) {
|
||||
CLI::write('Note: You need to run your linting tool to fix coding standards issues.', 'white', 'red');
|
||||
}
|
||||
|
||||
$this->writeIsVerbose('Files found: ' . $countFiles);
|
||||
$this->writeIsVerbose('New translates found: ' . $countNewKeys);
|
||||
$this->writeIsVerbose('Bad translates found: ' . count($badLanguageKeys));
|
||||
|
||||
if ($this->verbose && $badLanguageKeys !== []) {
|
||||
$tableBadRows = [];
|
||||
|
||||
foreach ($badLanguageKeys as $value) {
|
||||
$tableBadRows[] = [$value[1], $value[0]];
|
||||
}
|
||||
|
||||
ArrayHelper::sortValuesByNatural($tableBadRows, 0);
|
||||
|
||||
CLI::table($tableBadRows, ['Bad Key', 'Filepath']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SplFileInfo|string $file
|
||||
*
|
||||
* @return array<string, array>
|
||||
*/
|
||||
private function findTranslationsInFile($file): array
|
||||
{
|
||||
$foundLanguageKeys = [];
|
||||
$badLanguageKeys = [];
|
||||
|
||||
if (is_string($file) && is_file($file)) {
|
||||
$file = new SplFileInfo($file);
|
||||
}
|
||||
|
||||
$fileContent = file_get_contents($file->getRealPath());
|
||||
preg_match_all('/lang\(\'([._a-z0-9\-]+)\'\)/ui', $fileContent, $matches);
|
||||
|
||||
if ($matches[1] === []) {
|
||||
return compact('foundLanguageKeys', 'badLanguageKeys');
|
||||
}
|
||||
|
||||
foreach ($matches[1] as $phraseKey) {
|
||||
$phraseKeys = explode('.', $phraseKey);
|
||||
|
||||
// Language key not have Filename or Lang key
|
||||
if (count($phraseKeys) < 2) {
|
||||
$badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$languageFileName = array_shift($phraseKeys);
|
||||
$isEmptyNestedArray = ($languageFileName !== '' && $phraseKeys[0] === '')
|
||||
|| ($languageFileName === '' && $phraseKeys[0] !== '')
|
||||
|| ($languageFileName === '' && $phraseKeys[0] === '');
|
||||
|
||||
if ($isEmptyNestedArray) {
|
||||
$badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($phraseKeys) === 1) {
|
||||
$foundLanguageKeys[$languageFileName][$phraseKeys[0]] = $phraseKey;
|
||||
} else {
|
||||
$childKeys = $this->buildMultiArray($phraseKeys, $phraseKey);
|
||||
|
||||
$foundLanguageKeys[$languageFileName] = array_replace_recursive($foundLanguageKeys[$languageFileName] ?? [], $childKeys);
|
||||
}
|
||||
}
|
||||
|
||||
return compact('foundLanguageKeys', 'badLanguageKeys');
|
||||
}
|
||||
|
||||
private function isIgnoredFile(SplFileInfo $file): bool
|
||||
{
|
||||
if ($file->isDir() || $this->isSubDirectory($file->getRealPath(), $this->languagePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $file->getExtension() !== 'php';
|
||||
}
|
||||
|
||||
private function templateFile(array $language = []): string
|
||||
{
|
||||
if ($language !== []) {
|
||||
$languageArrayString = var_export($language, true);
|
||||
|
||||
$code = <<<PHP
|
||||
<?php
|
||||
|
||||
return {$languageArrayString};
|
||||
|
||||
PHP;
|
||||
|
||||
return $this->replaceArraySyntax($code);
|
||||
}
|
||||
|
||||
return <<<'PHP'
|
||||
<?php
|
||||
|
||||
return [];
|
||||
|
||||
PHP;
|
||||
}
|
||||
|
||||
private function replaceArraySyntax(string $code): string
|
||||
{
|
||||
$tokens = token_get_all($code);
|
||||
$newTokens = $tokens;
|
||||
|
||||
foreach ($tokens as $i => $token) {
|
||||
if (is_array($token)) {
|
||||
[$tokenId, $tokenValue] = $token;
|
||||
|
||||
// Replace "array ("
|
||||
if (
|
||||
$tokenId === T_ARRAY
|
||||
&& $tokens[$i + 1][0] === T_WHITESPACE
|
||||
&& $tokens[$i + 2] === '('
|
||||
) {
|
||||
$newTokens[$i][1] = '[';
|
||||
$newTokens[$i + 1][1] = '';
|
||||
$newTokens[$i + 2] = '';
|
||||
}
|
||||
|
||||
// Replace indent
|
||||
if ($tokenId === T_WHITESPACE && preg_match('/\n([ ]+)/u', $tokenValue, $matches)) {
|
||||
$newTokens[$i][1] = "\n{$matches[1]}{$matches[1]}";
|
||||
}
|
||||
} // Replace ")"
|
||||
elseif ($token === ')') {
|
||||
$newTokens[$i] = ']';
|
||||
}
|
||||
}
|
||||
|
||||
$output = '';
|
||||
|
||||
foreach ($newTokens as $token) {
|
||||
$output .= $token[1] ?? $token;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multidimensional array from another keys
|
||||
*/
|
||||
private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''): array
|
||||
{
|
||||
$newArray = [];
|
||||
$lastIndex = array_pop($fromKeys);
|
||||
$current = &$newArray;
|
||||
|
||||
foreach ($fromKeys as $value) {
|
||||
$current[$value] = [];
|
||||
$current = &$current[$value];
|
||||
}
|
||||
|
||||
$current[$lastIndex] = $lastArrayValue;
|
||||
|
||||
return $newArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert multi arrays to specific CLI table rows (flat array)
|
||||
*/
|
||||
private function arrayToTableRows(string $langFileName, array $array): array
|
||||
{
|
||||
$rows = [];
|
||||
|
||||
foreach ($array as $value) {
|
||||
if (is_array($value)) {
|
||||
$rows = array_merge($rows, $this->arrayToTableRows($langFileName, $value));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$rows[] = [$langFileName, $value];
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show details in the console if the flag is set
|
||||
*/
|
||||
private function writeIsVerbose(string $text = '', ?string $foreground = null, ?string $background = null): void
|
||||
{
|
||||
if ($this->verbose) {
|
||||
CLI::write($text, $foreground, $background);
|
||||
}
|
||||
}
|
||||
|
||||
private function isSubDirectory(string $directory, string $rootDirectory): bool
|
||||
{
|
||||
return 0 === strncmp($directory, $rootDirectory, strlen($directory));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<SplFileInfo> $files
|
||||
*
|
||||
* @return array{'foundLanguageKeys': array<string, array<string, string>>, 'badLanguageKeys': array<int, array<int, string>>, 'countFiles': int}
|
||||
*/
|
||||
private function findLanguageKeysInFiles(array $files): array
|
||||
{
|
||||
$foundLanguageKeys = [];
|
||||
$badLanguageKeys = [];
|
||||
$countFiles = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($this->isIgnoredFile($file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->writeIsVerbose('File found: ' . mb_substr($file->getRealPath(), mb_strlen(APPPATH)));
|
||||
$countFiles++;
|
||||
|
||||
$findInFile = $this->findTranslationsInFile($file);
|
||||
|
||||
$foundLanguageKeys = array_replace_recursive($findInFile['foundLanguageKeys'], $foundLanguageKeys);
|
||||
$badLanguageKeys = array_merge($findInFile['badLanguageKeys'], $badLanguageKeys);
|
||||
}
|
||||
|
||||
return compact('foundLanguageKeys', 'badLanguageKeys', 'countFiles');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Commands\Translation;
|
||||
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use CodeIgniter\Exceptions\LogicException;
|
||||
use Config\App;
|
||||
use ErrorException;
|
||||
use FilesystemIterator;
|
||||
use Locale;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* @see \CodeIgniter\Commands\Translation\LocalizationSyncTest
|
||||
*/
|
||||
class LocalizationSync extends BaseCommand
|
||||
{
|
||||
protected $group = 'Translation';
|
||||
protected $name = 'lang:sync';
|
||||
protected $description = 'Synchronize translation files from one language to another.';
|
||||
protected $usage = 'lang:sync [options]';
|
||||
protected $arguments = [];
|
||||
protected $options = [
|
||||
'--locale' => 'The original locale (en, ru, etc.).',
|
||||
'--target' => 'Target locale (en, ru, etc.).',
|
||||
];
|
||||
private string $languagePath;
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
$optionTargetLocale = '';
|
||||
$optionLocale = $params['locale'] ?? Locale::getDefault();
|
||||
$this->languagePath = APPPATH . 'Language';
|
||||
|
||||
if (isset($params['target']) && $params['target'] !== '') {
|
||||
$optionTargetLocale = $params['target'];
|
||||
}
|
||||
|
||||
if (! in_array($optionLocale, config(App::class)->supportedLocales, true)) {
|
||||
CLI::error(
|
||||
'Error: "' . $optionLocale . '" is not supported. Supported locales: '
|
||||
. implode(', ', config(App::class)->supportedLocales),
|
||||
);
|
||||
|
||||
return EXIT_USER_INPUT;
|
||||
}
|
||||
|
||||
if ($optionTargetLocale === '') {
|
||||
CLI::error(
|
||||
'Error: "--target" is not configured. Supported locales: '
|
||||
. implode(', ', config(App::class)->supportedLocales),
|
||||
);
|
||||
|
||||
return EXIT_USER_INPUT;
|
||||
}
|
||||
|
||||
if (! in_array($optionTargetLocale, config(App::class)->supportedLocales, true)) {
|
||||
CLI::error(
|
||||
'Error: "' . $optionTargetLocale . '" is not supported. Supported locales: '
|
||||
. implode(', ', config(App::class)->supportedLocales),
|
||||
);
|
||||
|
||||
return EXIT_USER_INPUT;
|
||||
}
|
||||
|
||||
if ($optionTargetLocale === $optionLocale) {
|
||||
CLI::error(
|
||||
'Error: You cannot have the same values for "--target" and "--locale".',
|
||||
);
|
||||
|
||||
return EXIT_USER_INPUT;
|
||||
}
|
||||
|
||||
if (ENVIRONMENT === 'testing') {
|
||||
$this->languagePath = SUPPORTPATH . 'Language';
|
||||
}
|
||||
|
||||
if ($this->process($optionLocale, $optionTargetLocale) === EXIT_ERROR) {
|
||||
return EXIT_ERROR;
|
||||
}
|
||||
|
||||
CLI::write('All operations done!');
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function process(string $originalLocale, string $targetLocale): int
|
||||
{
|
||||
$originalLocaleDir = $this->languagePath . DIRECTORY_SEPARATOR . $originalLocale;
|
||||
$targetLocaleDir = $this->languagePath . DIRECTORY_SEPARATOR . $targetLocale;
|
||||
|
||||
if (! is_dir($originalLocaleDir)) {
|
||||
CLI::error(
|
||||
'Error: The "' . clean_path($originalLocaleDir) . '" directory was not found.',
|
||||
);
|
||||
|
||||
return EXIT_ERROR;
|
||||
}
|
||||
|
||||
// Unifying the error - mkdir() may cause an exception.
|
||||
try {
|
||||
if (! is_dir($targetLocaleDir) && ! mkdir($targetLocaleDir, 0775)) {
|
||||
throw new ErrorException();
|
||||
}
|
||||
} catch (ErrorException $e) {
|
||||
CLI::error(
|
||||
'Error: The target directory "' . clean_path($targetLocaleDir) . '" cannot be accessed.',
|
||||
);
|
||||
|
||||
return EXIT_ERROR;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator(
|
||||
$originalLocaleDir,
|
||||
FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS,
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @var array<non-empty-string, SplFileInfo> $files
|
||||
*/
|
||||
$files = iterator_to_array($iterator, true);
|
||||
ksort($files);
|
||||
|
||||
foreach ($files as $originalLanguageFile) {
|
||||
if ($originalLanguageFile->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetLanguageFile = $targetLocaleDir . DIRECTORY_SEPARATOR . $originalLanguageFile->getFilename();
|
||||
|
||||
$targetLanguageKeys = [];
|
||||
$originalLanguageKeys = include $originalLanguageFile;
|
||||
|
||||
if (is_file($targetLanguageFile)) {
|
||||
$targetLanguageKeys = include $targetLanguageFile;
|
||||
}
|
||||
|
||||
$targetLanguageKeys = $this->mergeLanguageKeys($originalLanguageKeys, $targetLanguageKeys, $originalLanguageFile->getBasename('.php'));
|
||||
|
||||
$content = "<?php\n\nreturn " . var_export($targetLanguageKeys, true) . ";\n";
|
||||
file_put_contents($targetLanguageFile, $content);
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string,mixed>|string|null> $originalLanguageKeys
|
||||
* @param array<string, array<string,mixed>|string|null> $targetLanguageKeys
|
||||
*
|
||||
* @return array<string, array<string,mixed>|string|null>
|
||||
*/
|
||||
private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLanguageKeys, string $prefix = ''): array
|
||||
{
|
||||
$mergedLanguageKeys = [];
|
||||
|
||||
foreach ($originalLanguageKeys as $key => $value) {
|
||||
$placeholderValue = $prefix !== '' ? $prefix . '.' . $key : $key;
|
||||
|
||||
if (is_string($value)) {
|
||||
// Keep the old value
|
||||
// TODO: The value type may not match the original one
|
||||
if (array_key_exists($key, $targetLanguageKeys)) {
|
||||
$mergedLanguageKeys[$key] = $targetLanguageKeys[$key];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set new key with placeholder
|
||||
$mergedLanguageKeys[$key] = $placeholderValue;
|
||||
} elseif (is_array($value)) {
|
||||
if (! array_key_exists($key, $targetLanguageKeys)) {
|
||||
$mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, [], $placeholderValue);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, $targetLanguageKeys[$key], $placeholderValue);
|
||||
} else {
|
||||
throw new LogicException('Value for the key "' . $placeholderValue . '" is of the wrong type. Only "array" or "string" is allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return $mergedLanguageKeys;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user