From 3fa7394f827ce190a5044ca579cd7f3e1e54164b Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 5 Jul 2024 20:09:26 +0300 Subject: [PATCH 01/18] feat: Add `lang:sync` command --- .../Commands/Translation/LocalizationSync.php | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 system/Commands/Translation/LocalizationSync.php diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php new file mode 100644 index 000000000000..b2bff0c1d473 --- /dev/null +++ b/system/Commands/Translation/LocalizationSync.php @@ -0,0 +1,180 @@ + + * + * 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\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 "--target" and "--locale".' + ); + + return EXIT_USER_INPUT; + } + + if (ENVIRONMENT === 'testing') { + $this->languagePath = SUPPORTPATH . 'Language'; + } + + $this->process($optionLocale, $optionTargetLocale); + + CLI::write('All operations done!'); + + return EXIT_SUCCESS; + } + + private function process(string $originalLocale, string $targetLocale): void + { + $originalLocaleDir = $this->languagePath . DIRECTORY_SEPARATOR . $originalLocale; + $targetLocaleDir = $this->languagePath . DIRECTORY_SEPARATOR . $targetLocale; + + if (! is_dir($originalLocaleDir)) { + CLI::error( + 'Error: The "' . $originalLocaleDir . '" directory was not found.' + ); + } + + if (! is_dir($targetLocaleDir) && ! mkdir($targetLocaleDir, 0775)) { + CLI::error( + 'Error: The target directory "' . $targetLocaleDir . '" cannot be accessed.' + ); + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($originalLocaleDir)); + + /** + * @var list $files + */ + $files = iterator_to_array($iterator, true); + ksort($files); + + foreach ($files as $originalLanguageFile) { + if ($this->isIgnoredFile($originalLanguageFile)) { + 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')); + ksort($targetLanguageKeys); + + $content = "|string|null> $originalLanguageKeys + * @param array|string|null> $targetLanguageKeys + * + * @return array|string|null> + */ + private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLanguageKeys, string $prefix = ''): array + { + foreach ($originalLanguageKeys as $key => $value) { + $placeholderValue = $prefix === '' ? $prefix . '.' . $key : $key; + + if (! is_array($value)) { + // Keep the old value + // TODO: The value type may not match the original one + if (array_key_exists($key, $targetLanguageKeys)) { + continue; + } + + // Set new key with placeholder + $targetLanguageKeys[$key] = $placeholderValue; + } else { + if (! array_key_exists($key, $targetLanguageKeys)) { + $targetLanguageKeys[$key] = []; + } + + $targetLanguageKeys[$key] = $this->mergeLanguageKeys($value, $targetLanguageKeys[$key], $placeholderValue); + } + } + + return ArrayHelper::intersectKeyRecursive($targetLanguageKeys, $originalLanguageKeys); + } + + private function isIgnoredFile(SplFileInfo $file): bool + { + return $file->isDir() || $file->getFilename() === '.' || $file->getFilename() === '..' || $file->getExtension() !== 'php'; + } +} From 872434a6ffd2c5dc831ed47c3414f7b801ebbab1 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 5 Jul 2024 20:10:24 +0300 Subject: [PATCH 02/18] test: Add tests for `lang:sync` command --- .../Translation/LocalizationSyncTest.php | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 tests/system/Commands/Translation/LocalizationSyncTest.php diff --git a/tests/system/Commands/Translation/LocalizationSyncTest.php b/tests/system/Commands/Translation/LocalizationSyncTest.php new file mode 100644 index 000000000000..3a682cbea4ef --- /dev/null +++ b/tests/system/Commands/Translation/LocalizationSyncTest.php @@ -0,0 +1,214 @@ + + * + * 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\Test\CIUnitTestCase; +use CodeIgniter\Test\StreamFilterTrait; +use Config\App; +use Locale; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class LocalizationSyncTest extends CIUnitTestCase +{ + use StreamFilterTrait; + + private static string $locale; + private static string $languageTestPath; + + /** + * @var array|string|null> + */ + private array $expectedKeys = [ + 'a' => 'Sync.a', + 'b' => 'Sync.b', + 'c' => 'Sync.c', + 'd' => [], + 'e' => 'Sync.e', + 'f' => [ + 'g' => 'Sync.f.g', + 'h' => [ + 'i' => 'Sync.f.h.i', + ], + ], + ]; + + protected function setUp(): void + { + parent::setUp(); + + config(App::class)->supportedLocales = ['en', 'ru', 'test']; + + self::$locale = Locale::getDefault(); + self::$languageTestPath = SUPPORTPATH . 'Language' . DIRECTORY_SEPARATOR; + $this->makeLanguageFiles(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->clearGeneratedFiles(); + } + + public function testSyncDefaultLocale(): void + { + command('lang:sync --target test'); + + $langFile = self::$languageTestPath . 'test/Sync.php'; + + $this->assertFileExists($langFile); + + $langKeys = include $langFile; + + $this->assertIsArray($langKeys); + $this->assertSame($this->expectedKeys, $langKeys); + } + + public function testSyncWithLocaleOption(): void + { + command('lang:sync --locale ru --target test'); + + $langFile = self::$languageTestPath . 'test/Sync.php'; + + $this->assertFileExists($langFile); + + $langKeys = include $langFile; + + $this->assertIsArray($langKeys); + $this->assertSame($this->expectedKeys, $langKeys); + } + + public function testSyncWithExistTranslation(): void + { + // First run, add new keys + command('lang:sync --target test'); + + $langFile = self::$languageTestPath . 'test/Sync.php'; + + $this->assertFileExists($langFile); + + $langKeys = include $langFile; + + $this->assertIsArray($langKeys); + $this->assertSame($this->expectedKeys, $langKeys); + + // Second run, save old keys + $oldLangKeys = [ + 'a' => 'old value 1', + 'b' => 2000, + 'c' => null, + 'd' => [], + 'e' => '', + 'f' => [ + 'g' => 'old value 2', + 'h' => [ + 'i' => 'old value 3', + ], + ], + ]; + + $lang = <<<'TEXT_WRAP' + 'old value 1', + 'b' => 2000, + 'c' => null, + 'd' => [], + 'e' => '', + 'f' => [ + 'g' => 'old value 2', + 'h' => [ + 'i' => 'old value 3', + ], + ], + ]; + TEXT_WRAP; + + file_put_contents(self::$languageTestPath . 'test/Sync.php', $lang); + + command('lang:sync --target test'); + + $langFile = self::$languageTestPath . 'test/Sync.php'; + + $this->assertFileExists($langFile); + + $langKeys = include $langFile; + $this->assertIsArray($langKeys); + $this->assertSame($oldLangKeys, $langKeys); + } + + public function testSyncWithIncorrectLocaleOption(): void + { + command('lang:sync --locale test_locale_incorrect --target test'); + + $this->assertStringContainsString('is not supported', $this->getStreamFilterBuffer()); + } + + public function testSyncWithIncorrectTargetOption(): void + { + command('lang:sync --locale en --target test_locale_incorrect'); + + $this->assertStringContainsString('is not supported', $this->getStreamFilterBuffer()); + } + + private function makeLanguageFiles(): void + { + $lang = <<<'TEXT_WRAP' + 'value 1', + 'b' => 2, + 'c' => null, + 'd' => [], + 'e' => '', + 'f' => [ + 'g' => 'value 2', + 'h' => [ + 'i' => 'value 3', + ], + ], + ]; + TEXT_WRAP; + + file_put_contents(self::$languageTestPath . self::$locale . '/Sync.php', $lang); + file_put_contents(self::$languageTestPath . 'ru/Sync.php', $lang); + } + + private function clearGeneratedFiles(): void + { + if (is_file(self::$languageTestPath . self::$locale . '/Sync.php')) { + unlink(self::$languageTestPath . self::$locale . '/Sync.php'); + } + + if (is_file(self::$languageTestPath . 'ru/Sync.php')) { + unlink(self::$languageTestPath . 'ru/Sync.php'); + } + + if (is_dir(self::$languageTestPath . 'test')) { + $files = glob(self::$languageTestPath . 'test/*', GLOB_MARK); + + foreach ($files as $file) { + unlink($file); + } + + rmdir(self::$languageTestPath . 'test'); + } + } +} From 49d7a8730d50a3574e9cfc3f44ada85bd183bf83 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 5 Jul 2024 20:11:33 +0300 Subject: [PATCH 03/18] feat: Add array helper `intersectKeyRecursive` --- system/Helpers/Array/ArrayHelper.php | 23 +++ .../ArrayHelperIntersectKeyRecursiveTest.php | 188 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index 300b0dcde40a..71bf53201662 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -315,4 +315,27 @@ public static function sortValuesByNatural(array &$array, $sortByIndex = null): return strnatcmp((string) $currentValue, (string) $nextValue); }); } + + /** + * Returns a new array from $target with the keys that are in $original + * + * @param array|string|null> $target + * @param array|string|null> $original + * + * @return array|string|null> + */ + public static function intersectKeyRecursive(array $target, array $original): array + { + $result = []; + + foreach ($target as $key => $value) { + if (is_array($value) && isset($original[$key]) && is_array($original[$key])) { + $result[$key] = self::intersectKeyRecursive($value, $original[$key]); + } elseif (array_key_exists($key, $original)) { + $result[$key] = $value; + } + } + + return $result; + } } diff --git a/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php b/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php new file mode 100644 index 000000000000..a252eb9b0c2f --- /dev/null +++ b/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Helpers\Array; + +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class ArrayHelperIntersectKeyRecursiveTest extends CIUnitTestCase +{ + /** + * @var array|string|null> + */ + private array $targetArray; + + protected function setUp(): void + { + parent::setUp(); + + $this->targetArray = [ + 'a' => [ + 'b' => [ + 'c' => [ + 'd' => 'value1', + ], + ], + 'e' => 'value2', + 'f' => [ + 'g' => 'value3', + 'h' => 'value4', + 'i' => [ + 'j' => 'value5', + ], + ], + 'k' => null, + 'l' => [], + 'm' => '', + ], + ]; + } + + public function testShuffleCopy(): void + { + $original = [ + 'a' => [ + 'l' => [], + 'k' => null, + 'e' => 'value2', + 'f' => [ + 'i' => [ + 'j' => 'value5', + ], + 'h' => 'value4', + 'g' => 'value3', + ], + 'm' => '', + 'b' => [ + 'c' => [ + 'd' => 'value1', + ], + ], + ], + ]; + + // var_dump(ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); + + $this->assertSame($original, ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); + } + + public function testCopyWithAnotherValues(): void + { + $original = [ + 'a' => [ + 'b' => [ + 'c' => [ + 'd' => 'value1_1', + ], + ], + 'e' => 'value2_2', + 'f' => [ + 'g' => 'value3_3', + 'h' => 'value4_4', + 'i' => [ + 'j' => 'value5_5', + ], + ], + 'k' => [], + 'l' => null, + 'm' => 'value6_6', + ], + ]; + + $this->assertSame($original, ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); + } + + public function testEmptyCompare(): void + { + $this->assertSame([], ArrayHelper::intersectKeyRecursive($this->targetArray, [])); + } + + public function testEmptyOriginal(): void + { + $this->assertSame([], ArrayHelper::intersectKeyRecursive([], $this->targetArray)); + } + + public function testCompletelyDifferent(): void + { + $original = [ + 'new_a' => [ + 'new_b' => [ + 'new_c' => [ + 'new_d' => 'value1_1', + ], + ], + 'new_e' => 'value2_2', + 'new_f' => [ + 'new_g' => 'value3_3', + 'new_h' => 'value4_4', + 'new_i' => [ + 'new_j' => 'value5_5', + ], + ], + 'new_k' => [], + 'new_l' => null, + 'new_m' => '', + ], + ]; + + $this->assertSame([], ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); + } + + public function testRecursiveDiffPartlyDifferent(): void + { + $original = [ + 'a' => [ + 'b' => [ + 'new_c' => [ + 'd' => 'value1', + ], + ], + 'e' => 'value2', + 'f' => [ + 'g' => 'value3', + 'new_h' => 'value4', + 'i' => [ + 'new_j' => 'value5', + ], + ], + 'k' => null, + 'new_l' => [], + 'm' => [ + 'new_n' => '', + ], + ], + ]; + + $intersect = [ + 'a' => [ + 'b' => [], + 'e' => 'value2', + 'f' => [ + 'g' => 'value3', + 'i' => [], + ], + 'k' => null, + 'm' => [ + 'new_n' => '', + ], + ], + ]; + + $this->assertSame($intersect, ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); + } +} From 4ffb69c802d4b0ae90a51e61375e3ed0e861084e Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 5 Jul 2024 20:11:58 +0300 Subject: [PATCH 04/18] docs: Add changelog --- user_guide_src/source/changelogs/v4.6.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index d6429bf236e8..c31b628ccf55 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -234,6 +234,7 @@ Commands - The ``spark routes`` and ``spark filter:check`` commands now display filter arguments. - The ``spark filter:check`` command now displays filter classnames. +- The ``spark lang:sync`` command to synchronize translation files. See :ref:`sync-translations-command` Routing ======= From 96d640d9d53389d5b28309bd326ca72a8c5597f9 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 5 Jul 2024 20:12:32 +0300 Subject: [PATCH 05/18] docs: Add description for command --- .../source/outgoing/localization.rst | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index c5fba00adbc5..41542af4ad09 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -366,3 +366,29 @@ Detailed information can be found by running the command: .. code-block:: console php spark lang:find --help + +.. _sync-translations-command: + +Synchronization Translation Files via Command +======================================== + +.. versionadded:: 4.6.0 + +After working on your translation for the current language for a long time, in some cases you will need to create files for another language. +The ``spark lang:find`` command can be used, it is not prohibited. But it cannot fully detect absolutely all translations. Especially if the parameters are dynamically set as ``lang('App.status.' . $key, ['payload' => 'John'], 'en')``. +In this case, the best solution is to copy the finished language files and translate them. This way you can save your unique keys that the command did not find. + +All you need to do is execute: + +.. code-block:: console + + // Specify the locale for new/updated translations + php spark lang:sync --target ru + + // or set the original locale + php spark lang:sync --locale en --target ru + +As a result, you will receive files with the transfer keys. +If there were duplicate keys in the target locale, they are saved. + +.. warning:: Non-matching keys in new translations are deleted! From 7867ecb77f460ee5cc3597da212e22d8bae595db Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 5 Jul 2024 20:29:07 +0300 Subject: [PATCH 06/18] fix: Improve condition --- system/Commands/Translation/LocalizationSync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index b2bff0c1d473..f534f2f0889d 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -150,7 +150,7 @@ private function process(string $originalLocale, string $targetLocale): void private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLanguageKeys, string $prefix = ''): array { foreach ($originalLanguageKeys as $key => $value) { - $placeholderValue = $prefix === '' ? $prefix . '.' . $key : $key; + $placeholderValue = $prefix !== '' ? $prefix . '.' . $key : $key; if (! is_array($value)) { // Keep the old value From 042dbeabb168f54419a2ce3f7c6bfce018b2af66 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 6 Jul 2024 12:52:27 +0300 Subject: [PATCH 07/18] fix: docs title underline too short --- user_guide_src/source/outgoing/localization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 41542af4ad09..8ab24c5a0cda 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -370,7 +370,7 @@ Detailed information can be found by running the command: .. _sync-translations-command: Synchronization Translation Files via Command -======================================== +============================================== .. versionadded:: 4.6.0 From b57def9d14229283361c4190076c2d24e3d74b67 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Mon, 5 Aug 2024 08:34:21 +0300 Subject: [PATCH 08/18] fix: typo --- user_guide_src/source/outgoing/localization.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 8ab24c5a0cda..f1ae44282bec 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -369,8 +369,8 @@ Detailed information can be found by running the command: .. _sync-translations-command: -Synchronization Translation Files via Command -============================================== +Synchronization Translation Files via Command +============================================= .. versionadded:: 4.6.0 @@ -388,7 +388,7 @@ All you need to do is execute: // or set the original locale php spark lang:sync --locale en --target ru -As a result, you will receive files with the transfer keys. +As a result, you will receive files with the translation keys. If there were duplicate keys in the target locale, they are saved. .. warning:: Non-matching keys in new translations are deleted! From 5448d7a9154182fa7309be23ee9014a460bab843 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Mon, 5 Aug 2024 08:34:46 +0300 Subject: [PATCH 09/18] fix: Delete debug info --- .../Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php b/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php index a252eb9b0c2f..c5c2adfc5e21 100644 --- a/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php +++ b/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php @@ -76,8 +76,6 @@ public function testShuffleCopy(): void ], ]; - // var_dump(ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); - $this->assertSame($original, ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); } From 8933d3fdefba265aa8e3b359e1e236b4968ff492 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Mon, 5 Aug 2024 08:35:26 +0300 Subject: [PATCH 10/18] fix: Remove sorting lang keys --- system/Commands/Translation/LocalizationSync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index f534f2f0889d..38dc8c424bf6 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -134,7 +134,7 @@ private function process(string $originalLocale, string $targetLocale): void } $targetLanguageKeys = $this->mergeLanguageKeys($originalLanguageKeys, $targetLanguageKeys, $originalLanguageFile->getBasename('.php')); - ksort($targetLanguageKeys); + // ksort($targetLanguageKeys); $content = " Date: Thu, 8 Aug 2024 17:49:16 +0300 Subject: [PATCH 11/18] fix: key sorting --- system/Commands/Translation/LocalizationSync.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index 38dc8c424bf6..d8342fa64bc8 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -15,7 +15,6 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; -use CodeIgniter\Helpers\Array\ArrayHelper; use Config\App; use Locale; use RecursiveDirectoryIterator; @@ -134,7 +133,6 @@ private function process(string $originalLocale, string $targetLocale): void } $targetLanguageKeys = $this->mergeLanguageKeys($originalLanguageKeys, $targetLanguageKeys, $originalLanguageFile->getBasename('.php')); - // ksort($targetLanguageKeys); $content = " $value) { $placeholderValue = $prefix !== '' ? $prefix . '.' . $key : $key; @@ -156,21 +156,25 @@ private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLan // 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 - $targetLanguageKeys[$key] = $placeholderValue; + $mergedLanguageKeys[$key] = $placeholderValue; } else { if (! array_key_exists($key, $targetLanguageKeys)) { - $targetLanguageKeys[$key] = []; + $mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, [], $placeholderValue); + + continue; } - $targetLanguageKeys[$key] = $this->mergeLanguageKeys($value, $targetLanguageKeys[$key], $placeholderValue); + $mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, $targetLanguageKeys[$key], $placeholderValue); } } - return ArrayHelper::intersectKeyRecursive($targetLanguageKeys, $originalLanguageKeys); + return $mergedLanguageKeys; } private function isIgnoredFile(SplFileInfo $file): bool From d23c879cc22dcb364ae61429ff040ec8f0856c13 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Thu, 8 Aug 2024 17:51:49 +0300 Subject: [PATCH 12/18] Remove array helper `intersectKeyRecursive` --- system/Helpers/Array/ArrayHelper.php | 23 --- .../ArrayHelperIntersectKeyRecursiveTest.php | 186 ------------------ 2 files changed, 209 deletions(-) delete mode 100644 tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index 71bf53201662..300b0dcde40a 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -315,27 +315,4 @@ public static function sortValuesByNatural(array &$array, $sortByIndex = null): return strnatcmp((string) $currentValue, (string) $nextValue); }); } - - /** - * Returns a new array from $target with the keys that are in $original - * - * @param array|string|null> $target - * @param array|string|null> $original - * - * @return array|string|null> - */ - public static function intersectKeyRecursive(array $target, array $original): array - { - $result = []; - - foreach ($target as $key => $value) { - if (is_array($value) && isset($original[$key]) && is_array($original[$key])) { - $result[$key] = self::intersectKeyRecursive($value, $original[$key]); - } elseif (array_key_exists($key, $original)) { - $result[$key] = $value; - } - } - - return $result; - } } diff --git a/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php b/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php deleted file mode 100644 index c5c2adfc5e21..000000000000 --- a/tests/system/Helpers/Array/ArrayHelperIntersectKeyRecursiveTest.php +++ /dev/null @@ -1,186 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Helpers\Array; - -use CodeIgniter\Test\CIUnitTestCase; -use PHPUnit\Framework\Attributes\Group; - -/** - * @internal - */ -#[Group('Others')] -final class ArrayHelperIntersectKeyRecursiveTest extends CIUnitTestCase -{ - /** - * @var array|string|null> - */ - private array $targetArray; - - protected function setUp(): void - { - parent::setUp(); - - $this->targetArray = [ - 'a' => [ - 'b' => [ - 'c' => [ - 'd' => 'value1', - ], - ], - 'e' => 'value2', - 'f' => [ - 'g' => 'value3', - 'h' => 'value4', - 'i' => [ - 'j' => 'value5', - ], - ], - 'k' => null, - 'l' => [], - 'm' => '', - ], - ]; - } - - public function testShuffleCopy(): void - { - $original = [ - 'a' => [ - 'l' => [], - 'k' => null, - 'e' => 'value2', - 'f' => [ - 'i' => [ - 'j' => 'value5', - ], - 'h' => 'value4', - 'g' => 'value3', - ], - 'm' => '', - 'b' => [ - 'c' => [ - 'd' => 'value1', - ], - ], - ], - ]; - - $this->assertSame($original, ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); - } - - public function testCopyWithAnotherValues(): void - { - $original = [ - 'a' => [ - 'b' => [ - 'c' => [ - 'd' => 'value1_1', - ], - ], - 'e' => 'value2_2', - 'f' => [ - 'g' => 'value3_3', - 'h' => 'value4_4', - 'i' => [ - 'j' => 'value5_5', - ], - ], - 'k' => [], - 'l' => null, - 'm' => 'value6_6', - ], - ]; - - $this->assertSame($original, ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); - } - - public function testEmptyCompare(): void - { - $this->assertSame([], ArrayHelper::intersectKeyRecursive($this->targetArray, [])); - } - - public function testEmptyOriginal(): void - { - $this->assertSame([], ArrayHelper::intersectKeyRecursive([], $this->targetArray)); - } - - public function testCompletelyDifferent(): void - { - $original = [ - 'new_a' => [ - 'new_b' => [ - 'new_c' => [ - 'new_d' => 'value1_1', - ], - ], - 'new_e' => 'value2_2', - 'new_f' => [ - 'new_g' => 'value3_3', - 'new_h' => 'value4_4', - 'new_i' => [ - 'new_j' => 'value5_5', - ], - ], - 'new_k' => [], - 'new_l' => null, - 'new_m' => '', - ], - ]; - - $this->assertSame([], ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); - } - - public function testRecursiveDiffPartlyDifferent(): void - { - $original = [ - 'a' => [ - 'b' => [ - 'new_c' => [ - 'd' => 'value1', - ], - ], - 'e' => 'value2', - 'f' => [ - 'g' => 'value3', - 'new_h' => 'value4', - 'i' => [ - 'new_j' => 'value5', - ], - ], - 'k' => null, - 'new_l' => [], - 'm' => [ - 'new_n' => '', - ], - ], - ]; - - $intersect = [ - 'a' => [ - 'b' => [], - 'e' => 'value2', - 'f' => [ - 'g' => 'value3', - 'i' => [], - ], - 'k' => null, - 'm' => [ - 'new_n' => '', - ], - ], - ]; - - $this->assertSame($intersect, ArrayHelper::intersectKeyRecursive($original, $this->targetArray)); - } -} From 5b90b6f89c766e94eec52c61c3519efc057400a7 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Thu, 26 Dec 2024 22:09:35 +0300 Subject: [PATCH 13/18] refactor: Check type translated value --- system/Commands/Translation/LocalizationSync.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index d8342fa64bc8..379b7dc9c619 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -15,6 +15,7 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; +use CodeIgniter\Exceptions\LogicException; use Config\App; use Locale; use RecursiveDirectoryIterator; @@ -152,7 +153,7 @@ private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLan foreach ($originalLanguageKeys as $key => $value) { $placeholderValue = $prefix !== '' ? $prefix . '.' . $key : $key; - if (! is_array($value)) { + if (is_string($value)) { // Keep the old value // TODO: The value type may not match the original one if (array_key_exists($key, $targetLanguageKeys)) { @@ -163,7 +164,7 @@ private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLan // Set new key with placeholder $mergedLanguageKeys[$key] = $placeholderValue; - } else { + } elseif (is_array($value)) { if (! array_key_exists($key, $targetLanguageKeys)) { $mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, [], $placeholderValue); @@ -171,6 +172,8 @@ private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLan } $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.'); } } From 9c920411593a26f680ec49647661ff3c47855531 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Thu, 26 Dec 2024 22:11:51 +0300 Subject: [PATCH 14/18] fix: Update tests to be readable --- .../Translation/LocalizationSyncTest.php | 185 +++++++++++------- 1 file changed, 112 insertions(+), 73 deletions(-) diff --git a/tests/system/Commands/Translation/LocalizationSyncTest.php b/tests/system/Commands/Translation/LocalizationSyncTest.php index 3a682cbea4ef..6da56e775d68 100644 --- a/tests/system/Commands/Translation/LocalizationSyncTest.php +++ b/tests/system/Commands/Translation/LocalizationSyncTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Translation; +use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use Config\App; @@ -34,15 +35,17 @@ final class LocalizationSyncTest extends CIUnitTestCase * @var array|string|null> */ private array $expectedKeys = [ - 'a' => 'Sync.a', - 'b' => 'Sync.b', - 'c' => 'Sync.c', - 'd' => [], - 'e' => 'Sync.e', - 'f' => [ - 'g' => 'Sync.f.g', - 'h' => [ - 'i' => 'Sync.f.h.i', + 'title' => 'Sync.title', + 'status' => [ + 'error' => 'Sync.status.error', + 'done' => 'Sync.status.done', + 'critical' => 'Sync.status.critical', + ], + 'description' => 'Sync.description', + 'empty_array' => [], + 'more' => [ + 'nested' => [ + 'key' => 'Sync.more.nested.key', ], ], ]; @@ -51,10 +54,10 @@ protected function setUp(): void { parent::setUp(); - config(App::class)->supportedLocales = ['en', 'ru', 'test']; + config(App::class)->supportedLocales = ['en', 'ru', 'de']; self::$locale = Locale::getDefault(); - self::$languageTestPath = SUPPORTPATH . 'Language' . DIRECTORY_SEPARATOR; + self::$languageTestPath = SUPPORTPATH . 'Language/'; $this->makeLanguageFiles(); } @@ -67,9 +70,9 @@ protected function tearDown(): void public function testSyncDefaultLocale(): void { - command('lang:sync --target test'); + command('lang:sync --target de'); - $langFile = self::$languageTestPath . 'test/Sync.php'; + $langFile = self::$languageTestPath . 'de/Sync.php'; $this->assertFileExists($langFile); @@ -81,9 +84,9 @@ public function testSyncDefaultLocale(): void public function testSyncWithLocaleOption(): void { - command('lang:sync --locale ru --target test'); + command('lang:sync --locale ru --target de'); - $langFile = self::$languageTestPath . 'test/Sync.php'; + $langFile = self::$languageTestPath . 'de/Sync.php'; $this->assertFileExists($langFile); @@ -95,29 +98,21 @@ public function testSyncWithLocaleOption(): void public function testSyncWithExistTranslation(): void { - // First run, add new keys - command('lang:sync --target test'); - - $langFile = self::$languageTestPath . 'test/Sync.php'; - - $this->assertFileExists($langFile); - - $langKeys = include $langFile; - - $this->assertIsArray($langKeys); - $this->assertSame($this->expectedKeys, $langKeys); - - // Second run, save old keys - $oldLangKeys = [ - 'a' => 'old value 1', - 'b' => 2000, - 'c' => null, - 'd' => [], - 'e' => '', - 'f' => [ - 'g' => 'old value 2', - 'h' => [ - 'i' => 'old value 3', + // Save old values and add new keys from "en/Sync.php" + // Add value from the old file "de/Sync.php" to new + // Right sort as in "en/Sync.php" + $expectedLangKeys = [ + 'title' => 'Default title (old)', + 'status' => [ + 'error' => 'Error! (old)', + 'done' => 'Sync.status.done', + 'critical' => 'Critical! (old)', + ], + 'description' => 'Sync.description', + 'empty_array' => [], + 'more' => [ + 'nested' => [ + 'key' => 'More nested key... (old)', ], ], ]; @@ -126,40 +121,83 @@ public function testSyncWithExistTranslation(): void 'old value 1', - 'b' => 2000, - 'c' => null, - 'd' => [], - 'e' => '', - 'f' => [ - 'g' => 'old value 2', - 'h' => [ - 'i' => 'old value 3', + 'status' => [ + 'critical' => 'Critical! (old)', + 'error' => 'Error! (old)', + ], + 'skip' => 'skip this value', + 'title' => 'Default title (old)', + 'more' => [ + 'nested' => [ + 'key' => 'More nested key... (old)', ], ], + 'empty_array' => [], ]; TEXT_WRAP; - file_put_contents(self::$languageTestPath . 'test/Sync.php', $lang); + $langFile = self::$languageTestPath . 'de/Sync.php'; - command('lang:sync --target test'); + mkdir(self::$languageTestPath . 'de', 0755); + file_put_contents($langFile, $lang); - $langFile = self::$languageTestPath . 'test/Sync.php'; + command('lang:sync --target de'); $this->assertFileExists($langFile); $langKeys = include $langFile; + $this->assertIsArray($langKeys); - $this->assertSame($oldLangKeys, $langKeys); + $this->assertSame($expectedLangKeys, $langKeys); } public function testSyncWithIncorrectLocaleOption(): void { - command('lang:sync --locale test_locale_incorrect --target test'); + command('lang:sync --locale test_locale_incorrect --target de'); $this->assertStringContainsString('is not supported', $this->getStreamFilterBuffer()); } + public function testSyncWithNullableOriginalLangValue(): void + { + $langWithNullValue = <<<'TEXT_WRAP' + null, + ]; + TEXT_WRAP; + + file_put_contents(self::$languageTestPath . self::$locale . '/SyncInvalid.php', $langWithNullValue); + ob_get_flush(); + + $this->expectException(LogicException::class); + $this->expectExceptionMessageMatches('/Only "array" or "string" is allowed/'); + + command('lang:sync --target de'); + } + + public function testSyncWithIntegerOriginalLangValue(): void + { + $this->resetStreamFilterBuffer(); + + $langWithIntegerValue = <<<'TEXT_WRAP' + 1000, + ]; + TEXT_WRAP; + + file_put_contents(self::$languageTestPath . self::$locale . '/SyncInvalid.php', $langWithIntegerValue); + ob_get_flush(); + + $this->expectException(LogicException::class); + $this->expectExceptionMessageMatches('/Only "array" or "string" is allowed/'); + + command('lang:sync --target de'); + } + public function testSyncWithIncorrectTargetOption(): void { command('lang:sync --locale en --target test_locale_incorrect'); @@ -173,15 +211,17 @@ private function makeLanguageFiles(): void 'value 1', - 'b' => 2, - 'c' => null, - 'd' => [], - 'e' => '', - 'f' => [ - 'g' => 'value 2', - 'h' => [ - 'i' => 'value 3', + 'title' => 'Default title', + 'status' => [ + 'error' => 'Error!', + 'done' => 'Done!', + 'critical' => 'Critical!', + ], + 'description' => '', + 'empty_array' => [], + 'more' => [ + 'nested' => [ + 'key' => 'More nested key...', ], ], ]; @@ -193,22 +233,21 @@ private function makeLanguageFiles(): void private function clearGeneratedFiles(): void { - if (is_file(self::$languageTestPath . self::$locale . '/Sync.php')) { - unlink(self::$languageTestPath . self::$locale . '/Sync.php'); - } - - if (is_file(self::$languageTestPath . 'ru/Sync.php')) { - unlink(self::$languageTestPath . 'ru/Sync.php'); - } - - if (is_dir(self::$languageTestPath . 'test')) { - $files = glob(self::$languageTestPath . 'test/*', GLOB_MARK); + $files = [ + self::$languageTestPath . self::$locale . '/Sync.php', + self::$languageTestPath . self::$locale . '/SyncInvalid.php', + self::$languageTestPath . 'ru/Sync.php', + ]; - foreach ($files as $file) { + foreach ($files as $file) { + if (is_file($file)) { unlink($file); } + } - rmdir(self::$languageTestPath . 'test'); + if (is_dir(self::$languageTestPath . 'de')) { + delete_files(self::$languageTestPath . 'de'); + rmdir(self::$languageTestPath . 'de'); } } } From 76cd73ff1437be9c2bf07ef811c1882ae869aa80 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 4 Jan 2025 23:26:18 +0300 Subject: [PATCH 15/18] fix: Update changelog --- user_guide_src/source/outgoing/localization.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index f1ae44282bec..dfbc6d3130ac 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -370,13 +370,13 @@ Detailed information can be found by running the command: .. _sync-translations-command: Synchronization Translation Files via Command -============================================= +--------------------------------------------- .. versionadded:: 4.6.0 -After working on your translation for the current language for a long time, in some cases you will need to create files for another language. -The ``spark lang:find`` command can be used, it is not prohibited. But it cannot fully detect absolutely all translations. Especially if the parameters are dynamically set as ``lang('App.status.' . $key, ['payload' => 'John'], 'en')``. -In this case, the best solution is to copy the finished language files and translate them. This way you can save your unique keys that the command did not find. +You may need to create files for another language when you've finished translating for the current language. You can use the spark ``lang:find`` command to help with this. However, it might not detect all translations, particularly those with dynamically set parameters like ``lang('App.status.' . $key, ['payload' => 'John'], 'en')``. + +To ensure no translations are missed, it's best to copy the completed language files and translate them manually. This approach preserves any unique keys the command might have overlooked. All you need to do is execute: From cd84a6e02cfa0cf0f8d576b46d0da3089ead1c11 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 10 Jan 2025 08:35:16 +0300 Subject: [PATCH 16/18] fix: Typo --- system/Commands/Translation/LocalizationSync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index 379b7dc9c619..c65e155b6f2c 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -77,7 +77,7 @@ public function run(array $params) if ($optionTargetLocale === $optionLocale) { CLI::error( - 'Error: You cannot have the same values "--target" and "--locale".' + 'Error: You cannot have the same values for "--target" and "--locale".' ); return EXIT_USER_INPUT; From 47a6f8af1874fa5f15c2d604ec5a9fe84a6190c1 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Fri, 10 Jan 2025 10:17:38 +0300 Subject: [PATCH 17/18] refactor: Rework exit codes, skip dots --- .../Commands/Translation/LocalizationSync.php | 41 ++++++++++++------- .../Translation/LocalizationSyncTest.php | 22 ++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index c65e155b6f2c..0ba7a023b61c 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -17,6 +17,8 @@ use CodeIgniter\CLI\CLI; use CodeIgniter\Exceptions\LogicException; use Config\App; +use ErrorException; +use FilesystemIterator; use Locale; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -87,31 +89,45 @@ public function run(array $params) $this->languagePath = SUPPORTPATH . 'Language'; } - $this->process($optionLocale, $optionTargetLocale); - - CLI::write('All operations done!'); + if ($this->process($optionLocale, $optionTargetLocale) === EXIT_SUCCESS) { + CLI::write('All operations done!'); + } return EXIT_SUCCESS; } - private function process(string $originalLocale, string $targetLocale): void + 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 "' . $originalLocaleDir . '" directory was not found.' + 'Error: The "' . clean_path($originalLocaleDir) . '" directory was not found.' ); + + return EXIT_ERROR; } - if (! is_dir($targetLocaleDir) && ! mkdir($targetLocaleDir, 0775)) { + // 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 "' . $targetLocaleDir . '" cannot be accessed.' + 'Error: The target directory "' . clean_path($targetLocaleDir) . '" cannot be accessed.' ); + + return EXIT_ERROR; } - $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($originalLocaleDir)); + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $originalLocaleDir, + FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS + ) + ); /** * @var list $files @@ -120,7 +136,7 @@ private function process(string $originalLocale, string $targetLocale): void ksort($files); foreach ($files as $originalLanguageFile) { - if ($this->isIgnoredFile($originalLanguageFile)) { + if ($originalLanguageFile->getExtension() !== 'php') { continue; } @@ -138,6 +154,8 @@ private function process(string $originalLocale, string $targetLocale): void $content = "isDir() || $file->getFilename() === '.' || $file->getFilename() === '..' || $file->getExtension() !== 'php'; - } } diff --git a/tests/system/Commands/Translation/LocalizationSyncTest.php b/tests/system/Commands/Translation/LocalizationSyncTest.php index 6da56e775d68..88d7dc6f3bd6 100644 --- a/tests/system/Commands/Translation/LocalizationSyncTest.php +++ b/tests/system/Commands/Translation/LocalizationSyncTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Exceptions\LogicException; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\ReflectionHelper; use CodeIgniter\Test\StreamFilterTrait; use Config\App; use Locale; @@ -27,6 +28,7 @@ final class LocalizationSyncTest extends CIUnitTestCase { use StreamFilterTrait; + use ReflectionHelper; private static string $locale; private static string $languageTestPath; @@ -205,6 +207,26 @@ public function testSyncWithIncorrectTargetOption(): void $this->assertStringContainsString('is not supported', $this->getStreamFilterBuffer()); } + public function testProcessWithInvalidOption(): void + { + $langPath = SUPPORTPATH . 'Language'; + $command = new LocalizationSync(service('logger'), service('commands')); + $this->setPrivateProperty($command, 'languagePath', $langPath); + $runner = $this->getPrivateMethodInvoker($command, 'process'); + + $status = $runner('de', 'jp'); + + $this->assertSame(EXIT_ERROR, $status); + $this->assertStringContainsString('Error: The "ROOTPATH/tests/_support/Language/de" directory was not found.', $this->getStreamFilterBuffer()); + + chmod($langPath, 0544); + $status = $runner('en', 'jp'); + chmod($langPath, 0775); + + $this->assertSame(EXIT_ERROR, $status); + $this->assertStringContainsString('Error: The target directory "ROOTPATH/tests/_support/Language/jp" cannot be accessed.', $this->getStreamFilterBuffer()); + } + private function makeLanguageFiles(): void { $lang = <<<'TEXT_WRAP' From 1eae83473dc7b6071bd89b6a3d4d0dfd88bad422 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 11 Jan 2025 12:56:54 +0300 Subject: [PATCH 18/18] refactor: Accurate command finishing --- system/Commands/Translation/LocalizationSync.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index 0ba7a023b61c..b2614ab3ca2e 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -89,10 +89,12 @@ public function run(array $params) $this->languagePath = SUPPORTPATH . 'Language'; } - if ($this->process($optionLocale, $optionTargetLocale) === EXIT_SUCCESS) { - CLI::write('All operations done!'); + if ($this->process($optionLocale, $optionTargetLocale) === EXIT_ERROR) { + return EXIT_ERROR; } + CLI::write('All operations done!'); + return EXIT_SUCCESS; } @@ -130,7 +132,7 @@ private function process(string $originalLocale, string $targetLocale): int ); /** - * @var list $files + * @var array $files */ $files = iterator_to_array($iterator, true); ksort($files);