Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 2k
feat: Language translations finder and update#7896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
32c530703a81b6624ba01034f9dd1fc2852119f386e6707619602e7949d2c5713d93ea314577210c81bc00284d13b86c1aa5f9a79168c5af37ece4e9982db0d7a724b3b97664ca3652b672f8f884e5ae6156b947c56437462bff7ecde1e02b3cb7260d3ad09b55964e32ca130f6d9b2becad354ddf73f38122123b025df1d143a9b1026314db1567e1c16cb448f8f657bb80d9e22599ad675308935b387070d7e5File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,357 @@ | ||
| <?php | ||
| /** | ||
| * 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\Commands\Translation\LocalizationFinder\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)) { | ||
paulbalandan marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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 (false === $tempCurrentDir) { | ||
| CLI::error('Error: Directory must be located in "' . $currentDir . '"'); | ||
| return EXIT_USER_INPUT; | ||
| } | ||
neznaika0 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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; | ||
| } | ||
neznaika0 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
neznaika0 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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); | ||
neznaika0 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| [$foundLanguageKeys, $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 (false === file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys))) { | ||
| $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); | ||
| } | ||
| /** | ||
| * @param SplFileInfo|string $file | ||
| */ | ||
| private function findTranslationsInFile($file): array | ||
| { | ||
| $foundLanguageKeys = []; | ||
| 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 []; | ||
| } | ||
| foreach ($matches[1] as $phraseKey) { | ||
| $phraseKeys = explode('.', $phraseKey); | ||
| // Language key not have Filename or Lang key | ||
| if (count($phraseKeys) < 2) { | ||
| continue; | ||
| } | ||
| $languageFileName = array_shift($phraseKeys); | ||
| $isEmptyNestedArray = ($languageFileName !== '' && $phraseKeys[0] === '') | ||
| || ($languageFileName === '' && $phraseKeys[0] !== '') | ||
| || ($languageFileName === '' && $phraseKeys[0] === ''); | ||
| if ($isEmptyNestedArray) { | ||
| 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 $foundLanguageKeys; | ||
| } | ||
| 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}; | ||
neznaika0 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| PHP; | ||
| return $this->replaceArraySyntax($code); | ||
| } | ||
| return <<<'PHP' | ||
| <?php | ||
| return []; | ||
neznaika0 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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 SplFileInfo[] $files | ||
| * | ||
| * @return array<int, array|int> | ||
neznaika0 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| * @phpstan-return list{0: array<string, array<string, string>>, 1: int} | ||
| */ | ||
| private function findLanguageKeysInFiles(array $files): array | ||
| { | ||
| $foundLanguageKeys = []; | ||
| $countFiles = 0; | ||
| foreach ($files as $file) { | ||
| if ($this->isIgnoredFile($file)) { | ||
| continue; | ||
| } | ||
| $this->writeIsVerbose('File found: ' . mb_substr($file->getRealPath(), mb_strlen(APPPATH))); | ||
| $countFiles++; | ||
| $foundLanguageKeys = array_replace_recursive($this->findTranslationsInFile($file), $foundLanguageKeys); | ||
| } | ||
| return [$foundLanguageKeys, $countFiles]; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.