diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 434305545cc1e..4c91ff11bef31 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG * Added php ini session options `sid_length` and `sid_bits_per_character` to the `session` section of the configuration * Added support for Translator paths, Twig paths in translation commands. + * Added support for PHP files with translations in translation commands. 4.2.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index 57e364d8ffffd..555db678e1a24 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -346,7 +346,7 @@ private function extractMessages(string $locale, array $transPaths): MessageCata { $extractedCatalogue = new MessageCatalogue($locale); foreach ($transPaths as $path) { - if (is_dir($path)) { + if (is_dir($path) || is_file($path)) { $this->extractor->extract($path, $extractedCatalogue); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index 8045176fcc01b..ded71fd60e4bb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -207,7 +207,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $errorIo->comment('Parsing templates...'); $this->extractor->setPrefix($input->getOption('prefix')); foreach ($viewsPaths as $path) { - if (is_dir($path)) { + if (is_dir($path) || is_file($path)) { $this->extractor->extract($path, $extractedCatalogue); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 2a561bc19be43..debbc69a676ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -51,6 +51,7 @@ use Symfony\Component\Translation\DependencyInjection\TranslationDumperPass; use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; +use Symfony\Component\Translation\DependencyInjection\TranslatorPathsPass; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass; use Symfony\Component\Workflow\DependencyInjection\ValidateWorkflowsPass; @@ -103,6 +104,7 @@ public function build(ContainerBuilder $container) // must be registered as late as possible to get access to all Twig paths registered in // twig.template_iterator definition $this->addCompilerPassIfExists($container, TranslatorPass::class, PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); + $this->addCompilerPassIfExists($container, TranslatorPathsPass::class, PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new LoggingTranslatorPass()); $container->addCompilerPass(new AddExpressionLanguageProvidersPass(false)); $this->addCompilerPassIfExists($container, TranslationExtractorPass::class); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/TransController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/TransController.php new file mode 100644 index 0000000000000..e4127bc17bf37 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/TransController.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller; + +use Symfony\Contracts\Translation\TranslatorInterface; + +class TransController +{ + public function index(TranslatorInterface $translator) + { + $translator->trans('hello_from_controller'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransConstructArgService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransConstructArgService.php new file mode 100644 index 0000000000000..c4b985e8ee80d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransConstructArgService.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TransDebug; + +use Symfony\Contracts\Translation\TranslatorInterface; + +class TransConstructArgService +{ + private $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + public function hello(): string + { + return $this->translator->trans('hello_from_construct_arg_service'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransMethodCallsService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransMethodCallsService.php new file mode 100644 index 0000000000000..66247e56c6175 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransMethodCallsService.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TransDebug; + +use Symfony\Contracts\Translation\TranslatorInterface; + +class TransMethodCallsService +{ + private $translator; + + public function setTranslator(TranslatorInterface $translator): void + { + $this->translator = $translator; + } + + public function hello(): string + { + return $this->translator->trans('hello_from_method_calls_service'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransPropertyService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransPropertyService.php new file mode 100644 index 0000000000000..6bd17bdfd034a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransPropertyService.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TransDebug; + +use Symfony\Contracts\Translation\TranslatorInterface; + +class TransPropertyService +{ + /** @var TranslatorInterface */ + public $translator; + + public function hello(): string + { + return $this->translator->trans('hello_from_property_service'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransSubscriberService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransSubscriberService.php new file mode 100644 index 0000000000000..449b35f5e9450 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransSubscriberService.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TransDebug; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +class TransSubscriberService implements ServiceSubscriberInterface +{ + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + public static function getSubscribedServices() + { + return ['translator' => TranslatorInterface::class]; + } + + public function hello(): string + { + return $this->container->get('translator')->trans('hello_from_subscriber_service'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php index ebc485fa3a86e..1da49ce79c8f6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php @@ -33,8 +33,13 @@ public function testDumpAllTrans() $ret = $tester->execute(['locale' => 'en']); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('unused validators This value should be blank.', $tester->getDisplay()); - $this->assertContains('unused security Invalid CSRF token.', $tester->getDisplay()); + $this->assertContains('missing messages hello_from_construct_arg_service', $tester->getDisplay()); + $this->assertContains('missing messages hello_from_subscriber_service', $tester->getDisplay()); + $this->assertContains('missing messages hello_from_property_service', $tester->getDisplay()); + $this->assertContains('missing messages hello_from_method_calls_service', $tester->getDisplay()); + $this->assertContains('missing messages hello_from_controller', $tester->getDisplay()); + $this->assertContains('unused validators This value should be blank.', $tester->getDisplay()); + $this->assertContains('unused security Invalid CSRF token.', $tester->getDisplay()); } private function createCommandTester(): CommandTester diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml index 6f52f7404ff20..7f8815b2942fa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml @@ -1,5 +1,6 @@ imports: - { resource: ../config/default.yml } + - { resource: services.yml } framework: secret: '%secret%' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/services.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/services.yml new file mode 100644 index 0000000000000..cfb810144ca91 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/services.yml @@ -0,0 +1,21 @@ +services: + _defaults: + public: true + + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\TransController: + tags: ['controller.service_arguments'] + + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TransDebug\TransConstructArgService: + arguments: ['@translator'] + + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TransDebug\TransSubscriberService: + arguments: ['@Psr\Container\ContainerInterface'] + tags: ['container.service_subscriber'] + + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TransDebug\TransPropertyService: + properties: + $translator: '@translator' + + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TransDebug\TransMethodCallsService: + calls: + - [ setTranslator, ['@translator'] ] diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index 42ae7789fa192..c80716838b4b6 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * Improved Xliff 1.2 loader to load the original file's metadata + * Added `TranslatorPathsPass` 4.2.0 ----- diff --git a/src/Symfony/Component/Translation/DependencyInjection/TranslatorPathsPass.php b/src/Symfony/Component/Translation/DependencyInjection/TranslatorPathsPass.php new file mode 100644 index 0000000000000..d9fc71911fedb --- /dev/null +++ b/src/Symfony/Component/Translation/DependencyInjection/TranslatorPathsPass.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * @author Yonel Ceruto + */ +class TranslatorPathsPass extends AbstractRecursivePass +{ + private $translatorServiceId; + private $debugCommandServiceId; + private $updateCommandServiceId; + private $resolverServiceId; + private $level = 0; + private $paths = []; + private $definitions = []; + private $controllers = []; + + public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update', string $resolverServiceId = 'argument_resolver.service') + { + $this->translatorServiceId = $translatorServiceId; + $this->debugCommandServiceId = $debugCommandServiceId; + $this->updateCommandServiceId = $updateCommandServiceId; + $this->resolverServiceId = $resolverServiceId; + } + + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->translatorServiceId)) { + return; + } + + foreach ($this->findControllerArguments($container) as $controller => $argument) { + $id = \substr($controller, 0, \strpos($controller, ':') ?: \strlen($controller)); + if ($container->hasDefinition($id)) { + list($locatorRef) = $argument->getValues(); + $this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true; + } + } + + try { + parent::process($container); + + $paths = []; + foreach ($this->paths as $class => $_) { + if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) { + $paths[] = $r->getFileName(); + } + } + if ($paths) { + if ($container->hasDefinition($this->debugCommandServiceId)) { + $definition = $container->getDefinition($this->debugCommandServiceId); + $definition->replaceArgument(6, array_merge($definition->getArgument(6), $paths)); + } + if ($container->hasDefinition($this->updateCommandServiceId)) { + $definition = $container->getDefinition($this->updateCommandServiceId); + $definition->replaceArgument(7, array_merge($definition->getArgument(7), $paths)); + } + } + } finally { + $this->level = 0; + $this->paths = []; + $this->definitions = []; + } + } + + protected function processValue($value, $isRoot = false) + { + if ($value instanceof Reference) { + if ((string) $value === $this->translatorServiceId) { + for ($i = $this->level - 1; $i >= 0; --$i) { + $class = $this->definitions[$i]->getClass(); + + if (ServiceLocator::class === $class) { + if (!isset($this->controllers[$this->currentId])) { + continue; + } + foreach ($this->controllers[$this->currentId] as $class => $_) { + $this->paths[$class] = true; + } + } else { + $this->paths[$class] = true; + } + + break; + } + } + + return $value; + } + + if ($value instanceof Definition) { + $this->definitions[$this->level++] = $value; + $value = parent::processValue($value, $isRoot); + unset($this->definitions[--$this->level]); + + return $value; + } + + return parent::processValue($value, $isRoot); + } + + private function findControllerArguments(ContainerBuilder $container): array + { + if ($container->hasDefinition($this->resolverServiceId)) { + $argument = $container->getDefinition($this->resolverServiceId)->getArgument(0); + if ($argument instanceof Reference) { + $argument = $container->getDefinition($argument); + } + + return $argument->getArgument(0); + } + + if ($container->hasDefinition('debug.'.$this->resolverServiceId)) { + $argument = $container->getDefinition('debug.'.$this->resolverServiceId)->getArgument(0); + if ($argument instanceof Reference) { + $argument = $container->getDefinition($argument); + } + $argument = $argument->getArgument(0); + if ($argument instanceof Reference) { + $argument = $container->getDefinition($argument); + } + + return $argument->getArgument(0); + } + + return []; + } +} diff --git a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationPathsPassTest.php b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationPathsPassTest.php new file mode 100644 index 0000000000000..42ab398dffe7b --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationPathsPassTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Translation\DependencyInjection\TranslatorPathsPass; +use Symfony\Component\Translation\Tests\DependencyInjection\fixtures\ControllerArguments; +use Symfony\Component\Translation\Tests\DependencyInjection\fixtures\ServiceArguments; +use Symfony\Component\Translation\Tests\DependencyInjection\fixtures\ServiceMethodCalls; +use Symfony\Component\Translation\Tests\DependencyInjection\fixtures\ServiceProperties; +use Symfony\Component\Translation\Tests\DependencyInjection\fixtures\ServiceSubscriber; + +class TranslationPathsPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + $container->register('translator'); + $debugCommand = $container->register('console.command.translation_debug') + ->setArguments([null, null, null, null, null, [], []]) + ; + $updateCommand = $container->register('console.command.translation_update') + ->setArguments([null, null, null, null, null, null, [], []]) + ; + $container->register(ControllerArguments::class, ControllerArguments::class) + ->setTags(['controller.service_arguments']) + ; + $container->register(ServiceArguments::class, ServiceArguments::class) + ->setArguments([new Reference('translator')]) + ; + $container->register(ServiceProperties::class, ServiceProperties::class) + ->setProperties([new Reference('translator')]) + ; + $container->register(ServiceMethodCalls::class, ServiceMethodCalls::class) + ->setMethodCalls([['setTranslator', [new Reference('translator')]]]) + ; + $container->register('service_rc') + ->setArguments([new Definition(), new Reference(ServiceMethodCalls::class)]) + ; + $serviceLocator1 = $container->register('.service_locator.foo', ServiceLocator::class) + ->setArguments([new ServiceClosureArgument(new Reference('translator'))]) + ; + $serviceLocator2 = (new Definition(ServiceLocator::class)) + ->setArguments([ServiceSubscriber::class, new Reference('service_container')]) + ->setFactory([$serviceLocator1, 'withContext']) + ; + $container->register('service_subscriber', ServiceSubscriber::class) + ->setArguments([$serviceLocator2]) + ; + $container->register('.service_locator.bar', ServiceLocator::class) + ->setArguments([[ + ControllerArguments::class.'::index' => new ServiceClosureArgument(new Reference('.service_locator.foo')), + ControllerArguments::class.'::__invoke' => new ServiceClosureArgument(new Reference('.service_locator.foo')), + ControllerArguments::class => new ServiceClosureArgument(new Reference('.service_locator.foo')), + ]]) + ; + $container->register('argument_resolver.service') + ->setArguments([new Reference('.service_locator.bar')]) + ; + + $pass = new TranslatorPathsPass('translator', 'console.command.translation_debug', 'console.command.translation_update', 'argument_resolver.service'); + $pass->process($container); + + $expectedPaths = [ + $container->getReflectionClass(ServiceArguments::class)->getFileName(), + $container->getReflectionClass(ServiceProperties::class)->getFileName(), + $container->getReflectionClass(ServiceMethodCalls::class)->getFileName(), + $container->getReflectionClass(ControllerArguments::class)->getFileName(), + $container->getReflectionClass(ServiceSubscriber::class)->getFileName(), + ]; + + $this->assertSame($expectedPaths, $debugCommand->getArgument(6)); + $this->assertSame($expectedPaths, $updateCommand->getArgument(7)); + } +} diff --git a/src/Symfony/Component/Translation/Tests/DependencyInjection/fixtures/ControllerArguments.php b/src/Symfony/Component/Translation/Tests/DependencyInjection/fixtures/ControllerArguments.php new file mode 100644 index 0000000000000..97a53fa76bcd8 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/DependencyInjection/fixtures/ControllerArguments.php @@ -0,0 +1,16 @@ + TranslatorInterface::class]; + } +}