Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class TranslationUpdateCommandPass implements CompilerPassInterface
class TranslationExtractCommandPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationLintCommandPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationUpdateCommandPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractCommandPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\VirtualRequestStackPass;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
Expand Down Expand Up @@ -188,7 +188,7 @@ public function build(ContainerBuilder $container): void
// must be registered after MonologBundle's LoggerChannelPass
$container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new VirtualRequestStackPass());
$container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->addCompilerPass(new TranslationExtractCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
$this->addCompilerPassIfExists($container, StreamablePass::class);

if ($container->getParameter('kernel.debug')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\Extractor\PhpAstExtractor;
use Symfony\Component\Translation\Extractor\Visitor\ConstraintVisitor;
use Symfony\Component\Translation\Extractor\Visitor\FormTypeVisitor;
use Symfony\Component\Translation\Extractor\Visitor\TranslatableMessageVisitor;
use Symfony\Component\Translation\Extractor\Visitor\TransMethodVisitor;
use Symfony\Component\Translation\Formatter\MessageFormatter;
Expand Down Expand Up @@ -164,6 +165,9 @@
->set('translation.extractor.visitor.constraint', ConstraintVisitor::class)
->tag('translation.extractor.visitor')

->set('translation.extractor.visitor.form_type', FormTypeVisitor::class)
->tag('translation.extractor.visitor')

->set('translation.reader', TranslationReader::class)
->alias(TranslationReaderInterface::class, 'translation.reader')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ public function process(ContainerBuilder $container): void
return;
}

$this->processLoadersAndReaders($container);
$this->processExtractorConstraintVisitor($container);
$this->processTwigPaths($container);
}

private function processLoadersAndReaders(ContainerBuilder $container): void
{
$loaders = [];
$loaderRefs = [];
foreach ($container->findTaggedServiceIds('translation.loader', true) as $id => $attributes) {
Expand All @@ -48,29 +55,38 @@ public function process(ContainerBuilder $container): void
->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs))
->replaceArgument(3, $loaders)
;
}

if ($container->hasDefinition('validator') && $container->hasDefinition('translation.extractor.visitor.constraint')) {
$constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint');
$constraintClassNames = [];
private function processExtractorConstraintVisitor(ContainerBuilder $container): void
{
if (!$container->hasDefinition('validator') || !$container->hasDefinition('translation.extractor.visitor.constraint')) {
return;
}

foreach ($container->getDefinitions() as $definition) {
if (!$definition->hasTag('validator.constraint_validator')) {
continue;
}
// Resolve constraint validator FQCN even if defined as %foo.validator.class% parameter
$className = $container->getParameterBag()->resolveValue($definition->getClass());
// Extraction of the constraint class name from the Constraint Validator FQCN
$constraintClassNames[] = str_replace('Validator', '', substr(strrchr($className, '\\'), 1));
}
$constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint');
$constraintClassNames = [];

$constraintVisitorDefinition->setArgument(0, $constraintClassNames);
foreach ($container->getDefinitions() as $definition) {
if (!$definition->hasTag('validator.constraint_validator')) {
continue;
}
// Resolve constraint validator FQCN even if defined as %foo.validator.class% parameter
$className = $container->getParameterBag()->resolveValue($definition->getClass());
// Extraction of the constraint class name from the Constraint Validator FQCN
$constraintClassNames[] = str_replace('Validator', '', substr(strrchr($className, '\\'), 1));
}

$constraintVisitorDefinition->setArgument(0, $constraintClassNames);
}

private function processTwigPaths(ContainerBuilder $container): void
{
if (!$container->hasParameter('twig.default_path')) {
return;
}

$paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1));

if ($container->hasDefinition('console.command.translation_debug')) {
$definition = $container->getDefinition('console.command.translation_debug');
$definition->replaceArgument(4, $container->getParameter('twig.default_path'));
Expand All @@ -79,6 +95,7 @@ public function process(ContainerBuilder $container): void
$definition->replaceArgument(6, $paths);
}
}

if ($container->hasDefinition('console.command.translation_extract')) {
$definition = $container->getDefinition('console.command.translation_extract');
$definition->replaceArgument(5, $container->getParameter('twig.default_path'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ protected function canBeExtracted(string $file): bool
{
return 'php' === pathinfo($file, \PATHINFO_EXTENSION)
&& $this->isFile($file)
&& preg_match('/\bt\(|->trans\(|TranslatableMessage|Symfony\\\\Component\\\\Validator\\\\Constraints/i', file_get_contents($file));
&& preg_match('/\bt\(|->trans\(|TranslatableMessage|Symfony\\\\Component\\\\Validator\\\\Constraints|Symfony\\\\Component\\\\Form\\\\AbstractType/i', file_get_contents($file));
}

protected function extractFromDirectory(array|string $resource): iterable|Finder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ protected function nodeFirstNamedArgumentIndex(Node\Expr\CallLike|Node\Attribute
return \PHP_INT_MAX;
}

private function getStringNamedArguments(Node\Expr\CallLike|Node\Attribute $node, ?string $argumentName = null, bool $isArgumentNamePattern = false): array
protected function getStringNamedArguments(Node\Expr\CallLike|Node\Attribute $node, ?string $argumentName = null, bool $isArgumentNamePattern = false): array
{
$args = $node instanceof Node\Expr\CallLike ? $node->getArgs() : $node->args;
$argumentValues = [];
Expand All @@ -97,22 +97,14 @@ private function getStringNamedArguments(Node\Expr\CallLike|Node\Attribute $node
return array_filter($argumentValues);
}

private function getStringValue(Node $node): ?string
protected function getStringValue(Node $node): ?string
{
if ($node instanceof Node\Scalar\String_) {
return $node->value;
}

if ($node instanceof Node\Expr\BinaryOp\Concat) {
if (null === $left = $this->getStringValue($node->left)) {
return null;
}

if (null === $right = $this->getStringValue($node->right)) {
return null;
}

return $left.$right;
return $this->getStringValueFromConcatNode($node);
}

if ($node instanceof Node\Expr\Assign && $node->expr instanceof Node\Scalar\String_) {
Expand All @@ -132,4 +124,17 @@ private function getStringValue(Node $node): ?string

return null;
}

private function getStringValueFromConcatNode(Node\Expr\BinaryOp\Concat $node): ?string
{
if (null === $left = $this->getStringValue($node->left)) {
return null;
}

if (null === $right = $this->getStringValue($node->right)) {
return null;
}

return $left.$right;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Translation\Extractor\Visitor;

use PhpParser\Node;
use PhpParser\NodeVisitor;
use Symfony\Component\Form\AbstractType;

/**
* @author Mathieu Santostefano <[email protected]>
*
* Code mostly comes from https://github.com/php-translation/extractor/blob/master/src/Visitor/Php/Symfony/
*/
final class FormTypeVisitor extends AbstractVisitor implements NodeVisitor
{
/**
* Stores whether the current class is a form type across visits of all children nodes.
*/
private bool $isFormType = false;

public function beforeTraverse(array $nodes): ?Node
{
return null;
}

public function enterNode(Node $node): ?Node
{
if (!$this->isFormType($node)) {
return null;
}

// Visit all array expressions to look for options array
if ($node instanceof Node\Expr\Array_) {
$this->visitOptionsArray($node);
}

// Visit all "add()" method calls to look for implicit labels
if ($node instanceof Node\Expr\MethodCall) {
$this->visitAddMethodCall($node);
}

return null;
}

public function leaveNode(Node $node): ?Node
{
if ($node instanceof Node\Stmt\Class_) {
$this->isFormType = false;
}

return null;
}

public function afterTraverse(array $nodes): ?Node
{
return null;
}

private function visitAddMethodCall(Node\Expr\MethodCall $node): void
{
if ('add' !== $node->name->name) {

Check failure on line 70 in src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedPropertyFetch

src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php:70:23: UndefinedPropertyFetch: Instance property PhpParser\Node\Expr::$name is not defined (see https://psalm.dev/039)

Check failure on line 70 in src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedPropertyFetch

src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php:70:23: UndefinedPropertyFetch: Instance property PhpParser\Node\Expr::$name is not defined (see https://psalm.dev/039)
return;
}

if (!$node->args[0]->value instanceof Node\Scalar\String_) {

Check failure on line 74 in src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedPropertyFetch

src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php:74:14: UndefinedPropertyFetch: Instance property PhpParser\Node\VariadicPlaceholder::$value is not defined (see https://psalm.dev/039)

Check failure on line 74 in src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedPropertyFetch

src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php:74:14: UndefinedPropertyFetch: Instance property PhpParser\Node\VariadicPlaceholder::$value is not defined (see https://psalm.dev/039)
return;
}

if (\count($node->args) === 1) {
$this->addMessageToCatalogue($this->getStringValue($node->args[0]->value), 'messages', $node->args[0]->value->getStartLine());

Check failure on line 79 in src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedPropertyFetch

src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php:79:64: UndefinedPropertyFetch: Instance property PhpParser\Node\VariadicPlaceholder::$value is not defined (see https://psalm.dev/039)

Check failure on line 79 in src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedPropertyFetch

src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php:79:64: UndefinedPropertyFetch: Instance property PhpParser\Node\VariadicPlaceholder::$value is not defined (see https://psalm.dev/039)
}
}

private function visitOptionsArray(Node\Expr\Array_ $node): void
{
$translatableOptions = ['label', 'placeholder', 'help'];

foreach ($node->items as $item) {
if (!$item->key instanceof Node\Scalar\String_ || !in_array($item->key->value, $translatableOptions, true)) {
continue;
}

// If the option is a non-empty string, add it to the messages
$stringValue = $this->getStringValue($item->value);
if (null !== $stringValue && '' !== $stringValue) {
$this->addMessageToCatalogue($stringValue, 'messages', $item->getStartLine());
}
}
}

private function isFormType(Node $node): bool
{
if ($node instanceof Node\Stmt\Class_) {
if ($node->extends !== null) {
if ($node->extends->isFullyQualified()) {
if ($node->extends->name === AbstractType::class) {
$this->isFormType = true;
}
}
}
}

return $this->isFormType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
use Symfony\Component\Translation\Extractor\Visitor\ConstraintVisitor;
use Symfony\Component\Translation\Extractor\Visitor\FormTypeVisitor;
use Symfony\Component\Validator\Constraints\IsbnValidator;
use Symfony\Component\Validator\Constraints\LengthValidator;
use Symfony\Component\Validator\Constraints\NotBlankValidator;
Expand Down
Loading
Loading