From 90f82c09599b407ef2e59eabb0daa6b5fb4a07c2 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 19 Mar 2022 09:58:10 -0400 Subject: [PATCH] [Translation][FrameworkBundle] add `LocaleSwitcher` service --- .../Compiler/ConfigureLocaleSwitcherPass.php | 45 +++++++++ .../FrameworkExtension.php | 1 + .../FrameworkBundle/FrameworkBundle.php | 2 + .../Resources/config/translation.php | 19 ++++ .../ConfigureLocaleSwitcherPassTest.php | 62 +++++++++++++ .../FrameworkExtensionTest.php | 22 +++++ .../LocaleAwareRequestContextTest.php | 33 +++++++ .../Translation/LocaleAwareRequestContext.php | 35 +++++++ .../Component/Translation/LocaleSwitcher.php | 58 ++++++++++++ .../Translation/Tests/LocaleSwitcherTest.php | 92 +++++++++++++++++++ 10 files changed, 369 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ConfigureLocaleSwitcherPass.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ConfigureLocaleSwitcherPassTest.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Translation/LocaleAwareRequestContextTest.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Translation/LocaleAwareRequestContext.php create mode 100644 src/Symfony/Component/Translation/LocaleSwitcher.php create mode 100644 src/Symfony/Component/Translation/Tests/LocaleSwitcherTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ConfigureLocaleSwitcherPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ConfigureLocaleSwitcherPass.php new file mode 100644 index 0000000000000..78f633bfcb5d2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ConfigureLocaleSwitcherPass.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Kevin Bond + * + * @internal + */ +class ConfigureLocaleSwitcherPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->has('translation.locale_switcher')) { + return; + } + + $localeAwareServices = array_map( + fn (string $id) => new Reference($id), + array_keys($container->findTaggedServiceIds('kernel.locale_aware')) + ); + + if ($container->has('translation.locale_aware_request_context')) { + $localeAwareServices[] = new Reference('translation.locale_aware_request_context'); + } + + $container->getDefinition('translation.locale_switcher') + ->setArgument(1, new IteratorArgument($localeAwareServices)) + ; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 733341eeb2c7b..5f20557d5af73 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1056,6 +1056,7 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co $container->removeDefinition('console.command.router_debug'); $container->removeDefinition('console.command.router_match'); $container->removeDefinition('messenger.middleware.router_context'); + $container->removeDefinition('translation.locale_aware_request_context'); return; } diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 794686e3cdf5f..fe0bf8f5465a7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -15,6 +15,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddDebugLogProcessorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AssetsContextPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigureLocaleSwitcherPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass; @@ -158,6 +159,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new RegisterReverseContainerPass(true)); $container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new RemoveUnusedSessionMarshallingHandlerPass()); + $container->addCompilerPass(new ConfigureLocaleSwitcherPass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php index 7758078f277cf..79a7e9dce89e3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php @@ -13,6 +13,7 @@ use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\CacheWarmer\TranslationsCacheWarmer; +use Symfony\Bundle\FrameworkBundle\Translation\LocaleAwareRequestContext; use Symfony\Bundle\FrameworkBundle\Translation\Translator; use Symfony\Component\Translation\Dumper\CsvFileDumper; use Symfony\Component\Translation\Dumper\IcuResFileDumper; @@ -39,6 +40,7 @@ use Symfony\Component\Translation\Loader\QtFileLoader; use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Component\Translation\Loader\YamlFileLoader; +use Symfony\Component\Translation\LocaleSwitcher; use Symfony\Component\Translation\LoggingTranslator; use Symfony\Component\Translation\Reader\TranslationReader; use Symfony\Component\Translation\Reader\TranslationReaderInterface; @@ -163,4 +165,21 @@ ->tag('container.service_subscriber', ['id' => 'translator']) ->tag('kernel.cache_warmer') ; + + if (class_exists(LocaleSwitcher::class)) { + $container->services() + ->set('translation.locale_switcher', LocaleSwitcher::class) + ->args([ + param('kernel.default_locale'), + abstract_arg('LocaleAware services'), + ]) + ->alias(LocaleSwitcher::class, 'translation.locale_switcher') + + ->set('translation.locale_aware_request_context', LocaleAwareRequestContext::class) + ->args([ + service('router.request_context'), + param('kernel.default_locale'), + ]) + ; + } }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ConfigureLocaleSwitcherPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ConfigureLocaleSwitcherPassTest.php new file mode 100644 index 0000000000000..5a353ba460b1b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ConfigureLocaleSwitcherPassTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigureLocaleSwitcherPass; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class ConfigureLocaleSwitcherPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + $container->register('translation.locale_switcher')->setArgument(0, 'en'); + $container->register('locale_aware_service') + ->addTag('kernel.locale_aware') + ; + + $pass = new ConfigureLocaleSwitcherPass(); + $pass->process($container); + + $switcherDef = $container->getDefinition('translation.locale_switcher'); + + $this->assertInstanceOf(IteratorArgument::class, $switcherDef->getArgument(1)); + $this->assertEquals([new Reference('locale_aware_service')], $switcherDef->getArgument(1)->getValues()); + } + + public function testProcessWithRequestContext() + { + $container = new ContainerBuilder(); + $container->register('translation.locale_switcher'); + $container->register('locale_aware_service') + ->addTag('kernel.locale_aware') + ; + $container->register('translation.locale_aware_request_context'); + + $pass = new ConfigureLocaleSwitcherPass(); + $pass->process($container); + + $switcherDef = $container->getDefinition('translation.locale_switcher'); + + $this->assertInstanceOf(IteratorArgument::class, $switcherDef->getArgument(1)); + $this->assertEquals( + [ + new Reference('locale_aware_service'), + new Reference('translation.locale_aware_request_context'), + ], + $switcherDef->getArgument(1)->getValues() + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index ccd35edd68d08..5fad40e90655a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -30,6 +30,7 @@ use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; @@ -65,6 +66,7 @@ use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; +use Symfony\Component\Translation\LocaleSwitcher; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Validator\Validation; @@ -1967,6 +1969,26 @@ public function testIfNotifierTransportsAreKnownByFrameworkExtension() } } + public function testLocaleSwitcherServiceRegistered() + { + if (!class_exists(LocaleSwitcher::class)) { + $this->markTestSkipped('LocaleSwitcher not available.'); + } + + $container = $this->createContainerFromFile('full'); + + $this->assertTrue($container->has('translation.locale_switcher')); + $this->assertTrue($container->has('translation.locale_aware_request_context')); + + $switcherDef = $container->getDefinition('translation.locale_switcher'); + $localeAwareRequestContextDef = $container->getDefinition('translation.locale_aware_request_context'); + + $this->assertSame('%kernel.default_locale%', $switcherDef->getArgument(0)); + $this->assertInstanceOf(AbstractArgument::class, $switcherDef->getArgument(1)); + $this->assertEquals(new Reference('router.request_context'), $localeAwareRequestContextDef->getArgument(0)); + $this->assertSame('%kernel.default_locale%', $localeAwareRequestContextDef->getArgument(1)); + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/LocaleAwareRequestContextTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/LocaleAwareRequestContextTest.php new file mode 100644 index 0000000000000..f98454cb1136b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/LocaleAwareRequestContextTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Translation; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Translation\LocaleAwareRequestContext; +use Symfony\Component\Routing\RequestContext; + +class LocaleAwareRequestContextTest extends TestCase +{ + public function testCanSwitchLocale() + { + $context = new RequestContext(); + $service = new LocaleAwareRequestContext($context, 'en'); + + $this->assertSame('en', $service->getLocale()); + $this->assertNull($context->getParameter('_locale')); + + $service->setLocale('fr'); + + $this->assertSame('fr', $service->getLocale()); + $this->assertSame('fr', $context->getParameter('_locale')); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/LocaleAwareRequestContext.php b/src/Symfony/Bundle/FrameworkBundle/Translation/LocaleAwareRequestContext.php new file mode 100644 index 0000000000000..59e846b1b4ad4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/LocaleAwareRequestContext.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Translation; + +use Symfony\Component\Routing\RequestContext; +use Symfony\Contracts\Translation\LocaleAwareInterface; + +/** + * @author Kevin Bond + */ +final class LocaleAwareRequestContext implements LocaleAwareInterface +{ + public function __construct(private RequestContext $requestContext, private string $defaultLocale) + { + } + + public function setLocale(string $locale): void + { + $this->requestContext->setParameter('_locale', $locale); + } + + public function getLocale(): string + { + return $this->requestContext->getParameter('_locale') ?? $this->defaultLocale; + } +} diff --git a/src/Symfony/Component/Translation/LocaleSwitcher.php b/src/Symfony/Component/Translation/LocaleSwitcher.php new file mode 100644 index 0000000000000..9c2dc38096eef --- /dev/null +++ b/src/Symfony/Component/Translation/LocaleSwitcher.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\LocaleAwareInterface; + +/** + * @author Kevin Bond + */ +final class LocaleSwitcher implements LocaleAwareInterface +{ + /** + * @param LocaleAwareInterface[] $localeAwareServices + */ + public function __construct(private string $locale, private iterable $localeAwareServices) + { + } + + public function setLocale(string $locale): void + { + \Locale::setDefault($this->locale = $locale); + + foreach ($this->localeAwareServices as $service) { + $service->setLocale($locale); + } + } + + public function getLocale(): string + { + return $this->locale; + } + + /** + * Switch to a new locale, execute a callback, then switch back to the original. + * + * @param callable():void $callback + */ + public function runWithLocale(string $locale, callable $callback): void + { + $original = $this->getLocale(); + $this->setLocale($locale); + + try { + $callback(); + } finally { + $this->setLocale($original); + } + } +} diff --git a/src/Symfony/Component/Translation/Tests/LocaleSwitcherTest.php b/src/Symfony/Component/Translation/Tests/LocaleSwitcherTest.php new file mode 100644 index 0000000000000..a860dd64943cb --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/LocaleSwitcherTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Translation\LocaleSwitcher; +use Symfony\Contracts\Translation\LocaleAwareInterface; + +class LocaleSwitcherTest extends TestCase +{ + private string $intlLocale; + + protected function setUp(): void + { + parent::setUp(); + + $this->intlLocale = \Locale::getDefault(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + \Locale::setDefault($this->intlLocale); + } + + public function testCanSwitchLocale() + { + \Locale::setDefault('en'); + + $service = new DummyLocaleAware('en'); + $switcher = new LocaleSwitcher('en', [$service]); + + $this->assertSame('en', \Locale::getDefault()); + $this->assertSame('en', $service->getLocale()); + $this->assertSame('en', $switcher->getLocale()); + + $switcher->setLocale('fr'); + + $this->assertSame('fr', \Locale::getDefault()); + $this->assertSame('fr', $service->getLocale()); + $this->assertSame('fr', $switcher->getLocale()); + } + + public function testCanSwitchLocaleForCallback() + { + \Locale::setDefault('en'); + + $service = new DummyLocaleAware('en'); + $switcher = new LocaleSwitcher('en', [$service]); + + $this->assertSame('en', \Locale::getDefault()); + $this->assertSame('en', $service->getLocale()); + $this->assertSame('en', $switcher->getLocale()); + + $switcher->runWithLocale('fr', function () use ($switcher, $service) { + $this->assertSame('fr', \Locale::getDefault()); + $this->assertSame('fr', $service->getLocale()); + $this->assertSame('fr', $switcher->getLocale()); + }); + + $this->assertSame('en', \Locale::getDefault()); + $this->assertSame('en', $service->getLocale()); + $this->assertSame('en', $switcher->getLocale()); + } +} + +class DummyLocaleAware implements LocaleAwareInterface +{ + public function __construct(private string $locale) + { + } + + public function setLocale(string $locale): void + { + $this->locale = $locale; + } + + public function getLocale(): string + { + return $this->locale; + } +}