diff --git a/framework/core/src/Locale/Translator.php b/framework/core/src/Locale/Translator.php index dce685a4c9..8e4043715e 100644 --- a/framework/core/src/Locale/Translator.php +++ b/framework/core/src/Locale/Translator.php @@ -17,6 +17,17 @@ class Translator extends BaseTranslator implements TranslatorContract { const REFERENCE_REGEX = '/^=>\s*([a-z0-9_\-\.]+)$/i'; + /** + * Catalogue objects whose `=> reference` values have already been resolved. + * + * Tracked per object rather than per locale: Symfony stores an original + * catalogue per locale but attaches fresh copies as fallbacks of other + * locales, so several objects can share one locale name. + * + * @var \SplObjectStorage|null + */ + private $parsedCatalogues; + public function get($key, array $replace = [], $locale = null) { return $this->trans($key, $replace, null, $locale); @@ -39,16 +50,16 @@ public function getCatalogue($locale = null) $this->assertValidLocale($locale); } - $parse = ! isset($this->catalogues[$locale]); - $catalogue = parent::getCatalogue($locale); - if ($parse) { - $this->parseCatalogue($catalogue); + if ($this->parsedCatalogues === null) { + $this->parsedCatalogues = new \SplObjectStorage(); + } - $fallbackCatalogue = $catalogue; - while ($fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue()) { - $this->parseCatalogue($fallbackCatalogue); + for ($current = $catalogue; $current !== null; $current = $current->getFallbackCatalogue()) { + if (! $this->parsedCatalogues->contains($current)) { + $this->parseCatalogue($current); + $this->parsedCatalogues->attach($current); } } diff --git a/framework/core/tests/unit/Locale/TranslatorReferenceResolutionTest.php b/framework/core/tests/unit/Locale/TranslatorReferenceResolutionTest.php new file mode 100644 index 0000000000..1be1cc460a --- /dev/null +++ b/framework/core/tests/unit/Locale/TranslatorReferenceResolutionTest.php @@ -0,0 +1,84 @@ +cacheDir = sys_get_temp_dir().'/flarum-translator-test-'.uniqid('', true); + mkdir($this->cacheDir); + + file_put_contents($this->cacheDir.'/en.yml', "foo: '=> bar'\nbar: Resolved\n"); + file_put_contents($this->cacheDir.'/de.yml', "baz: Beispiel\n"); + } + + protected function tearDown(): void + { + array_map('unlink', glob($this->cacheDir.'/*') ?: []); + rmdir($this->cacheDir); + + parent::tearDown(); + } + + private function translator(): Translator + { + // Wired like LocaleServiceProvider: non-debug, cache dir, 'en' fallback. + $translator = new Translator('de', null, $this->cacheDir, false); + $translator->setFallbackLocales(['en']); + $translator->addLoader('prefixed_yaml', new PrefixedYamlFileLoader()); + $translator->addResource('prefixed_yaml', ['file' => $this->cacheDir.'/en.yml', 'prefix' => ''], 'en', self::DOMAIN); + $translator->addResource('prefixed_yaml', ['file' => $this->cacheDir.'/de.yml', 'prefix' => ''], 'de', self::DOMAIN); + + return $translator; + } + + public function test_references_are_resolved_when_locale_is_loaded_directly() + { + $translator = $this->translator(); + + $this->assertSame('Resolved', $translator->getCatalogue('en')->get('foo', 'messages')); + } + + public function test_references_are_resolved_when_locale_was_first_loaded_as_a_fallback() + { + $translator = $this->translator(); + + // Loading 'de' makes Symfony implicitly load and store the 'en' + // catalogue as its fallback, before 'en' is ever requested directly. + $translator->getCatalogue('de'); + + $this->assertSame('Resolved', $translator->getCatalogue('en')->get('foo', 'messages')); + } + + public function test_references_are_resolved_in_fallback_catalogues() + { + $translator = $this->translator(); + + $fallback = $translator->getCatalogue('de')->getFallbackCatalogue(); + + $this->assertSame('Resolved', $fallback->get('foo', 'messages')); + $this->assertSame('Resolved', $translator->trans('foo', [], null, 'de')); + } +}