diff --git a/system/Commands/Translation/LocalizationFinder.php b/system/Commands/Translation/LocalizationFinder.php index e6557b74d1c4..ca7a1fb20230 100644 --- a/system/Commands/Translation/LocalizationFinder.php +++ b/system/Commands/Translation/LocalizationFinder.php @@ -133,13 +133,17 @@ private function process(string $currentDir, string $currentLocale): void $languageStoredKeys = require $languageFilePath; } - $languageDiff = ArrayHelper::recursiveDiff($foundLanguageKeys[$langFileName], $languageStoredKeys); + // Keys already resolvable from any namespace's language file (framework, packages, or app) + // are not new and must not be re-reported or written. + $resolvedKeys = $this->findResolvedTranslations($langFileName, $currentLocale); + + $languageDiff = ArrayHelper::recursiveDiff($foundLanguageKeys[$langFileName], $resolvedKeys); $countNewKeys += ArrayHelper::recursiveCount($languageDiff); if ($this->showNew) { $tableRows = array_merge($this->arrayToTableRows($langFileName, $languageDiff), $tableRows); } else { - $newLanguageKeys = array_replace_recursive($foundLanguageKeys[$langFileName], $languageStoredKeys); + $newLanguageKeys = array_replace_recursive($languageDiff, $languageStoredKeys); if ($languageDiff !== []) { if (file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys)) === false) { @@ -177,6 +181,35 @@ private function process(string $currentDir, string $currentLocale): void } } + /** + * Loads the translations already resolvable for the given file and locale + * from every registered namespace (framework, packages, and app). + * + * @return array + */ + private function findResolvedTranslations(string $langFileName, string $currentLocale): array + { + $translations = []; + + foreach (service('locator')->search("Language/{$currentLocale}/{$langFileName}.php", 'php', false) as $file) { + if (! is_file($file)) { + continue; + } + + $keys = require $file; + + if (is_array($keys)) { + $translations[] = $keys; + } + } + + if ($translations === []) { + return []; + } + + return array_replace_recursive(...$translations); + } + /** * @param SplFileInfo|string $file * diff --git a/tests/system/Commands/Translation/LocalizationFinderTest.php b/tests/system/Commands/Translation/LocalizationFinderTest.php index 3baa82290f1f..bfbd00d8bf83 100644 --- a/tests/system/Commands/Translation/LocalizationFinderTest.php +++ b/tests/system/Commands/Translation/LocalizationFinderTest.php @@ -30,6 +30,7 @@ final class LocalizationFinderTest extends CIUnitTestCase private static string $locale; private static string $languageTestPath; private string $originalLocale; + private ?string $tempSourceDir = null; /** * @var list @@ -57,6 +58,7 @@ protected function tearDown(): void parent::tearDown(); $this->clearGeneratedFiles(); + $this->clearSourceFixture(); Locale::setDefault($this->originalLocale); config(App::class)->supportedLocales = $this->originalSupportedLocales; } @@ -131,6 +133,39 @@ public function testShowBadTranslation(): void $this->assertStringContainsString($this->getActualTableWithBadKeys(), $this->getStreamFilterBuffer()); } + public function testShowNewSkipsKeysAlreadyTranslatedByFramework(): void + { + $this->makeLocaleDirectory(); + $this->makeSourceFixture( + 'ResolvedKeys', + "getStreamFilterBuffer(); + $this->assertStringNotContainsString('Errors.pageNotFound', $buffer); + $this->assertStringContainsString('Errors.thisKeyIsBrandNew', $buffer); + } + + public function testWriteSkipsKeysAlreadyTranslatedByFramework(): void + { + $this->makeLocaleDirectory(); + $this->makeSourceFixture( + 'ResolvedKeys', + "assertFileExists($generatedFile); + + $generatedKeys = require $generatedFile; + $this->assertArrayHasKey('thisKeyIsBrandNew', $generatedKeys); + $this->assertArrayNotHasKey('pageNotFound', $generatedKeys); + } + private function getActualTranslationOneKeys(): array { return [ @@ -261,6 +296,32 @@ private function makeLocaleDirectory(): void @mkdir(self::$languageTestPath . self::$locale, 0777, true); } + private function makeSourceFixture(string $dirName, string $contents): void + { + $this->tempSourceDir = SUPPORTPATH . 'Services' . DIRECTORY_SEPARATOR . $dirName; + @mkdir($this->tempSourceDir, 0777, true); + file_put_contents($this->tempSourceDir . DIRECTORY_SEPARATOR . 'Source.php', $contents); + } + + private function clearSourceFixture(): void + { + if ($this->tempSourceDir === null) { + return; + } + + $sourceFile = $this->tempSourceDir . DIRECTORY_SEPARATOR . 'Source.php'; + + if (is_file($sourceFile)) { + unlink($sourceFile); + } + + if (is_dir($this->tempSourceDir)) { + rmdir($this->tempSourceDir); + } + + $this->tempSourceDir = null; + } + private function clearGeneratedFiles(): void { if (is_file(self::$languageTestPath . self::$locale . '/TranslationOne.php')) { @@ -275,6 +336,10 @@ private function clearGeneratedFiles(): void unlink(self::$languageTestPath . self::$locale . '/Translation-Four.php'); } + if (is_file(self::$languageTestPath . self::$locale . '/Errors.php')) { + unlink(self::$languageTestPath . self::$locale . '/Errors.php'); + } + if (is_dir(self::$languageTestPath . '/test_locale_incorrect')) { rmdir(self::$languageTestPath . '/test_locale_incorrect'); } diff --git a/user_guide_src/source/changelogs/v4.7.4.rst b/user_guide_src/source/changelogs/v4.7.4.rst index f9cc995ad22a..2e07bffffedf 100644 --- a/user_guide_src/source/changelogs/v4.7.4.rst +++ b/user_guide_src/source/changelogs/v4.7.4.rst @@ -31,6 +31,7 @@ Bugs Fixed ********** - **API:** Fixed a bug in Transformers where the root request's ``fields`` and ``include`` query parameters leaked into nested transformers created inside ``include*()`` methods, causing incorrect field filtering, unexpected includes, or infinite recursion. +- **Commands:** Fixed a bug where ``spark lang:find`` treated translation keys already provided by the framework or another namespace (such as ``Errors.*`` in ``system/Language``) as new, listing them under ``--show-new`` and writing untranslated placeholders into ``app/Language`` that overrode the existing translations. - **Database:** Fixed a bug where ``updateBatch()`` could be called after Query Builder ``where()`` conditions, even though it's not supported. In this situation, now the ``DatabaseException`` is thrown. - **HTTP:** Fixed a bug where the User Agent library reported Safari's WebKit version instead of the browser version from the ``Version`` token. - **Model:** Fixed a bug in ``Model::objectToRawArray()`` where the ``$recursive`` parameter was ignored. diff --git a/user_guide_src/source/outgoing/localization.rst b/user_guide_src/source/outgoing/localization.rst index 4d9facce4bfa..c73cf9449d00 100644 --- a/user_guide_src/source/outgoing/localization.rst +++ b/user_guide_src/source/outgoing/localization.rst @@ -317,6 +317,10 @@ After the operation, you need to translate the language keys yourself. The command is able to recognize nested keys normally ``File.array.nested.text``. Previously saved keys do not change. +.. note:: Keys that ``lang()`` can already resolve (for example ``Errors.*`` + from **system/Language**) are treated as already translated, so they are + neither listed by ``--show-new`` nor written into **app/Language**. + .. code-block:: console php spark lang:find