Added support for PHP files with translation in translation commands

This commit is contained in:
Yonel Ceruto 2019-02-08 12:17:48 -05:00
parent c6a2c3348f
commit 9f9b828832
20 changed files with 476 additions and 4 deletions

View File

@ -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
-----

View File

@ -346,7 +346,7 @@ EOF
{
$extractedCatalogue = new MessageCatalogue($locale);
foreach ($transPaths as $path) {
if (is_dir($path)) {
if (is_dir($path) || is_file($path)) {
$this->extractor->extract($path, $extractedCatalogue);
}
}

View File

@ -207,7 +207,7 @@ EOF
$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);
}
}

View File

@ -51,6 +51,7 @@ use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
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 @@ class FrameworkBundle extends Bundle
// 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);

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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');
}
}

View File

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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');
}
}

View File

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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');
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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');
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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');
}
}

View File

@ -33,8 +33,13 @@ class TranslationDebugCommandTest extends WebTestCase
$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

View File

@ -1,5 +1,6 @@
imports:
- { resource: ../config/default.yml }
- { resource: services.yml }
framework:
secret: '%secret%'

View File

@ -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'] ]

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* Improved Xliff 1.2 loader to load the original file's metadata
* Added `TranslatorPathsPass`
4.2.0
-----

View File

@ -0,0 +1,144 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <yonelceruto@gmail.com>
*/
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 [];
}
}

View File

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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));
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Symfony\Component\Translation\Tests\DependencyInjection\fixtures;
use Symfony\Contracts\Translation\TranslatorInterface;
class ControllerArguments
{
public function __invoke(TranslatorInterface $translator)
{
}
public function index(TranslatorInterface $translator)
{
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Symfony\Component\Translation\Tests\DependencyInjection\fixtures;
use Symfony\Contracts\Translation\TranslatorInterface;
class ServiceArguments
{
public function __construct(TranslatorInterface $translator)
{
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Symfony\Component\Translation\Tests\DependencyInjection\fixtures;
use Symfony\Contracts\Translation\TranslatorInterface;
class ServiceMethodCalls
{
public function setTranslator(TranslatorInterface $translator)
{
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Symfony\Component\Translation\Tests\DependencyInjection\fixtures;
class ServiceProperties
{
public $translator;
}

View File

@ -0,0 +1,19 @@
<?php
namespace Symfony\Component\Translation\Tests\DependencyInjection\fixtures;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ServiceSubscriber implements ServiceSubscriberInterface
{
public function __construct(ContainerInterface $container)
{
}
public static function getSubscribedServices()
{
return ['translator' => TranslatorInterface::class];
}
}