From 2d1c72174f378bd57edc8825a0d3e97d9ca28433 Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Mon, 24 Feb 2025 12:26:29 +0100 Subject: [PATCH 01/12] feat(PhpAstExtractor): Add extraction of FormType explicit labels --- .../Resources/config/translation.php | 4 + .../DependencyInjection/TranslatorPass.php | 66 ++++++++++++++--- .../Translation/Extractor/PhpAstExtractor.php | 2 +- .../Extractor/Visitor/AbstractVisitor.php | 27 ++++--- .../Extractor/Visitor/FormTrait.php | 38 ++++++++++ .../Extractor/Visitor/FormTypeVisitor.php | 73 +++++++++++++++++++ .../TranslatorPassTest.php | 17 +++++ .../Tests/Extractor/PhpAstExtractorTest.php | 49 ++++++++++--- .../Fixtures/extractor-ast/form-types.php | 72 ++++++++++++++++++ 9 files changed, 312 insertions(+), 36 deletions(-) create mode 100644 src/Symfony/Component/Translation/Extractor/Visitor/FormTrait.php create mode 100644 src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-ast/form-types.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php index a450e6894cc8a..787071e749ff8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php @@ -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; @@ -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') diff --git a/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php b/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php index 948f378d6637a..e2cbec804cc99 100644 --- a/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php +++ b/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php @@ -24,6 +24,14 @@ public function process(ContainerBuilder $container): void return; } + $this->processLoadersAndReaders($container); + $this->processExtractorFormTypeVisitor($container); + $this->processExtractorConstraintVisitor($container); + $this->processTwigPaths($container); + } + + private function processLoadersAndReaders(ContainerBuilder $container): void + { $loaders = []; $loaderRefs = []; foreach ($container->findTaggedServiceIds('translation.loader', true) as $id => $attributes) { @@ -48,29 +56,62 @@ public function process(ContainerBuilder $container): void ->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs)) ->replaceArgument(3, $loaders) ; + } + + // TO BE DELETED + private function processExtractorFormTypeVisitor(ContainerBuilder $container): void + { + if (!$container->hasDefinition('translation.extractor.visitor.form_type')) { + return; + } - if ($container->hasDefinition('validator') && $container->hasDefinition('translation.extractor.visitor.constraint')) { - $constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint'); - $constraintClassNames = []; + $formTypeVisitorDefinition = $container->getDefinition('translation.extractor.visitor.form_type'); + $formTypeClassNames = []; - 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)); + foreach ($container->getDefinitions() as $definition) { + if (!$definition->hasTag('form.type')) { + continue; } - $constraintVisitorDefinition->setArgument(0, $constraintClassNames); + // 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 + $formTypeClassNames[] = str_replace('Type', '', substr(strrchr($className, '\\'), 1)); + } + + $formTypeVisitorDefinition->setArgument(0, $formTypeClassNames); + } + + private function processExtractorConstraintVisitor(ContainerBuilder $container): void + { + if (!$container->hasDefinition('validator') || !$container->hasDefinition('translation.extractor.visitor.constraint')) { + return; + } + + $constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint'); + $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')); @@ -79,6 +120,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')); diff --git a/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php b/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php index a5375f480d2ee..34244874baad8 100644 --- a/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php +++ b/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php @@ -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 diff --git a/src/Symfony/Component/Translation/Extractor/Visitor/AbstractVisitor.php b/src/Symfony/Component/Translation/Extractor/Visitor/AbstractVisitor.php index c336896169a8b..67c2da9618c3a 100644 --- a/src/Symfony/Component/Translation/Extractor/Visitor/AbstractVisitor.php +++ b/src/Symfony/Component/Translation/Extractor/Visitor/AbstractVisitor.php @@ -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 = []; @@ -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_) { @@ -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; + } } diff --git a/src/Symfony/Component/Translation/Extractor/Visitor/FormTrait.php b/src/Symfony/Component/Translation/Extractor/Visitor/FormTrait.php new file mode 100644 index 0000000000000..0027d91de14a6 --- /dev/null +++ b/src/Symfony/Component/Translation/Extractor/Visitor/FormTrait.php @@ -0,0 +1,38 @@ + + * + * 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 Symfony\Component\Form\AbstractType; + +trait FormTrait +{ + /** + * Stores whether the current class is a form type across visits of all children nodes. + */ + private bool $isFormType = false; + + 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; + } +} diff --git a/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php b/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php new file mode 100644 index 0000000000000..33db299befe7d --- /dev/null +++ b/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php @@ -0,0 +1,73 @@ + + * + * 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; + +/** + * @author Mathieu Santostefano + * + * Code mostly comes from https://github.com/php-translation/extractor/blob/master/src/Visitor/Php/Symfony/ + */ +final class FormTypeVisitor extends AbstractVisitor implements NodeVisitor +{ + use FormTrait; + + public function __construct( + // to be deleted + private readonly array $formTypeClassNames = [], + ) { + } + + 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 (containing explicit labels) + if ($node instanceof Node\Expr\Array_) { + $this->visitArray($node); + } + + return null; + } + + public function leaveNode(Node $node): ?Node + { + return null; + } + + public function afterTraverse(array $nodes): ?Node + { + return null; + } + + private function visitArray(Node\Expr\Array_ $node): void + { + foreach ($node->items as $item) { + if ($item->key instanceof Node\Scalar\String_ && 'label' === $item->key->value) { + // If the label 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()); + } + } + } + } +} diff --git a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php index 1efd932be9014..72cd9f2f76ec9 100644 --- a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php +++ b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php @@ -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; @@ -147,4 +149,19 @@ public function testValidPhpAstExtractorConstraintVisitorArguments() $this->assertSame(['NotBlank', 'Isbn', 'Length', 'Time'], $constraintVisitor->getArgument(0)); } + + public function testValidPhpAstExtractorFormTypeVisitorArguments() + { + $container = new ContainerBuilder(); + $container->register('translator.default') + ->setArguments([null, null, null, null]); + $formTypeVisitor = $container->register('translation.extractor.visitor.form_type', FormTypeVisitor::class); + $container->register('form.type.text', TextType::class) + ->addTag('form.type'); + + $pass = new TranslatorPass(); + $pass->process($container); + + $this->assertSame(['Text'], $formTypeVisitor->getArgument(0)); + } } diff --git a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php index 658164a37150d..44ef032227a6d 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; 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\MessageCatalogue; @@ -34,7 +35,10 @@ public function testExtraction(iterable|string $resource) 'NotBlank', 'Isbn', 'Length', - ], new TranslatableMessageVisitor()), + ]), + new FormTypeVisitor([ + 'ExplicitLabelType', + ]), ]); $extractor->setPrefix('prefix'); $catalogue = new MessageCatalogue('en'); @@ -107,6 +111,16 @@ public function testExtraction(iterable|string $resource) 'mix-named-arguments' => 'prefixmix-named-arguments', 'mix-named-arguments-locale' => 'prefixmix-named-arguments-locale', 'mix-named-arguments-without-domain' => 'prefixmix-named-arguments-without-domain', + 'label.foo1' => 'prefixlabel.foo1', + 'label.find1' => 'prefixlabel.find1', + 'find2' => 'prefixfind2', + 'FOUND3' => 'prefixFOUND3', + 'label.find4' => 'prefixlabel.find4', + 'label.find5' => 'prefixlabel.find5', + 'find6' => 'prefixfind6', + 'bigger_find7' => 'prefixbigger_find7', + 'camelFind8' => 'prefixcamelFind8', + 'label.find9' => 'prefixlabel.find9', ], 'not_messages' => [ 'translatable other-domain-test-no-params-short-array' => 'prefixtranslatable other-domain-test-no-params-short-array', @@ -195,7 +209,10 @@ public function testExtractionFromIndentedHeredocNowdoc() 'NotBlank', 'Isbn', 'Length', - ], new TranslatableMessageVisitor()), + ]), + new FormTypeVisitor([ + 'ExplicitLabelType', + ]), ]); $extractor->setPrefix('prefix'); $extractor->extract(__DIR__.'/../Fixtures/extractor-7.3/translation.html.php', $catalogue); @@ -210,8 +227,18 @@ public function testExtractionFromIndentedHeredocNowdoc() $this->assertEquals($expectedCatalogue, $catalogue->all()); } - public static function resourcesProvider(): array + public static function resourcesProvider(): \Generator { + $fixtureFiles = [ + 'translatable.html.php', + 'translatable-fqn.html.php', + 'translatable-short.html.php', + 'translatable-short-fqn.html.php', + 'translation.html.php', + 'validator-constraints.php', + 'form-types.php', + ]; + $directory = __DIR__.'/../Fixtures/extractor-ast/'; $phpFiles = []; $splFiles = []; @@ -219,19 +246,17 @@ public static function resourcesProvider(): array if ($fileInfo->isDot()) { continue; } - if (\in_array($fileInfo->getBasename(), ['translatable.html.php', 'translatable-fqn.html.php', 'translatable-short.html.php', 'translatable-short-fqn.html.php', 'translation.html.php', 'validator-constraints.php'], true)) { + if (\in_array($fileInfo->getBasename(), $fixtureFiles, true)) { $phpFiles[] = $fileInfo->getPathname(); } $splFiles[] = $fileInfo->getFileInfo(); } - return [ - [$directory], - [$phpFiles], - [glob($directory.'*')], - [$splFiles], - [new \ArrayObject(glob($directory.'*'))], - [new \ArrayObject($splFiles)], - ]; + yield 'directory' => [$directory]; + yield 'phpFiles' => [$phpFiles]; + yield 'glob' => [glob($directory.'*')]; + yield 'splFiles' => [$splFiles]; + yield 'ArrayObject_glob' => [new \ArrayObject(glob($directory.'*'))]; + yield 'ArrayObject_splFiles' => [new \ArrayObject($splFiles)]; } } diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-ast/form-types.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-ast/form-types.php new file mode 100644 index 0000000000000..8c1b87b24a48c --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-ast/form-types.php @@ -0,0 +1,72 @@ +This template is used for translation message extraction tests +add('foo1', null, [ + 'label' => 'label.foo1' + ]); + } +} + +class ExplicitLabelType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $var = "something"; + $builder->add('find1', null, [ + 'label' => 'label.find1' + ]); + $builder + ->add('find2', null, array( + 'label' => 'find2' + )) + ->add('field_longer_name3', null, [ + 'label' => 'FOUND3' + ]) + ->add('skip1', null, [ + 'label' => $var, // shouldn't be picked up + 'somethingelse' => 'skipthis', + ]) + ->add('skip2', null, [ + 'label' => PHP_OS, // constant shouldn't work + ]) + ->add('skip3', null, [ + 'label' // value label, shouldn't be picked up + ]) + ->add('skip4', null, [ + 'label' => 'something '.$var // string+var concatenation, shouldn't be picked up + ]) + ; + + // add label in variable should be found + $opts = ['label'=>'label.find4']; + $builder->add('find4', null, $opts); + + // empty label should be skipped + $builder->add('skip5', null, ['label'=>'']); + + // collection test + $builder->add('find5', CollectionType::class, [ + 'options' => [ + 'label' => 'label.find5', + ], + ]); + + // implicit labels should be found + $builder->add('find6'); + $builder->add('bigger_find7'); + $builder->add('camelFind8'); + $builder->add('skip6'.$var); + $builder->add('skip7', null, ['label'=>'label.find9']); + } +} From 2f93b0274198618b689fdd8570efc7d69cb69bbf Mon Sep 17 00:00:00 2001 From: "hubert.lenoir" Date: Wed, 12 Mar 2025 09:31:18 +0100 Subject: [PATCH 02/12] rework tests --- .../Tests/Extractor/PhpAstExtractorTest.php | 220 +----------------- .../Extractor/Visitor/AbstractVisitorTest.php | 27 +++ .../Visitor/ConstraintVisitorTest.php | 47 ++++ .../Extractor/Visitor/FormTypeVisitorTest.php | 46 ++++ .../Visitor/TransMethodVisitorTest.php | 115 +++++++++ .../TranslatableMessageVisitorTest.php | 81 +++++++ .../validator-constraints.php | 40 ++++ .../extract-files/resource.format.engine | 0 .../this.is.a.template.format.engine | 0 .../extract-files/translation.html.php | 1 + .../form-type-visitor/form-type.php | 72 ++++++ .../translatable-short-fqn.html.php | 47 ++++ .../translatable-short.html.php | 47 ++++ .../translation-73.html.php | 13 ++ .../trans-method-visitor/translation.html.php | 70 ++++++ .../translatable-fqn.html.php | 47 ++++ .../translatable.html.php | 47 ++++ 17 files changed, 710 insertions(+), 210 deletions(-) create mode 100644 src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php create mode 100644 src/Symfony/Component/Translation/Tests/Extractor/Visitor/ConstraintVisitorTest.php create mode 100644 src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php create mode 100644 src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php create mode 100644 src/Symfony/Component/Translation/Tests/Extractor/Visitor/TranslatableMessageVisitorTest.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/constraint-visitor/validator-constraints.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/resource.format.engine create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/this.is.a.template.format.engine create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translation.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-short-fqn.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-short.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translation-73.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translation.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/translatable-message-visitor/translatable-fqn.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/translatable-message-visitor/translatable.html.php diff --git a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php index 37564e10def1d..13e02f87aad73 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php @@ -13,246 +13,46 @@ use PHPUnit\Framework\TestCase; 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\MessageCatalogue; final class PhpAstExtractorTest extends TestCase { - public const OTHER_DOMAIN = 'not_messages'; + private const FIXTURES_FOLDER = __DIR__ . '/../Fixtures/extractor-php-ast/extract-files/'; /** * @dataProvider resourcesProvider */ - public function testExtraction(iterable|string $resource) + public function testExtractFiles(iterable|string $resource) { - $extractor = new PhpAstExtractor([ - new TransMethodVisitor(), - new TranslatableMessageVisitor(), - new ConstraintVisitor([ - 'NotBlank', - 'Isbn', - 'Length', - ]), - new FormTypeVisitor(), - ]); - $extractor->setPrefix('prefix'); + $extractor = new PhpAstExtractor([new TransMethodVisitor()]); $catalogue = new MessageCatalogue('en'); $extractor->extract($resource, $catalogue); - $expectedHeredoc = << [ - 'translatable single-quoted key' => 'prefixtranslatable single-quoted key', - 'translatable double-quoted key' => 'prefixtranslatable double-quoted key', - 'translatable heredoc key' => 'prefixtranslatable heredoc key', - 'translatable nowdoc key' => 'prefixtranslatable nowdoc key', - "translatable double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable double-quoted key with whitespace and escaped \$\n\" sequences", - 'translatable single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable single-quoted key with whitespace and nonescaped \$\n\' sequences', - 'translatable single-quoted key with "quote mark at the end"' => 'prefixtranslatable single-quoted key with "quote mark at the end"', - 'translatable '.$expectedHeredoc => 'prefixtranslatable '.$expectedHeredoc, - 'translatable '.$expectedNowdoc => 'prefixtranslatable '.$expectedNowdoc, - 'translatable concatenated message with heredoc and nowdoc' => 'prefixtranslatable concatenated message with heredoc and nowdoc', - 'translatable default domain' => 'prefixtranslatable default domain', - 'translatable-fqn single-quoted key' => 'prefixtranslatable-fqn single-quoted key', - 'translatable-fqn double-quoted key' => 'prefixtranslatable-fqn double-quoted key', - 'translatable-fqn heredoc key' => 'prefixtranslatable-fqn heredoc key', - 'translatable-fqn nowdoc key' => 'prefixtranslatable-fqn nowdoc key', - "translatable-fqn double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-fqn double-quoted key with whitespace and escaped \$\n\" sequences", - 'translatable-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences', - 'translatable-fqn single-quoted key with "quote mark at the end"' => 'prefixtranslatable-fqn single-quoted key with "quote mark at the end"', - 'translatable-fqn '.$expectedHeredoc => 'prefixtranslatable-fqn '.$expectedHeredoc, - 'translatable-fqn '.$expectedNowdoc => 'prefixtranslatable-fqn '.$expectedNowdoc, - 'translatable-fqn concatenated message with heredoc and nowdoc' => 'prefixtranslatable-fqn concatenated message with heredoc and nowdoc', - 'translatable-fqn default domain' => 'prefixtranslatable-fqn default domain', - 'translatable-short single-quoted key' => 'prefixtranslatable-short single-quoted key', - 'translatable-short double-quoted key' => 'prefixtranslatable-short double-quoted key', - 'translatable-short heredoc key' => 'prefixtranslatable-short heredoc key', - 'translatable-short nowdoc key' => 'prefixtranslatable-short nowdoc key', - "translatable-short double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-short double-quoted key with whitespace and escaped \$\n\" sequences", - 'translatable-short single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-short single-quoted key with whitespace and nonescaped \$\n\' sequences', - 'translatable-short single-quoted key with "quote mark at the end"' => 'prefixtranslatable-short single-quoted key with "quote mark at the end"', - 'translatable-short '.$expectedHeredoc => 'prefixtranslatable-short '.$expectedHeredoc, - 'translatable-short '.$expectedNowdoc => 'prefixtranslatable-short '.$expectedNowdoc, - 'translatable-short concatenated message with heredoc and nowdoc' => 'prefixtranslatable-short concatenated message with heredoc and nowdoc', - 'translatable-short default domain' => 'prefixtranslatable-short default domain', - 'translatable-short-fqn single-quoted key' => 'prefixtranslatable-short-fqn single-quoted key', - 'translatable-short-fqn double-quoted key' => 'prefixtranslatable-short-fqn double-quoted key', - 'translatable-short-fqn heredoc key' => 'prefixtranslatable-short-fqn heredoc key', - 'translatable-short-fqn nowdoc key' => 'prefixtranslatable-short-fqn nowdoc key', - "translatable-short-fqn double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-short-fqn double-quoted key with whitespace and escaped \$\n\" sequences", - 'translatable-short-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-short-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences', - 'translatable-short-fqn single-quoted key with "quote mark at the end"' => 'prefixtranslatable-short-fqn single-quoted key with "quote mark at the end"', - 'translatable-short-fqn '.$expectedHeredoc => 'prefixtranslatable-short-fqn '.$expectedHeredoc, - 'translatable-short-fqn '.$expectedNowdoc => 'prefixtranslatable-short-fqn '.$expectedNowdoc, - 'translatable-short-fqn concatenated message with heredoc and nowdoc' => 'prefixtranslatable-short-fqn concatenated message with heredoc and nowdoc', - 'translatable-short-fqn default domain' => 'prefixtranslatable-short-fqn default domain', - 'single-quoted key' => 'prefixsingle-quoted key', - 'double-quoted key' => 'prefixdouble-quoted key', - 'heredoc key' => 'prefixheredoc key', - 'nowdoc key' => 'prefixnowdoc key', - "double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixdouble-quoted key with whitespace and escaped \$\n\" sequences", - 'single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixsingle-quoted key with whitespace and nonescaped \$\n\' sequences', - 'single-quoted key with "quote mark at the end"' => 'prefixsingle-quoted key with "quote mark at the end"', - $expectedHeredoc => 'prefix'.$expectedHeredoc, - $expectedNowdoc => 'prefix'.$expectedNowdoc, - 'concatenated message with heredoc and nowdoc' => 'prefixconcatenated message with heredoc and nowdoc', - 'default domain' => 'prefixdefault domain', - 'mix-named-arguments' => 'prefixmix-named-arguments', - 'mix-named-arguments-locale' => 'prefixmix-named-arguments-locale', - 'mix-named-arguments-without-domain' => 'prefixmix-named-arguments-without-domain', - 'label.foo1' => 'prefixlabel.foo1', - 'label.find1' => 'prefixlabel.find1', - 'find2' => 'prefixfind2', - 'FOUND3' => 'prefixFOUND3', - 'label.find4' => 'prefixlabel.find4', - 'label.find5' => 'prefixlabel.find5', - 'find6' => 'prefixfind6', - 'bigger_find7' => 'prefixbigger_find7', - 'camelFind8' => 'prefixcamelFind8', - 'label.find9' => 'prefixlabel.find9', - ], - 'not_messages' => [ - 'translatable other-domain-test-no-params-short-array' => 'prefixtranslatable other-domain-test-no-params-short-array', - 'translatable other-domain-test-no-params-long-array' => 'prefixtranslatable other-domain-test-no-params-long-array', - 'translatable other-domain-test-params-short-array' => 'prefixtranslatable other-domain-test-params-short-array', - 'translatable other-domain-test-params-long-array' => 'prefixtranslatable other-domain-test-params-long-array', - 'translatable typecast' => 'prefixtranslatable typecast', - 'translatable-fqn other-domain-test-no-params-short-array' => 'prefixtranslatable-fqn other-domain-test-no-params-short-array', - 'translatable-fqn other-domain-test-no-params-long-array' => 'prefixtranslatable-fqn other-domain-test-no-params-long-array', - 'translatable-fqn other-domain-test-params-short-array' => 'prefixtranslatable-fqn other-domain-test-params-short-array', - 'translatable-fqn other-domain-test-params-long-array' => 'prefixtranslatable-fqn other-domain-test-params-long-array', - 'translatable-fqn typecast' => 'prefixtranslatable-fqn typecast', - 'translatable-short other-domain-test-no-params-short-array' => 'prefixtranslatable-short other-domain-test-no-params-short-array', - 'translatable-short other-domain-test-no-params-long-array' => 'prefixtranslatable-short other-domain-test-no-params-long-array', - 'translatable-short other-domain-test-params-short-array' => 'prefixtranslatable-short other-domain-test-params-short-array', - 'translatable-short other-domain-test-params-long-array' => 'prefixtranslatable-short other-domain-test-params-long-array', - 'translatable-short typecast' => 'prefixtranslatable-short typecast', - 'translatable-short-fqn other-domain-test-no-params-short-array' => 'prefixtranslatable-short-fqn other-domain-test-no-params-short-array', - 'translatable-short-fqn other-domain-test-no-params-long-array' => 'prefixtranslatable-short-fqn other-domain-test-no-params-long-array', - 'translatable-short-fqn other-domain-test-params-short-array' => 'prefixtranslatable-short-fqn other-domain-test-params-short-array', - 'translatable-short-fqn other-domain-test-params-long-array' => 'prefixtranslatable-short-fqn other-domain-test-params-long-array', - 'translatable-short-fqn typecast' => 'prefixtranslatable-short-fqn typecast', - 'other-domain-test-no-params-short-array' => 'prefixother-domain-test-no-params-short-array', - 'other-domain-test-no-params-long-array' => 'prefixother-domain-test-no-params-long-array', - 'other-domain-test-params-short-array' => 'prefixother-domain-test-params-short-array', - 'other-domain-test-params-long-array' => 'prefixother-domain-test-params-long-array', - 'typecast' => 'prefixtypecast', - 'ordered-named-arguments-in-trans-method' => 'prefixordered-named-arguments-in-trans-method', - 'disordered-named-arguments-in-trans-method' => 'prefixdisordered-named-arguments-in-trans-method', - 'variable-assignation-inlined-in-trans-method-call1' => 'prefixvariable-assignation-inlined-in-trans-method-call1', - 'variable-assignation-inlined-in-trans-method-call2' => 'prefixvariable-assignation-inlined-in-trans-method-call2', - 'variable-assignation-inlined-in-trans-method-call3' => 'prefixvariable-assignation-inlined-in-trans-method-call3', - 'variable-assignation-inlined-with-named-arguments-in-trans-method' => 'prefixvariable-assignation-inlined-with-named-arguments-in-trans-method', - 'mix-named-arguments-without-parameters' => 'prefixmix-named-arguments-without-parameters', - 'mix-named-arguments-disordered' => 'prefixmix-named-arguments-disordered', - 'const-domain' => 'prefixconst-domain', - ], - 'validators' => [ - 'message-in-constraint-attribute' => 'prefixmessage-in-constraint-attribute', - // 'custom Isbn message from attribute' => 'prefixcustom Isbn message from attribute', - 'custom Isbn message from attribute with options as array' => 'prefixcustom Isbn message from attribute with options as array', - 'custom Length exact message from attribute from named argument' => 'prefixcustom Length exact message from attribute from named argument', - 'custom Length exact message from attribute from named argument 1/2' => 'prefixcustom Length exact message from attribute from named argument 1/2', - 'custom Length min message from attribute from named argument 2/2' => 'prefixcustom Length min message from attribute from named argument 2/2', - // 'custom Isbn message' => 'prefixcustom Isbn message', - 'custom Isbn message with options as array' => 'prefixcustom Isbn message with options as array', - 'custom Isbn message from named argument' => 'prefixcustom Isbn message from named argument', - 'custom Length exact message from named argument' => 'prefixcustom Length exact message from named argument', - 'custom Length exact message from named argument 1/2' => 'prefixcustom Length exact message from named argument 1/2', - 'custom Length min message from named argument 2/2' => 'prefixcustom Length min message from named argument 2/2', - ], - ]; - $actualCatalogue = $catalogue->all(); - - $this->assertEquals($expectedCatalogue, $actualCatalogue); - - $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translatable.html.php'; - $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('translatable single-quoted key')); - $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('translatable other-domain-test-no-params-short-array', 'not_messages')); - - $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translatable-fqn.html.php'; - $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('translatable-fqn single-quoted key')); - $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('translatable-fqn other-domain-test-no-params-short-array', 'not_messages')); - - $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translatable-short.html.php'; - $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('translatable-short single-quoted key')); - $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('translatable-short other-domain-test-no-params-short-array', 'not_messages')); - - $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translatable-short-fqn.html.php'; - $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('translatable-short-fqn single-quoted key')); - $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('translatable-short-fqn other-domain-test-no-params-short-array', 'not_messages')); - - $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translation.html.php'; - $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('single-quoted key')); - $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('other-domain-test-no-params-short-array', 'not_messages')); - } - - public function testExtractionFromIndentedHeredocNowdoc() - { - $catalogue = new MessageCatalogue('en'); - - $extractor = new PhpAstExtractor([ - new TransMethodVisitor(), - new TranslatableMessageVisitor(), - new ConstraintVisitor([ - 'NotBlank', - 'Isbn', - 'Length', - ]), - new FormTypeVisitor(), - ]); - $extractor->setPrefix('prefix'); - $extractor->extract(__DIR__.'/../Fixtures/extractor-7.3/translation.html.php', $catalogue); - - $expectedCatalogue = [ - 'messages' => [ - "heredoc\nindented\n further" => "prefixheredoc\nindented\n further", - "nowdoc\nindented\n further" => "prefixnowdoc\nindented\n further", - ], - ]; - - $this->assertEquals($expectedCatalogue, $catalogue->all()); + $this->assertEquals(['messages' => ['example' => 'example']], $catalogue->all()); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER.'translation.html.php:1']], $catalogue->getMetadata('example')); } public static function resourcesProvider(): \Generator { - $fixtureFiles = [ - 'translatable.html.php', - 'translatable-fqn.html.php', - 'translatable-short.html.php', - 'translatable-short-fqn.html.php', - 'translation.html.php', - 'validator-constraints.php', - 'form-types.php', - ]; - - $directory = __DIR__.'/../Fixtures/extractor-ast/'; $phpFiles = []; $splFiles = []; - foreach (new \DirectoryIterator($directory) as $fileInfo) { + foreach (new \DirectoryIterator(self::FIXTURES_FOLDER) as $fileInfo) { if ($fileInfo->isDot()) { continue; } - if (\in_array($fileInfo->getBasename(), $fixtureFiles, true)) { + if ('php' === $fileInfo->getExtension()) { $phpFiles[] = $fileInfo->getPathname(); } $splFiles[] = $fileInfo->getFileInfo(); } - yield 'directory' => [$directory]; + yield 'directory' => [self::FIXTURES_FOLDER]; yield 'phpFiles' => [$phpFiles]; - yield 'glob' => [glob($directory.'*')]; + yield 'glob' => [glob(self::FIXTURES_FOLDER.'*')]; yield 'splFiles' => [$splFiles]; - yield 'ArrayObject_glob' => [new \ArrayObject(glob($directory.'*'))]; + yield 'ArrayObject_glob' => [new \ArrayObject(glob(self::FIXTURES_FOLDER.'*'))]; yield 'ArrayObject_splFiles' => [new \ArrayObject($splFiles)]; } } diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php new file mode 100644 index 0000000000000..3efbe559a86b4 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php @@ -0,0 +1,27 @@ +getVisitors()]); + $extractor->setPrefix('prefix'); + $catalogue = new MessageCatalogue('en'); + + $extractor->extract($this->getResource(), $catalogue); + + $this->assertCatalogue($catalogue); + + } +} diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/ConstraintVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/ConstraintVisitorTest.php new file mode 100644 index 0000000000000..023e8a2c71024 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/ConstraintVisitorTest.php @@ -0,0 +1,47 @@ +assertEquals( + [ + 'validators' => [ + 'message-in-constraint-attribute' => 'prefixmessage-in-constraint-attribute', + // 'custom Isbn message from attribute' => 'prefixcustom Isbn message from attribute', + 'custom Isbn message from attribute with options as array' => 'prefixcustom Isbn message from attribute with options as array', + 'custom Length exact message from attribute from named argument' => 'prefixcustom Length exact message from attribute from named argument', + 'custom Length exact message from attribute from named argument 1/2' => 'prefixcustom Length exact message from attribute from named argument 1/2', + 'custom Length min message from attribute from named argument 2/2' => 'prefixcustom Length min message from attribute from named argument 2/2', + // 'custom Isbn message' => 'prefixcustom Isbn message', + 'custom Isbn message with options as array' => 'prefixcustom Isbn message with options as array', + 'custom Isbn message from named argument' => 'prefixcustom Isbn message from named argument', + 'custom Length exact message from named argument' => 'prefixcustom Length exact message from named argument', + 'custom Length exact message from named argument 1/2' => 'prefixcustom Length exact message from named argument 1/2', + 'custom Length min message from named argument 2/2' => 'prefixcustom Length min message from named argument 2/2', + ], + ], + $catalogue->all(), + ); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'validator-constraints.php:8']], $catalogue->getMetadata('message-in-constraint-attribute', 'validators')); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php new file mode 100644 index 0000000000000..4a6b33afea7c5 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php @@ -0,0 +1,46 @@ +assertEquals( + [ + 'messages' => [ + 'label.foo1' => 'prefixlabel.foo1', + 'label.find1' => 'prefixlabel.find1', + 'find2' => 'prefixfind2', + 'FOUND3' => 'prefixFOUND3', + 'label.find4' => 'prefixlabel.find4', + 'label.find5' => 'prefixlabel.find5', + 'find6' => 'prefixfind6', + 'bigger_find7' => 'prefixbigger_find7', + 'camelFind8' => 'prefixcamelFind8', + 'label.find9' => 'prefixlabel.find9', + ], + ], + $catalogue->all(), + ); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'form-type.php:27']], $catalogue->getMetadata('label.find1')); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php new file mode 100644 index 0000000000000..e9193d0c1b911 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php @@ -0,0 +1,115 @@ +assertEquals( + [ + 'messages' => [ + 'single-quoted key' => 'prefixsingle-quoted key', + 'double-quoted key' => 'prefixdouble-quoted key', + 'heredoc key' => 'prefixheredoc key', + 'nowdoc key' => 'prefixnowdoc key', + "double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixdouble-quoted key with whitespace and escaped \$\n\" sequences", + 'single-quoted key with whitespace and nonescaped \\$\\n\' sequences' => 'prefixsingle-quoted key with whitespace and nonescaped \\$\\n\' sequences', + $expectedHeredoc => 'prefix'.$expectedHeredoc, + $expectedNowdoc => 'prefix'.$expectedNowdoc, + 'single-quoted key with "quote mark at the end"' => 'prefixsingle-quoted key with "quote mark at the end"', + 'concatenated message with heredoc and nowdoc' => 'prefixconcatenated message with heredoc and nowdoc', + 'default domain' => 'prefixdefault domain', + 'mix-named-arguments' => 'prefixmix-named-arguments', + 'mix-named-arguments-locale' => 'prefixmix-named-arguments-locale', + 'mix-named-arguments-without-domain' => 'prefixmix-named-arguments-without-domain', + "heredoc\nindented\n further" => "prefixheredoc\nindented\n further", + "nowdoc\nindented\n further" => "prefixnowdoc\nindented\n further", + 'translatable-short single-quoted key' => 'prefixtranslatable-short single-quoted key', + 'translatable-short double-quoted key' => 'prefixtranslatable-short double-quoted key', + 'translatable-short heredoc key' => 'prefixtranslatable-short heredoc key', + 'translatable-short nowdoc key' => 'prefixtranslatable-short nowdoc key', + "translatable-short double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-short double-quoted key with whitespace and escaped \$\n\" sequences", + 'translatable-short single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-short single-quoted key with whitespace and nonescaped \$\n\' sequences', + 'translatable-short single-quoted key with "quote mark at the end"' => 'prefixtranslatable-short single-quoted key with "quote mark at the end"', + 'translatable-short '.$expectedHeredoc => 'prefixtranslatable-short '.$expectedHeredoc, + 'translatable-short '.$expectedNowdoc => 'prefixtranslatable-short '.$expectedNowdoc, + 'translatable-short concatenated message with heredoc and nowdoc' => 'prefixtranslatable-short concatenated message with heredoc and nowdoc', + 'translatable-short default domain' => 'prefixtranslatable-short default domain', + 'translatable-short-fqn single-quoted key' => 'prefixtranslatable-short-fqn single-quoted key', + 'translatable-short-fqn double-quoted key' => 'prefixtranslatable-short-fqn double-quoted key', + 'translatable-short-fqn heredoc key' => 'prefixtranslatable-short-fqn heredoc key', + 'translatable-short-fqn nowdoc key' => 'prefixtranslatable-short-fqn nowdoc key', + "translatable-short-fqn double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-short-fqn double-quoted key with whitespace and escaped \$\n\" sequences", + 'translatable-short-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-short-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences', + 'translatable-short-fqn single-quoted key with "quote mark at the end"' => 'prefixtranslatable-short-fqn single-quoted key with "quote mark at the end"', + 'translatable-short-fqn '.$expectedHeredoc => 'prefixtranslatable-short-fqn '.$expectedHeredoc, + 'translatable-short-fqn '.$expectedNowdoc => 'prefixtranslatable-short-fqn '.$expectedNowdoc, + 'translatable-short-fqn concatenated message with heredoc and nowdoc' => 'prefixtranslatable-short-fqn concatenated message with heredoc and nowdoc', + 'translatable-short-fqn default domain' => 'prefixtranslatable-short-fqn default domain', + ], + 'not_messages' => [ + 'other-domain-test-no-params-short-array' => 'prefixother-domain-test-no-params-short-array', + 'other-domain-test-no-params-long-array' => 'prefixother-domain-test-no-params-long-array', + 'other-domain-test-params-short-array' => 'prefixother-domain-test-params-short-array', + 'other-domain-test-params-long-array' => 'prefixother-domain-test-params-long-array', + 'typecast' => 'prefixtypecast', + 'ordered-named-arguments-in-trans-method' => 'prefixordered-named-arguments-in-trans-method', + 'disordered-named-arguments-in-trans-method' => 'prefixdisordered-named-arguments-in-trans-method', + 'variable-assignation-inlined-in-trans-method-call1' => 'prefixvariable-assignation-inlined-in-trans-method-call1', + 'variable-assignation-inlined-in-trans-method-call2' => 'prefixvariable-assignation-inlined-in-trans-method-call2', + 'variable-assignation-inlined-in-trans-method-call3' => 'prefixvariable-assignation-inlined-in-trans-method-call3', + 'variable-assignation-inlined-with-named-arguments-in-trans-method' => 'prefixvariable-assignation-inlined-with-named-arguments-in-trans-method', + 'mix-named-arguments-without-parameters' => 'prefixmix-named-arguments-without-parameters', + 'mix-named-arguments-disordered' => 'prefixmix-named-arguments-disordered', + 'const-domain' => 'prefixconst-domain', + 'translatable-short other-domain-test-no-params-short-array' => 'prefixtranslatable-short other-domain-test-no-params-short-array', + 'translatable-short other-domain-test-no-params-long-array' => 'prefixtranslatable-short other-domain-test-no-params-long-array', + 'translatable-short other-domain-test-params-short-array' => 'prefixtranslatable-short other-domain-test-params-short-array', + 'translatable-short other-domain-test-params-long-array' => 'prefixtranslatable-short other-domain-test-params-long-array', + 'translatable-short typecast' => 'prefixtranslatable-short typecast', + 'translatable-short-fqn other-domain-test-no-params-short-array' => 'prefixtranslatable-short-fqn other-domain-test-no-params-short-array', + 'translatable-short-fqn other-domain-test-no-params-long-array' => 'prefixtranslatable-short-fqn other-domain-test-no-params-long-array', + 'translatable-short-fqn other-domain-test-params-short-array' => 'prefixtranslatable-short-fqn other-domain-test-params-short-array', + 'translatable-short-fqn other-domain-test-params-long-array' => 'prefixtranslatable-short-fqn other-domain-test-params-long-array', + 'translatable-short-fqn typecast' => 'prefixtranslatable-short-fqn typecast', + ], + ], + $catalogue->all(), + ); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translation.html.php:2']], $catalogue->getMetadata('single-quoted key')); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translation.html.php:37']], $catalogue->getMetadata('other-domain-test-no-params-short-array', 'not_messages')); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translation-73.html.php:8']], $catalogue->getMetadata("nowdoc\nindented\n further")); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-short.html.php:2']], $catalogue->getMetadata('translatable-short single-quoted key')); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-short.html.php:37']], $catalogue->getMetadata('translatable-short other-domain-test-no-params-short-array', 'not_messages')); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-short-fqn.html.php:2']], $catalogue->getMetadata('translatable-short-fqn single-quoted key')); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-short-fqn.html.php:37']], $catalogue->getMetadata('translatable-short-fqn other-domain-test-no-params-short-array', 'not_messages')); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TranslatableMessageVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TranslatableMessageVisitorTest.php new file mode 100644 index 0000000000000..e795cda190981 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TranslatableMessageVisitorTest.php @@ -0,0 +1,81 @@ +assertEquals( + [ + 'messages' => [ + 'translatable single-quoted key' => 'prefixtranslatable single-quoted key', + 'translatable double-quoted key' => 'prefixtranslatable double-quoted key', + 'translatable heredoc key' => 'prefixtranslatable heredoc key', + 'translatable nowdoc key' => 'prefixtranslatable nowdoc key', + "translatable double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable double-quoted key with whitespace and escaped \$\n\" sequences", + 'translatable single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable single-quoted key with whitespace and nonescaped \$\n\' sequences', + 'translatable single-quoted key with "quote mark at the end"' => 'prefixtranslatable single-quoted key with "quote mark at the end"', + 'translatable '.$expectedHeredoc => 'prefixtranslatable '.$expectedHeredoc, + 'translatable '.$expectedNowdoc => 'prefixtranslatable '.$expectedNowdoc, + 'translatable concatenated message with heredoc and nowdoc' => 'prefixtranslatable concatenated message with heredoc and nowdoc', + 'translatable default domain' => 'prefixtranslatable default domain', + 'translatable-fqn single-quoted key' => 'prefixtranslatable-fqn single-quoted key', + 'translatable-fqn double-quoted key' => 'prefixtranslatable-fqn double-quoted key', + 'translatable-fqn heredoc key' => 'prefixtranslatable-fqn heredoc key', + 'translatable-fqn nowdoc key' => 'prefixtranslatable-fqn nowdoc key', + "translatable-fqn double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-fqn double-quoted key with whitespace and escaped \$\n\" sequences", + 'translatable-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences', + 'translatable-fqn single-quoted key with "quote mark at the end"' => 'prefixtranslatable-fqn single-quoted key with "quote mark at the end"', + 'translatable-fqn '.$expectedHeredoc => 'prefixtranslatable-fqn '.$expectedHeredoc, + 'translatable-fqn '.$expectedNowdoc => 'prefixtranslatable-fqn '.$expectedNowdoc, + 'translatable-fqn concatenated message with heredoc and nowdoc' => 'prefixtranslatable-fqn concatenated message with heredoc and nowdoc', + 'translatable-fqn default domain' => 'prefixtranslatable-fqn default domain', + ], + 'not_messages' => [ + 'translatable other-domain-test-no-params-short-array' => 'prefixtranslatable other-domain-test-no-params-short-array', + 'translatable other-domain-test-no-params-long-array' => 'prefixtranslatable other-domain-test-no-params-long-array', + 'translatable other-domain-test-params-short-array' => 'prefixtranslatable other-domain-test-params-short-array', + 'translatable other-domain-test-params-long-array' => 'prefixtranslatable other-domain-test-params-long-array', + 'translatable typecast' => 'prefixtranslatable typecast', + 'translatable-fqn other-domain-test-no-params-short-array' => 'prefixtranslatable-fqn other-domain-test-no-params-short-array', + 'translatable-fqn other-domain-test-no-params-long-array' => 'prefixtranslatable-fqn other-domain-test-no-params-long-array', + 'translatable-fqn other-domain-test-params-short-array' => 'prefixtranslatable-fqn other-domain-test-params-short-array', + 'translatable-fqn other-domain-test-params-long-array' => 'prefixtranslatable-fqn other-domain-test-params-long-array', + 'translatable-fqn typecast' => 'prefixtranslatable-fqn typecast', + ], + ], + $catalogue->all(), + ); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable.html.php:2']], $catalogue->getMetadata('translatable single-quoted key')); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable.html.php:37']], $catalogue->getMetadata('translatable other-domain-test-no-params-short-array', 'not_messages')); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-fqn.html.php:2']], $catalogue->getMetadata('translatable-fqn single-quoted key')); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-fqn.html.php:37']], $catalogue->getMetadata('translatable-fqn other-domain-test-no-params-short-array', 'not_messages')); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/constraint-visitor/validator-constraints.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/constraint-visitor/validator-constraints.php new file mode 100644 index 0000000000000..091d251b63a0d --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/constraint-visitor/validator-constraints.php @@ -0,0 +1,40 @@ +This template is used for translation message extraction tests + 'isbn10', + 'message' => 'custom Isbn message from attribute with options as array', + ])] + public string $isbn2; +} + +class Foo2 +{ + public function index() + { + $constraint1 = new Assert\Isbn('isbn10', 'custom Isbn message'); // no way to handle those arguments (not named, not in associative array). + $constraint2 = new Assert\Isbn([ + 'type' => 'isbn10', + 'message' => 'custom Isbn message with options as array', + ]); + $constraint3 = new Assert\Isbn(message: 'custom Isbn message from named argument'); + $constraint4 = new Assert\Length(exactMessage: 'custom Length exact message from named argument'); + $constraint5 = new Assert\Length(exactMessage: 'custom Length exact message from named argument 1/2', minMessage: 'custom Length min message from named argument 2/2'); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/resource.format.engine b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/resource.format.engine new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/this.is.a.template.format.engine b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/this.is.a.template.format.engine new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translation.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translation.html.php new file mode 100644 index 0000000000000..97cb0f024ac3c --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translation.html.php @@ -0,0 +1 @@ +trans('example'); ?> diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php new file mode 100644 index 0000000000000..8c1b87b24a48c --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php @@ -0,0 +1,72 @@ +This template is used for translation message extraction tests +add('foo1', null, [ + 'label' => 'label.foo1' + ]); + } +} + +class ExplicitLabelType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $var = "something"; + $builder->add('find1', null, [ + 'label' => 'label.find1' + ]); + $builder + ->add('find2', null, array( + 'label' => 'find2' + )) + ->add('field_longer_name3', null, [ + 'label' => 'FOUND3' + ]) + ->add('skip1', null, [ + 'label' => $var, // shouldn't be picked up + 'somethingelse' => 'skipthis', + ]) + ->add('skip2', null, [ + 'label' => PHP_OS, // constant shouldn't work + ]) + ->add('skip3', null, [ + 'label' // value label, shouldn't be picked up + ]) + ->add('skip4', null, [ + 'label' => 'something '.$var // string+var concatenation, shouldn't be picked up + ]) + ; + + // add label in variable should be found + $opts = ['label'=>'label.find4']; + $builder->add('find4', null, $opts); + + // empty label should be skipped + $builder->add('skip5', null, ['label'=>'']); + + // collection test + $builder->add('find5', CollectionType::class, [ + 'options' => [ + 'label' => 'label.find5', + ], + ]); + + // implicit labels should be found + $builder->add('find6'); + $builder->add('bigger_find7'); + $builder->add('camelFind8'); + $builder->add('skip6'.$var); + $builder->add('skip7', null, ['label'=>'label.find9']); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-short-fqn.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-short-fqn.html.php new file mode 100644 index 0000000000000..c02e0b51be028 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-short-fqn.html.php @@ -0,0 +1,47 @@ +This template is used for translation message extraction tests + + + + + + + + + + + + + + + + + + 'bar'], 'not_messages'); ?> + + 'bar'], 'not_messages'); ?> + + (int) '123'], 'not_messages'); ?> + + diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-short.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-short.html.php new file mode 100644 index 0000000000000..d8842b97f1ada --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-short.html.php @@ -0,0 +1,47 @@ +This template is used for translation message extraction tests + + + + + + + + + + + + + + + + + + 'bar'], 'not_messages'); ?> + + 'bar'], 'not_messages'); ?> + + (int) '123'], 'not_messages'); ?> + + diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translation-73.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translation-73.html.php new file mode 100644 index 0000000000000..35ffe8812ed66 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translation-73.html.php @@ -0,0 +1,13 @@ +This template is used for translation message extraction tests +trans(<< +trans(<<<'EOF' + nowdoc + indented + further + EOF +); ?> diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translation.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translation.html.php new file mode 100644 index 0000000000000..1bba72115e235 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translation.html.php @@ -0,0 +1,70 @@ +This template is used for translation message extraction tests +trans('single-quoted key'); ?> +trans('double-quoted key'); ?> +trans(<< +trans(<<<'EOF' +nowdoc key +EOF +); ?> +trans( + "double-quoted key with whitespace and escaped \$\n\" sequences" +); ?> +trans( + 'single-quoted key with whitespace and nonescaped \$\n\' sequences' +); ?> +trans(<< +trans(<<<'EOF' +nowdoc key with whitespace and nonescaped \$\n sequences +EOF +); ?> + +trans('single-quoted key with "quote mark at the end"'); ?> + +trans('concatenated'.' message'.<< + +trans('other-domain-test-no-params-short-array', [], 'not_messages'); ?> + +trans('other-domain-test-no-params-long-array', [], 'not_messages'); ?> + +trans('other-domain-test-params-short-array', ['foo' => 'bar'], 'not_messages'); ?> + +trans('other-domain-test-params-long-array', ['foo' => 'bar'], 'not_messages'); ?> + +trans('typecast', ['a' => (int) '123'], 'not_messages'); ?> + +trans('default domain', [], null); ?> + +trans(id: 'ordered-named-arguments-in-trans-method', parameters: [], domain: 'not_messages'); ?> +trans(domain: 'not_messages', id: 'disordered-named-arguments-in-trans-method', parameters: []); ?> + +trans($key = 'variable-assignation-inlined-in-trans-method-call1', $parameters = [], $domain = 'not_messages'); ?> +trans('variable-assignation-inlined-in-trans-method-call2', $parameters = [], $domain = 'not_messages'); ?> +trans('variable-assignation-inlined-in-trans-method-call3', [], $domain = 'not_messages'); ?> + +trans(domain: $domain = 'not_messages', id: $key = 'variable-assignation-inlined-with-named-arguments-in-trans-method', parameters: $parameters = []); ?> + +trans('mix-named-arguments', parameters: ['foo' => 'bar']); ?> +trans('mix-named-arguments-locale', parameters: ['foo' => 'bar'], locale: 'de'); ?> +trans('mix-named-arguments-without-domain', parameters: ['foo' => 'bar']); ?> +trans('mix-named-arguments-without-parameters', domain: 'not_messages'); ?> +trans('mix-named-arguments-disordered', domain: 'not_messages', parameters: []); ?> + +trans(...); // should not fail ?> + +trans('const-domain', [], TransMethodVisitorTest::OTHER_DOMAIN); +?> + diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/translatable-message-visitor/translatable-fqn.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/translatable-message-visitor/translatable-fqn.html.php new file mode 100644 index 0000000000000..87a64c42f1eec --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/translatable-message-visitor/translatable-fqn.html.php @@ -0,0 +1,47 @@ +This template is used for translation message extraction tests + + + + + + + + + + + + + + + + + + 'bar'], 'not_messages'); ?> + + 'bar'], 'not_messages'); ?> + + (int) '123'], 'not_messages'); ?> + + diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/translatable-message-visitor/translatable.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/translatable-message-visitor/translatable.html.php new file mode 100644 index 0000000000000..828707e26ed02 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/translatable-message-visitor/translatable.html.php @@ -0,0 +1,47 @@ +This template is used for translation message extraction tests + + + + + + + + + + + + + + + + + + 'bar'], 'not_messages'); ?> + + 'bar'], 'not_messages'); ?> + + (int) '123'], 'not_messages'); ?> + + From b056274949ddc827b07a8681a54bdaf11cdf48f0 Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Mon, 24 Feb 2025 14:13:08 +0100 Subject: [PATCH 03/12] feat(PhpAstExtractor): Add extraction of FormType implicit labels --- ....php => TranslationExtractCommandPass.php} | 2 +- .../FrameworkBundle/FrameworkBundle.php | 4 +- .../DependencyInjection/TranslatorPass.php | 25 ---------- .../Extractor/Visitor/FormTrait.php | 38 --------------- .../Extractor/Visitor/FormTypeVisitor.php | 47 ++++++++++++++++--- .../TranslatorPassTest.php | 15 ------ .../Tests/Extractor/PhpAstExtractorTest.php | 8 +--- 7 files changed, 45 insertions(+), 94 deletions(-) rename src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/{TranslationUpdateCommandPass.php => TranslationExtractCommandPass.php} (93%) delete mode 100644 src/Symfony/Component/Translation/Extractor/Visitor/FormTrait.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationExtractCommandPass.php similarity index 93% rename from src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php rename to src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationExtractCommandPass.php index 7542191d0e83e..9339c6035b6f1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationExtractCommandPass.php @@ -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 { diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index faf2841f40105..f8934c508a54a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -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; @@ -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')) { diff --git a/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php b/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php index e2cbec804cc99..64fa93073df23 100644 --- a/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php +++ b/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php @@ -25,7 +25,6 @@ public function process(ContainerBuilder $container): void } $this->processLoadersAndReaders($container); - $this->processExtractorFormTypeVisitor($container); $this->processExtractorConstraintVisitor($container); $this->processTwigPaths($container); } @@ -58,30 +57,6 @@ private function processLoadersAndReaders(ContainerBuilder $container): void ; } - // TO BE DELETED - private function processExtractorFormTypeVisitor(ContainerBuilder $container): void - { - if (!$container->hasDefinition('translation.extractor.visitor.form_type')) { - return; - } - - $formTypeVisitorDefinition = $container->getDefinition('translation.extractor.visitor.form_type'); - $formTypeClassNames = []; - - foreach ($container->getDefinitions() as $definition) { - if (!$definition->hasTag('form.type')) { - 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 - $formTypeClassNames[] = str_replace('Type', '', substr(strrchr($className, '\\'), 1)); - } - - $formTypeVisitorDefinition->setArgument(0, $formTypeClassNames); - } - private function processExtractorConstraintVisitor(ContainerBuilder $container): void { if (!$container->hasDefinition('validator') || !$container->hasDefinition('translation.extractor.visitor.constraint')) { diff --git a/src/Symfony/Component/Translation/Extractor/Visitor/FormTrait.php b/src/Symfony/Component/Translation/Extractor/Visitor/FormTrait.php deleted file mode 100644 index 0027d91de14a6..0000000000000 --- a/src/Symfony/Component/Translation/Extractor/Visitor/FormTrait.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * 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 Symfony\Component\Form\AbstractType; - -trait FormTrait -{ - /** - * Stores whether the current class is a form type across visits of all children nodes. - */ - private bool $isFormType = false; - - 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; - } -} diff --git a/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php b/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php index 33db299befe7d..c10fa223ce51e 100644 --- a/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php +++ b/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php @@ -13,6 +13,7 @@ use PhpParser\Node; use PhpParser\NodeVisitor; +use Symfony\Component\Form\AbstractType; /** * @author Mathieu Santostefano @@ -21,13 +22,10 @@ */ final class FormTypeVisitor extends AbstractVisitor implements NodeVisitor { - use FormTrait; - - public function __construct( - // to be deleted - private readonly array $formTypeClassNames = [], - ) { - } + /** + * 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 { @@ -45,6 +43,11 @@ public function enterNode(Node $node): ?Node $this->visitArray($node); } + // Visit all "add()" method calls to look for implicit labels + if ($node instanceof Node\Expr\MethodCall) { + $this->visitMethodCall($node); + } + return null; } @@ -58,6 +61,21 @@ public function afterTraverse(array $nodes): ?Node return null; } + private function visitMethodCall(Node\Expr\MethodCall $node): void + { + if ('add' !== $node->name->name) { + return; + } + + if (!$node->args[0]->value instanceof Node\Scalar\String_) { + return; + } + + if (\count($node->args) === 1) { + $this->addMessageToCatalogue($this->getStringValue($node->args[0]->value), 'messages', $node->args[0]->value->getStartLine()); + } + } + private function visitArray(Node\Expr\Array_ $node): void { foreach ($node->items as $item) { @@ -70,4 +88,19 @@ private function visitArray(Node\Expr\Array_ $node): void } } } + + 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; + } } diff --git a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php index 72cd9f2f76ec9..7ff0d11483f0d 100644 --- a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php +++ b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php @@ -149,19 +149,4 @@ public function testValidPhpAstExtractorConstraintVisitorArguments() $this->assertSame(['NotBlank', 'Isbn', 'Length', 'Time'], $constraintVisitor->getArgument(0)); } - - public function testValidPhpAstExtractorFormTypeVisitorArguments() - { - $container = new ContainerBuilder(); - $container->register('translator.default') - ->setArguments([null, null, null, null]); - $formTypeVisitor = $container->register('translation.extractor.visitor.form_type', FormTypeVisitor::class); - $container->register('form.type.text', TextType::class) - ->addTag('form.type'); - - $pass = new TranslatorPass(); - $pass->process($container); - - $this->assertSame(['Text'], $formTypeVisitor->getArgument(0)); - } } diff --git a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php index 44ef032227a6d..37564e10def1d 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php @@ -36,9 +36,7 @@ public function testExtraction(iterable|string $resource) 'Isbn', 'Length', ]), - new FormTypeVisitor([ - 'ExplicitLabelType', - ]), + new FormTypeVisitor(), ]); $extractor->setPrefix('prefix'); $catalogue = new MessageCatalogue('en'); @@ -210,9 +208,7 @@ public function testExtractionFromIndentedHeredocNowdoc() 'Isbn', 'Length', ]), - new FormTypeVisitor([ - 'ExplicitLabelType', - ]), + new FormTypeVisitor(), ]); $extractor->setPrefix('prefix'); $extractor->extract(__DIR__.'/../Fixtures/extractor-7.3/translation.html.php', $catalogue); From 4ea83c36c44d3a585d83f2155945193cfd65e1aa Mon Sep 17 00:00:00 2001 From: "hubert.lenoir" Date: Wed, 12 Mar 2025 13:39:27 +0100 Subject: [PATCH 04/12] typo --- .../Tests/Extractor/Visitor/AbstractVisitorTest.php | 4 ++-- .../Tests/Extractor/Visitor/ConstraintVisitorTest.php | 2 +- .../Tests/Extractor/Visitor/FormTypeVisitorTest.php | 2 +- .../Tests/Extractor/Visitor/TransMethodVisitorTest.php | 2 +- .../Extractor/Visitor/TranslatableMessageVisitorTest.php | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php index 3efbe559a86b4..44241cb7eb75d 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php @@ -9,13 +9,13 @@ abstract class AbstractVisitorTest extends TestCase { - abstract public function getVisitors(): NodeVisitor; + abstract public function getVisitor(): NodeVisitor; abstract public function getResource(): iterable|string; abstract public function assertCatalogue(MessageCatalogue $catalogue): void; public function testVisitor() { - $extractor = new PhpAstExtractor([$this->getVisitors()]); + $extractor = new PhpAstExtractor([$this->getVisitor()]); $extractor->setPrefix('prefix'); $catalogue = new MessageCatalogue('en'); diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/ConstraintVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/ConstraintVisitorTest.php index 023e8a2c71024..b3a5e9f7f22ec 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/ConstraintVisitorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/ConstraintVisitorTest.php @@ -10,7 +10,7 @@ class ConstraintVisitorTest extends AbstractVisitorTest { private const FIXTURES_FOLDER = __DIR__ . '/../../Fixtures/extractor-php-ast/constraint-visitor/'; - public function getVisitors(): NodeVisitor + public function getVisitor(): NodeVisitor { return new ConstraintVisitor(['NotBlank', 'Isbn', 'Length']); } diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php index 4a6b33afea7c5..9d607aac8ad87 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php @@ -11,7 +11,7 @@ class FormTypeVisitorTest extends AbstractVisitorTest { private const FIXTURES_FOLDER = __DIR__ . '/../../Fixtures/extractor-php-ast/form-type-visitor/'; - public function getVisitors(): FormTypeVisitor + public function getVisitor(): FormTypeVisitor { return new FormTypeVisitor(); } diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php index e9193d0c1b911..2b0ffda00f5c3 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php @@ -11,7 +11,7 @@ class TransMethodVisitorTest extends AbstractVisitorTest private const FIXTURES_FOLDER = __DIR__ . '/../../Fixtures/extractor-php-ast/trans-method-visitor/'; public const OTHER_DOMAIN = 'not_messages'; - public function getVisitors(): NodeVisitor + public function getVisitor(): NodeVisitor { return new TransMethodVisitor(); } diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TranslatableMessageVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TranslatableMessageVisitorTest.php index e795cda190981..ad9c2e191701d 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TranslatableMessageVisitorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TranslatableMessageVisitorTest.php @@ -11,7 +11,7 @@ class TranslatableMessageVisitorTest extends AbstractVisitorTest { private const FIXTURES_FOLDER = __DIR__ . '/../../Fixtures/extractor-php-ast/translatable-message-visitor/'; - public function getVisitors(): NodeVisitor + public function getVisitor(): NodeVisitor { return new TranslatableMessageVisitor(); } From 9006696800015f6e9ee1f16673110eed200fc8f1 Mon Sep 17 00:00:00 2001 From: Thibaut Chieux Date: Tue, 6 May 2025 15:35:55 +0200 Subject: [PATCH 05/12] Add test for translatable BackedEnum with explicit match in trans method --- .../Visitor/TransMethodVisitorTest.php | 5 +++++ .../translatable-backed-enum.html.php | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-backed-enum.html.php diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php index 2b0ffda00f5c3..f3762f6a2212b 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php @@ -71,6 +71,9 @@ public function assertCatalogue(MessageCatalogue $catalogue): void 'translatable-short-fqn '.$expectedNowdoc => 'prefixtranslatable-short-fqn '.$expectedNowdoc, 'translatable-short-fqn concatenated message with heredoc and nowdoc' => 'prefixtranslatable-short-fqn concatenated message with heredoc and nowdoc', 'translatable-short-fqn default domain' => 'prefixtranslatable-short-fqn default domain', + 'text_align.left.label' => 'prefixtext_align.left.label', + 'text_align.center.label' => 'prefixtext_align.center.label', + 'text_align.right.label' => 'prefixtext_align.right.label', ], 'not_messages' => [ 'other-domain-test-no-params-short-array' => 'prefixother-domain-test-no-params-short-array', @@ -111,5 +114,7 @@ public function assertCatalogue(MessageCatalogue $catalogue): void $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-short-fqn.html.php:2']], $catalogue->getMetadata('translatable-short-fqn single-quoted key')); $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-short-fqn.html.php:37']], $catalogue->getMetadata('translatable-short-fqn other-domain-test-no-params-short-array', 'not_messages')); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-backed-enum.html.php:16']], $catalogue->getMetadata('text_align.left.label')); } } diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-backed-enum.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-backed-enum.html.php new file mode 100644 index 0000000000000..e3529bf56a34f --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/translatable-backed-enum.html.php @@ -0,0 +1,22 @@ +This template is used for translation message extraction tests + $translator->trans('text_align.left.label', locale: $locale), + self::Center => $translator->trans('text_align.center.label', locale: $locale), + self::Right => $translator->trans('text_align.right.label', locale: $locale), + }; + } +} From 405c413dccd1b5e9c7ffbd737d910ef96f4a3713 Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Fri, 2 May 2025 16:17:11 +0200 Subject: [PATCH 06/12] feat(PhpAstExtractor): Handle placeholder and help FormType options --- .../Extractor/Visitor/FormTypeVisitor.php | 26 ++++++++------- .../Extractor/Visitor/FormTypeVisitorTest.php | 8 ++++- .../form-type-visitor/form-type.php | 32 ++++++++++++------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php b/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php index c10fa223ce51e..fa3b5d01d82a9 100644 --- a/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php +++ b/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php @@ -38,14 +38,14 @@ public function enterNode(Node $node): ?Node return null; } - // Visit all array expressions to look for options array (containing explicit labels) + // Visit all array expressions to look for options array if ($node instanceof Node\Expr\Array_) { - $this->visitArray($node); + $this->visitOptionsArray($node); } // Visit all "add()" method calls to look for implicit labels if ($node instanceof Node\Expr\MethodCall) { - $this->visitMethodCall($node); + $this->visitAddMethodCall($node); } return null; @@ -61,7 +61,7 @@ public function afterTraverse(array $nodes): ?Node return null; } - private function visitMethodCall(Node\Expr\MethodCall $node): void + private function visitAddMethodCall(Node\Expr\MethodCall $node): void { if ('add' !== $node->name->name) { return; @@ -76,15 +76,19 @@ private function visitMethodCall(Node\Expr\MethodCall $node): void } } - private function visitArray(Node\Expr\Array_ $node): void + private function visitOptionsArray(Node\Expr\Array_ $node): void { + $translatableOptions = ['label', 'placeholder', 'help']; + foreach ($node->items as $item) { - if ($item->key instanceof Node\Scalar\String_ && 'label' === $item->key->value) { - // If the label 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()); - } + 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()); } } } diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php index 9d607aac8ad87..5aac0c6042002 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php @@ -36,11 +36,17 @@ public function assertCatalogue(MessageCatalogue $catalogue): void 'bigger_find7' => 'prefixbigger_find7', 'camelFind8' => 'prefixcamelFind8', 'label.find9' => 'prefixlabel.find9', + 'placeholder.foo1' => 'prefixplaceholder.foo1', + 'help.foo1' => 'prefixhelp.foo1', + 'placeholder.find4' => 'prefixplaceholder.find4', + 'help.find4' => 'prefixhelp.find4', + 'placeholder.find5' => 'prefixplaceholder.find5', + 'help.find5' => 'prefixhelp.find5', ], ], $catalogue->all(), ); - $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'form-type.php:27']], $catalogue->getMetadata('label.find1')); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'form-type.php:29']], $catalogue->getMetadata('label.find1')); } } diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php index 8c1b87b24a48c..bdf454590417f 100644 --- a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php @@ -13,52 +13,62 @@ class FooType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('foo1', null, [ - 'label' => 'label.foo1' + 'label' => 'label.foo1', + 'placeholder' => 'placeholder.foo1', + 'help' => 'help.foo1', ]); } } -class ExplicitLabelType extends AbstractType +class ExtractionTestFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $var = "something"; $builder->add('find1', null, [ - 'label' => 'label.find1' + 'label' => 'label.find1', ]); $builder ->add('find2', null, array( - 'label' => 'find2' + 'label' => 'find2', )) ->add('field_longer_name3', null, [ - 'label' => 'FOUND3' + 'label' => 'FOUND3', ]) ->add('skip1', null, [ 'label' => $var, // shouldn't be picked up + 'placeholder' => $var, // shouldn't be picked up + 'help' => $var, // shouldn't be picked up 'somethingelse' => 'skipthis', ]) ->add('skip2', null, [ 'label' => PHP_OS, // constant shouldn't work + 'placeholder' => PHP_OS, // constant shouldn't work + 'help' => PHP_OS, // constant shouldn't work ]) ->add('skip3', null, [ - 'label' // value label, shouldn't be picked up + 'label', // value label, shouldn't be picked up ]) ->add('skip4', null, [ - 'label' => 'something '.$var // string+var concatenation, shouldn't be picked up + 'label' => 'something '.$var, // string+var concatenation, shouldn't be picked up + 'placeholder' => 'something '.$var, // string+var concatenation, shouldn't be picked up + 'help' => 'something '.$var, // string+var concatenation, shouldn't be picked up ]) ; - // add label in variable should be found - $opts = ['label'=>'label.find4']; + // add options in variable should be found + $opts = ['label'=>'label.find4','placeholder'=>'placeholder.find4','help'=>'help.find4']; $builder->add('find4', null, $opts); - // empty label should be skipped - $builder->add('skip5', null, ['label'=>'']); + // empty options should be skipped + $builder->add('skip5', null, ['label'=>'', 'placeholder'=>'', 'help'=>'']); // collection test $builder->add('find5', CollectionType::class, [ 'options' => [ 'label' => 'label.find5', + 'placeholder' => 'placeholder.find5', + 'help' => 'help.find5', ], ]); From 61ae4b124f43f44dfeb34e73d1de9db54dc85d2d Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Fri, 9 May 2025 14:40:28 +0200 Subject: [PATCH 07/12] Update src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php Co-authored-by: Hubert Lenoir --- .../Tests/Extractor/Visitor/TransMethodVisitorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php index f3762f6a2212b..9340b23a18269 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php @@ -115,6 +115,6 @@ public function assertCatalogue(MessageCatalogue $catalogue): void $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-short-fqn.html.php:2']], $catalogue->getMetadata('translatable-short-fqn single-quoted key')); $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-short-fqn.html.php:37']], $catalogue->getMetadata('translatable-short-fqn other-domain-test-no-params-short-array', 'not_messages')); - $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-backed-enum.html.php:16']], $catalogue->getMetadata('text_align.left.label')); + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-backed-enum.html.php:17']], $catalogue->getMetadata('text_align.left.label')); } } From f9a5167996894dbfc29c95919595efdf68a33193 Mon Sep 17 00:00:00 2001 From: jprivet-dev Date: Tue, 6 May 2025 17:55:31 +0200 Subject: [PATCH 08/12] refactor(PhpAstExtractor): create toRegex() for canBeExtracted() --- .../Translation/Extractor/PhpAstExtractor.php | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php b/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php index 34244874baad8..25c8b8fe70284 100644 --- a/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php +++ b/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php @@ -69,9 +69,17 @@ public function setPrefix(string $prefix): void protected function canBeExtracted(string $file): bool { + $regex = $this->toRegex([ + 't(', + '->trans(', + 'TranslatableMessage', + 'Symfony\Component\Validator\Constraints', + 'Symfony\Component\Form\AbstractType', + ]); + return 'php' === pathinfo($file, \PATHINFO_EXTENSION) && $this->isFile($file) - && preg_match('/\bt\(|->trans\(|TranslatableMessage|Symfony\\\\Component\\\\Validator\\\\Constraints|Symfony\\\\Component\\\\Form\\\\AbstractType/i', file_get_contents($file)); + && preg_match($regex, file_get_contents($file)); } protected function extractFromDirectory(array|string $resource): iterable|Finder @@ -82,4 +90,20 @@ protected function extractFromDirectory(array|string $resource): iterable|Finder return (new Finder())->files()->name('*.php')->in($resource); } + + /** + * Returns a regexp that finds the raw string elements of the transmitted array. + * + * @param array $find + */ + protected function toRegex(array $find): string + { + // In the regex '\\\\' matches the character \ + $find = array_map(fn(string $current): string => str_replace('\\', '\\\\', $current), $find); + + // In the regex '\(' matches the character ( + $find = array_map(fn(string $current): string => str_replace('(', '\(', $current), $find); + + return '/\b'.implode('|', $find).'/i'; + } } From 04c539262b1c3a4c54ced26fb39eb315ae42d0d5 Mon Sep 17 00:00:00 2001 From: jprivet-dev Date: Wed, 7 May 2025 16:24:46 +0200 Subject: [PATCH 09/12] feat(PhpAstExtractor): canBeExtracted() > switch from protected to public --- src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php b/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php index 25c8b8fe70284..b914c7e4475e6 100644 --- a/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php +++ b/src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php @@ -67,7 +67,7 @@ public function setPrefix(string $prefix): void $this->prefix = $prefix; } - protected function canBeExtracted(string $file): bool + public function canBeExtracted(string $file): bool { $regex = $this->toRegex([ 't(', From 273d71224fa480e4aa8d08e836ca1a6c0c5aee10 Mon Sep 17 00:00:00 2001 From: jprivet-dev Date: Wed, 7 May 2025 16:25:58 +0200 Subject: [PATCH 10/12] feat(Translation/Tests/Fixtures): complete extractor-php-ast files --- .../extract-files/form-type.php | 16 ++++++++++++++++ .../extract-files/translatable-fqn.html.php | 1 + .../translatable-short-fqn.html.php | 1 + .../extract-files/translatable-short.html.php | 1 + .../extract-files/translatable.html.php | 1 + .../extract-files/validator-constraints.php | 9 +++++++++ 6 files changed, 29 insertions(+) create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/form-type.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-fqn.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-short-fqn.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-short.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable.html.php create mode 100644 src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/validator-constraints.php diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/form-type.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/form-type.php new file mode 100644 index 0000000000000..fd8e99f704700 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/form-type.php @@ -0,0 +1,16 @@ +add('foo1', null, [ + 'label' => 'label.foo1', + 'placeholder' => 'placeholder.foo1', + 'help' => 'help.foo1', + ]); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-fqn.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-fqn.html.php new file mode 100644 index 0000000000000..670d7b1f06a87 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-fqn.html.php @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-short-fqn.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-short-fqn.html.php new file mode 100644 index 0000000000000..9d93251058ba1 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-short-fqn.html.php @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-short.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-short.html.php new file mode 100644 index 0000000000000..89e9bc8517434 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable-short.html.php @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable.html.php new file mode 100644 index 0000000000000..8a062aad04305 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/translatable.html.php @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/validator-constraints.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/validator-constraints.php new file mode 100644 index 0000000000000..f2d5232c3752e --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/extract-files/validator-constraints.php @@ -0,0 +1,9 @@ + Date: Wed, 7 May 2025 16:27:38 +0200 Subject: [PATCH 11/12] feat(PhpAstExtractorTest): testExtractFiles() > complete sources in assertion --- .../Translation/Tests/Extractor/PhpAstExtractorTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php index 13e02f87aad73..fb88206c85ebb 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php @@ -31,7 +31,13 @@ public function testExtractFiles(iterable|string $resource) $extractor->extract($resource, $catalogue); $this->assertEquals(['messages' => ['example' => 'example']], $catalogue->all()); - $this->assertEquals(['sources' => [self::FIXTURES_FOLDER.'translation.html.php:1']], $catalogue->getMetadata('example')); + $this->assertEqualsCanonicalizing([ + 'sources' => [ + self::FIXTURES_FOLDER.'translation.html.php:1', + self::FIXTURES_FOLDER.'translatable-short.html.php:1', + self::FIXTURES_FOLDER.'translatable-short-fqn.html.php:1', + ], + ], $catalogue->getMetadata('example')); } public static function resourcesProvider(): \Generator From 7425f5a7c5bc63a55b9400bd56fd29ab1381d2a9 Mon Sep 17 00:00:00 2001 From: jprivet-dev Date: Wed, 7 May 2025 16:30:33 +0200 Subject: [PATCH 12/12] feat(PhpAstExtractorTest): create testCanBeExtracted() --- .../Tests/Extractor/PhpAstExtractorTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php index fb88206c85ebb..95b7d889621bb 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php @@ -40,6 +40,16 @@ public function testExtractFiles(iterable|string $resource) ], $catalogue->getMetadata('example')); } + public function testCanBeExtracted() + { + $extractor = new PhpAstExtractor([new TransMethodVisitor()]); + $phpFiles = glob(self::FIXTURES_FOLDER.'*.php'); + + foreach ($phpFiles as $file) { + $this->assertTrue($extractor->canBeExtracted($file), sprintf('Current PHP file: %s', $file)); + } + } + public static function resourcesProvider(): \Generator { $phpFiles = [];