-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Merged
Merged
Changes from all commits
Commits
Show all changes
46 commits
Select commit
Hold shift + click to select a range
32c5307
feat: Add lang:find command
neznaika0 03a81b6
test: Add test for lang:find command
neznaika0 624ba01
docs: Description using command
neznaika0 034f9dd
docs: Example code with command
neznaika0 1fc2852
fix: Sorting of found files
neznaika0 119f386
test: Reduce the search depth
neznaika0 e670761
test: Added missed actions
neznaika0 9602e79
test: Move test file to subfolder
neznaika0 49d2c57
fix: Missing sorted files in a loop
neznaika0 13d93ea
fix: Remove prefix variables
neznaika0 3145772
fix: Typo in desciption
neznaika0 10c81bc
fix: Return with a constant exit code
neznaika0 00284d1
fix: Error text is corrected
neznaika0 3b86c1a
fix: Format saved array code
neznaika0 a5f9a79
fix: Add method test is subfolder
neznaika0 168c5af
fix: Move actions to tearDown()
neznaika0 37ece4e
fix: Add makeLocaleDirectory()
neznaika0 9982db0
docs: Correct spelling
neznaika0 d7a724b
fix: Assert returned code
neznaika0 3b97664
fix: Method to private
neznaika0 ca3652b
fix: Move long code to process()
neznaika0 672f8f8
docs: add changelog
neznaika0 84e5ae6
docs: add link
neznaika0 156b947
docs: Language correction
neznaika0 c564374
fix: Language correction
neznaika0 62bff7e
fix: Shorten the syntax
neznaika0 cde1e02
style: Format code
neznaika0 b3cb726
docs: add @phpstan-return
neznaika0 0d3ad09
fix: Replace str_contains()
neznaika0 b55964e
fix: Shorten the syntax
neznaika0 32ca130
fix: Rename variable to foundLanguageKeys
neznaika0 f6d9b2b
tests: Delete unnecessary cleaning
neznaika0 ecad354
fix: Add array helper as separate class
neznaika0 ddf73f3
test: Add array helper tests
neznaika0 8122123
fix: Move methods to array helper class
neznaika0 b025df1
fix: Language correction
neznaika0 d143a9b
style: Format code
neznaika0 1026314
style: Format code
neznaika0 db1567e
tests: Compliance with the project rules
neznaika0 1c16cb4
fix: Added a warning instead of cs-fix
neznaika0 48f8f65
style: Format code
neznaika0 7bb80d9
fix: Replace array syntax
neznaika0 e22599a
docs: add formatting advice
neznaika0 d675308
style: compliance with phpstan strict rules
neznaika0 935b387
style: compliance with rector rules
neznaika0 070d7e5
docs: update note in localization.rst
neznaika0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 <[email protected]> | ||
* | ||
* 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.
Show resolved
Hide resolved
|
||
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.
Show resolved
Hide resolved
|
||
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.
Show resolved
Hide resolved
|
||
|
||
neznaika0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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.
Show resolved
Hide resolved
|
||
|
||
[$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.
Show resolved
Hide resolved
|
||
|
||
PHP; | ||
|
||
return $this->replaceArraySyntax($code); | ||
} | ||
|
||
return <<<'PHP' | ||
<?php | ||
|
||
return []; | ||
neznaika0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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.
Show resolved
Hide resolved
|
||
* @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]; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.