From 85177a649e3e50b7c59da25c763bfea62414ef08 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Wed, 15 Mar 2017 16:59:02 +0100 Subject: [PATCH] [FrameworkBundle] Make Translator works with any PSR-11 container --- UPGRADE-3.3.md | 3 + UPGRADE-4.0.md | 3 + .../Bundle/FrameworkBundle/CHANGELOG.md | 2 + .../Compiler/TranslatorPass.php | 12 +- .../FrameworkExtension.php | 4 +- .../Resources/config/translation.xml | 6 +- .../Compiler/TranslatorPassTest.php | 12 +- .../FrameworkExtensionTest.php | 5 +- .../Tests/Translation/TranslatorTest.php | 182 ++++++++++++++++-- .../Translation/Translator.php | 19 +- 10 files changed, 222 insertions(+), 26 deletions(-) diff --git a/UPGRADE-3.3.md b/UPGRADE-3.3.md index 7caf4a7e14edd..beeeea4c7ffab 100644 --- a/UPGRADE-3.3.md +++ b/UPGRADE-3.3.md @@ -180,6 +180,9 @@ FrameworkBundle Require `symfony/web-server-bundle` in your composer.json and register `Symfony\Bundle\WebServerBundle\WebServerBundle` in your AppKernel to use them. + * The `Symfony\Bundle\FrameworkBundle\Translation\Translator` constructor now takes the + default locale as 3rd argument. Not passing it will trigger an error in 4.0. + HttpKernel ----------- diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index b4fafca01bdc6..7c9d0a29ff5eb 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -274,6 +274,9 @@ FrameworkBundle class has been removed. Use the `Symfony\Component\Routing\DependencyInjection\RoutingResolverPass` class instead. + * The `Symfony\Bundle\FrameworkBundle\Translation\Translator` constructor now takes the + default locale as mandatory 3rd argument. + HttpFoundation --------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index da650439d6601..1dcc2f42f1e05 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -35,6 +35,8 @@ CHANGELOG `server:status` console commands have been moved to a dedicated bundle. Require `symfony/web-server-bundle` in your composer.json and register `Symfony\Bundle\WebServerBundle\WebServerBundle` in your AppKernel to use them. + * Added `$defaultLocale` as 3rd argument of `Translator::__construct()` + making `Translator` works with any PSR-11 container 3.2.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslatorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslatorPass.php index 4e450166afa44..2bdd9d8055e98 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslatorPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslatorPass.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ServiceLocator; class TranslatorPass implements CompilerPassInterface { @@ -24,7 +26,9 @@ public function process(ContainerBuilder $container) } $loaders = array(); + $loaderRefs = array(); foreach ($container->findTaggedServiceIds('translation.loader') as $id => $attributes) { + $loaderRefs[$id] = new Reference($id); $loaders[$id][] = $attributes[0]['alias']; if (isset($attributes[0]['legacy-alias'])) { $loaders[$id][] = $attributes[0]['legacy-alias']; @@ -35,11 +39,15 @@ public function process(ContainerBuilder $container) $definition = $container->getDefinition('translation.loader'); foreach ($loaders as $id => $formats) { foreach ($formats as $format) { - $definition->addMethodCall('addLoader', array($format, new Reference($id))); + $definition->addMethodCall('addLoader', array($format, $loaderRefs[$id])); } } } - $container->findDefinition('translator.default')->replaceArgument(2, $loaders); + $container + ->findDefinition('translator.default') + ->replaceArgument(0, (new Definition(ServiceLocator::class, array($loaderRefs)))->addTag('container.service_locator')) + ->replaceArgument(3, $loaders) + ; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 17a613b70c1bc..637d2b88c6fd4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -954,11 +954,11 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder } $options = array_merge( - $translator->getArgument(3), + $translator->getArgument(4), array('resource_files' => $files) ); - $translator->replaceArgument(3, $options); + $translator->replaceArgument(4, $options); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml index 6cd41fb882f3e..2fa53885b9d8f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml @@ -6,14 +6,14 @@ - + - + %kernel.default_locale% + %kernel.cache_dir%/translations %kernel.debug% - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TranslatorPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TranslatorPassTest.php index 10a38aabdb402..cfb5beeb550f4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TranslatorPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TranslatorPassTest.php @@ -12,8 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslatorPass; +use Symfony\Component\DependencyInjection\ServiceLocator; class TranslatorPassTest extends TestCase { @@ -39,7 +41,15 @@ public function testValidCollector() ->will($this->returnValue(array('xliff' => array(array('alias' => 'xliff', 'legacy-alias' => 'xlf'))))); $container->expects($this->once()) ->method('findDefinition') - ->will($this->returnValue($this->getMockBuilder('Symfony\Component\DependencyInjection\Definition')->getMock())); + ->will($this->returnValue($translator = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition')->getMock())); + $translator->expects($this->at(0)) + ->method('replaceArgument') + ->with(0, $this->equalTo((new Definition(ServiceLocator::class, array(array('xliff' => new Reference('xliff')))))->addTag('container.service_locator'))) + ->willReturn($translator); + $translator->expects($this->at(1)) + ->method('replaceArgument') + ->with(3, array('xliff' => array('xliff', 'xlf'))) + ->willReturn($translator); $pass = new TranslatorPass(); $pass->process($container); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 64423bda88702..a04a213aeaa48 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\Common\Annotations\Annotation; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslatorPass; use Symfony\Bundle\FullStack; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddAnnotationsCachedReaderPass; @@ -417,7 +418,7 @@ public function testTranslator() $container = $this->createContainerFromFile('full'); $this->assertTrue($container->hasDefinition('translator.default'), '->registerTranslatorConfiguration() loads translation.xml'); $this->assertEquals('translator.default', (string) $container->getAlias('translator'), '->registerTranslatorConfiguration() redefines translator service from identity to real translator'); - $options = $container->getDefinition('translator.default')->getArgument(3); + $options = $container->getDefinition('translator.default')->getArgument(4); $files = array_map('realpath', $options['resource_files']['en']); $ref = new \ReflectionClass('Symfony\Component\Validator\Validation'); @@ -922,7 +923,7 @@ protected function createContainerFromFile($file, $data = array(), $resetCompile $container->getCompilerPassConfig()->setOptimizationPasses(array()); $container->getCompilerPassConfig()->setRemovingPasses(array()); } - $container->getCompilerPassConfig()->setBeforeRemovingPasses(array(new AddAnnotationsCachedReaderPass(), new AddConstraintValidatorsPass())); + $container->getCompilerPassConfig()->setBeforeRemovingPasses(array(new AddAnnotationsCachedReaderPass(), new AddConstraintValidatorsPass(), new TranslatorPass())); $container->compile(); return self::$containerCache[$cacheKey] = $container; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index d9efdfe6e94d5..ef6fa9330b0d8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Translation; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Translation\Translator; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Filesystem\Filesystem; @@ -42,6 +43,157 @@ protected function deleteTmpDir() $fs->remove($dir); } + /** + * @group legacy + * @expectedDeprecation Method Symfony\Bundle\FrameworkBundle\Translation\Translator::__construct() takes the default locale as 3rd argument since version 3.3. Not passing it is deprecated and will trigger an error in 4.0. + */ + public function testTransWithoutCachingOmittingLocale() + { + $translator = $this->getTranslator($this->getLoader(), array(), 'loader', '\Symfony\Bundle\FrameworkBundle\Translation\Translator', null); + $translator->setLocale('fr'); + $translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin')); + + $this->assertEquals('foo (FR)', $translator->trans('foo')); + $this->assertEquals('bar (EN)', $translator->trans('bar')); + $this->assertEquals('foobar (ES)', $translator->trans('foobar')); + $this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0)); + $this->assertEquals('no translation', $translator->trans('no translation')); + $this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo')); + $this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1)); + $this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz')); + $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); + } + + /** + * @group legacy + * @expectedDeprecation Method Symfony\Bundle\FrameworkBundle\Translation\Translator::__construct() takes the default locale as 3rd argument since version 3.3. Not passing it is deprecated and will trigger an error in 4.0. + */ + public function testTransWithCachingOmittingLocale() + { + // prime the cache + $translator = $this->getTranslator($this->getLoader(), array('cache_dir' => $this->tmpDir), 'loader', '\Symfony\Bundle\FrameworkBundle\Translation\Translator', null); + $translator->setLocale('fr'); + $translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin')); + + $this->assertEquals('foo (FR)', $translator->trans('foo')); + $this->assertEquals('bar (EN)', $translator->trans('bar')); + $this->assertEquals('foobar (ES)', $translator->trans('foobar')); + $this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0)); + $this->assertEquals('no translation', $translator->trans('no translation')); + $this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo')); + $this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1)); + $this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz')); + $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); + + // do it another time as the cache is primed now + $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); + $loader->expects($this->never())->method('load'); + + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), 'loader', '\Symfony\Bundle\FrameworkBundle\Translation\Translator', null); + $translator->setLocale('fr'); + $translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin')); + + $this->assertEquals('foo (FR)', $translator->trans('foo')); + $this->assertEquals('bar (EN)', $translator->trans('bar')); + $this->assertEquals('foobar (ES)', $translator->trans('foobar')); + $this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0)); + $this->assertEquals('no translation', $translator->trans('no translation')); + $this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo')); + $this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1)); + $this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz')); + $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); + } + + /** + * @group legacy + * @expectedDeprecation Method Symfony\Bundle\FrameworkBundle\Translation\Translator::__construct() takes the default locale as 3rd argument since version 3.3. Not passing it is deprecated and will trigger an error in 4.0. + * @expectedException \InvalidArgumentException + */ + public function testTransWithCachingWithInvalidLocaleOmittingLocale() + { + $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), 'loader', '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale', null); + + $translator->trans('foo'); + } + + /** + * @group legacy + * @expectedDeprecation Method Symfony\Bundle\FrameworkBundle\Translation\Translator::__construct() takes the default locale as 3rd argument since version 3.3. Not passing it is deprecated and will trigger an error in 4.0. + */ + public function testLoadResourcesWithoutCachingOmittingLocale() + { + $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $resourceFiles = array( + 'fr' => array( + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ), + ); + + $translator = $this->getTranslator($loader, array('resource_files' => $resourceFiles), 'yml', '\Symfony\Bundle\FrameworkBundle\Translation\Translator', null); + $translator->setLocale('fr'); + + $this->assertEquals('répertoire', $translator->trans('folder')); + } + + /** + * @group legacy + * @expectedDeprecation Method Symfony\Bundle\FrameworkBundle\Translation\Translator::__construct() takes the default locale as 3rd argument since version 3.3. Not passing it is deprecated and will trigger an error in 4.0. + */ + public function testGetDefaultLocaleOmittingLocale() + { + $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + $container + ->expects($this->once()) + ->method('getParameter') + ->with('kernel.default_locale') + ->will($this->returnValue('en')) + ; + $translator = new Translator($container, new MessageSelector()); + + $this->assertSame('en', $translator->getLocale()); + } + + /** + * @group legacy + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Missing third $defaultLocale argument. + */ + public function testGetDefaultLocaleOmittingLocaleWithPsrContainer() + { + $container = $this->getMockBuilder(ContainerInterface::class)->getMock(); + $translator = new Translator($container, new MessageSelector()); + } + + /** + * @group legacy + * @expectedDeprecation Method Symfony\Bundle\FrameworkBundle\Translation\Translator::__construct() takes the default locale as 3rd argument since version 3.3. Not passing it is deprecated and will trigger an error in 4.0. + */ + public function testWarmupOmittingLocale() + { + $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $resourceFiles = array( + 'fr' => array( + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ), + ); + + // prime the cache + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml', '\Symfony\Bundle\FrameworkBundle\Translation\Translator', null); + $translator->setFallbackLocales(array('fr')); + $translator->warmup($this->tmpDir); + + $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); + $loader + ->expects($this->never()) + ->method('load'); + + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml', '\Symfony\Bundle\FrameworkBundle\Translation\Translator', null); + $translator->setLocale('fr'); + $translator->setFallbackLocales(array('fr')); + $this->assertEquals('répertoire', $translator->trans('folder')); + } + public function testTransWithoutCaching() { $translator = $this->getTranslator($this->getLoader()); @@ -97,6 +249,7 @@ public function testTransWithCaching() /** * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Invalid "invalid locale" locale. */ public function testTransWithCachingWithInvalidLocale() { @@ -123,15 +276,8 @@ public function testLoadResourcesWithoutCaching() public function testGetDefaultLocale() { - $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); - $container - ->expects($this->once()) - ->method('getParameter') - ->with('kernel.default_locale') - ->will($this->returnValue('en')) - ; - - $translator = new Translator($container, new MessageSelector()); + $container = $this->getMockBuilder(ContainerInterface::class)->getMock(); + $translator = new Translator($container, new MessageSelector(), 'en'); $this->assertSame('en', $translator->getLocale()); } @@ -144,7 +290,7 @@ public function testInvalidOptions() { $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); - (new Translator($container, new MessageSelector(), array(), array('foo' => 'bar'))); + (new Translator($container, new MessageSelector(), 'en', array(), array('foo' => 'bar'))); } protected function getCatalogue($locale, $messages, $resources = array()) @@ -230,9 +376,9 @@ protected function getContainer($loader) return $container; } - public function getTranslator($loader, $options = array(), $loaderFomat = 'loader', $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator') + public function getTranslator($loader, $options = array(), $loaderFomat = 'loader', $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator', $defaultLocale = 'en') { - $translator = $this->createTranslator($loader, $options, $translatorClass, $loaderFomat); + $translator = $this->createTranslator($loader, $options, $translatorClass, $loaderFomat, $defaultLocale); if ('loader' === $loaderFomat) { $translator->addResource('loader', 'foo', 'fr'); @@ -272,11 +418,21 @@ public function testWarmup() $this->assertEquals('répertoire', $translator->trans('folder')); } - private function createTranslator($loader, $options, $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator', $loaderFomat = 'loader') + private function createTranslator($loader, $options, $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator', $loaderFomat = 'loader', $defaultLocale = 'en') { + if (null === $defaultLocale) { + return new $translatorClass( + $this->getContainer($loader), + new MessageSelector(), + array($loaderFomat => array($loaderFomat)), + $options + ); + } + return new $translatorClass( $this->getContainer($loader), new MessageSelector(), + $defaultLocale, array($loaderFomat => array($loaderFomat)), $options ); diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index fedc425a11a07..6bcbaa8e97416 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -11,10 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Translation; +use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Translation\Translator as BaseTranslator; use Symfony\Component\Translation\MessageSelector; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** @@ -54,8 +55,20 @@ class Translator extends BaseTranslator implements WarmableInterface * * @throws InvalidArgumentException */ - public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array()) + public function __construct(ContainerInterface $container, MessageSelector $selector, $defaultLocale = null, array $loaderIds = array(), array $options = array()) { + // BC 3.x, to be removed in 4.0 along with the $defaultLocale default value + if (is_array($defaultLocale) || 3 > func_num_args()) { + if (!$container instanceof SymfonyContainerInterface) { + throw new \InvalidArgumentException('Missing third $defaultLocale argument.'); + } + + $options = $loaderIds; + $loaderIds = $defaultLocale; + $defaultLocale = $container->getParameter('kernel.default_locale'); + @trigger_error(sprintf('Method %s() takes the default locale as 3rd argument since version 3.3. Not passing it is deprecated and will trigger an error in 4.0.', __METHOD__), E_USER_DEPRECATED); + } + $this->container = $container; $this->loaderIds = $loaderIds; @@ -70,7 +83,7 @@ public function __construct(ContainerInterface $container, MessageSelector $sele $this->loadResources(); } - parent::__construct($container->getParameter('kernel.default_locale'), $selector, $this->options['cache_dir'], $this->options['debug']); + parent::__construct($defaultLocale, $selector, $this->options['cache_dir'], $this->options['debug']); } /**