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 300fe22fb37a9..f79a67562532f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -21,7 +21,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; @@ -191,7 +191,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/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..64fa93073df23 100644 --- a/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php +++ b/src/Symfony/Component/Translation/DependencyInjection/TranslatorPass.php @@ -24,6 +24,13 @@ public function process(ContainerBuilder $container): void return; } + $this->processLoadersAndReaders($container); + $this->processExtractorConstraintVisitor($container); + $this->processTwigPaths($container); + } + + private function processLoadersAndReaders(ContainerBuilder $container): void + { $loaders = []; $loaderRefs = []; foreach ($container->findTaggedServiceIds('translation.loader', true) as $id => $attributes) { @@ -48,29 +55,38 @@ public function process(ContainerBuilder $container): void ->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs)) ->replaceArgument(3, $loaders) ; + } - if ($container->hasDefinition('validator') && $container->hasDefinition('translation.extractor.visitor.constraint')) { - $constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint'); - $constraintClassNames = []; + private function processExtractorConstraintVisitor(ContainerBuilder $container): void + { + if (!$container->hasDefinition('validator') || !$container->hasDefinition('translation.extractor.visitor.constraint')) { + return; + } - foreach ($container->getDefinitions() as $definition) { - if (!$definition->hasTag('validator.constraint_validator')) { - continue; - } - // Resolve constraint validator FQCN even if defined as %foo.validator.class% parameter - $className = $container->getParameterBag()->resolveValue($definition->getClass()); - // Extraction of the constraint class name from the Constraint Validator FQCN - $constraintClassNames[] = str_replace('Validator', '', substr(strrchr($className, '\\'), 1)); - } + $constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint'); + $constraintClassNames = []; - $constraintVisitorDefinition->setArgument(0, $constraintClassNames); + foreach ($container->getDefinitions() as $definition) { + if (!$definition->hasTag('validator.constraint_validator')) { + continue; + } + // Resolve constraint validator FQCN even if defined as %foo.validator.class% parameter + $className = $container->getParameterBag()->resolveValue($definition->getClass()); + // Extraction of the constraint class name from the Constraint Validator FQCN + $constraintClassNames[] = str_replace('Validator', '', substr(strrchr($className, '\\'), 1)); } + $constraintVisitorDefinition->setArgument(0, $constraintClassNames); + } + + private function processTwigPaths(ContainerBuilder $container): void + { if (!$container->hasParameter('twig.default_path')) { return; } $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1)); + if ($container->hasDefinition('console.command.translation_debug')) { $definition = $container->getDefinition('console.command.translation_debug'); $definition->replaceArgument(4, $container->getParameter('twig.default_path')); @@ -79,6 +95,7 @@ public function process(ContainerBuilder $container): void $definition->replaceArgument(6, $paths); } } + if ($container->hasDefinition('console.command.translation_extract')) { $definition = $container->getDefinition('console.command.translation_extract'); $definition->replaceArgument(5, $container->getParameter('twig.default_path')); 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/FormTypeVisitor.php b/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php new file mode 100644 index 0000000000000..fa3b5d01d82a9 --- /dev/null +++ b/src/Symfony/Component/Translation/Extractor/Visitor/FormTypeVisitor.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor\Visitor; + +use PhpParser\Node; +use PhpParser\NodeVisitor; +use Symfony\Component\Form\AbstractType; + +/** + * @author Mathieu Santostefano + * + * Code mostly comes from https://github.com/php-translation/extractor/blob/master/src/Visitor/Php/Symfony/ + */ +final class FormTypeVisitor extends AbstractVisitor implements NodeVisitor +{ + /** + * Stores whether the current class is a form type across visits of all children nodes. + */ + private bool $isFormType = false; + + public function beforeTraverse(array $nodes): ?Node + { + return null; + } + + public function enterNode(Node $node): ?Node + { + if (!$this->isFormType($node)) { + return null; + } + + // Visit all array expressions to look for options array + if ($node instanceof Node\Expr\Array_) { + $this->visitOptionsArray($node); + } + + // Visit all "add()" method calls to look for implicit labels + if ($node instanceof Node\Expr\MethodCall) { + $this->visitAddMethodCall($node); + } + + return null; + } + + public function leaveNode(Node $node): ?Node + { + return null; + } + + public function afterTraverse(array $nodes): ?Node + { + return null; + } + + private function visitAddMethodCall(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 visitOptionsArray(Node\Expr\Array_ $node): void + { + $translatableOptions = ['label', 'placeholder', 'help']; + + foreach ($node->items as $item) { + if (!$item->key instanceof Node\Scalar\String_ || !in_array($item->key->value, $translatableOptions, true)) { + continue; + } + + // If the option is a non-empty string, add it to the messages + $stringValue = $this->getStringValue($item->value); + if (null !== $stringValue && '' !== $stringValue) { + $this->addMessageToCatalogue($stringValue, 'messages', $item->getStartLine()); + } + } + } + + private function isFormType(Node $node): bool + { + if ($node instanceof Node\Stmt\Class_) { + if ($node->extends !== null) { + if ($node->extends->isFullyQualified()) { + if ($node->extends->name === AbstractType::class) { + $this->isFormType = true; + } + } + } + } + + return $this->isFormType; + } +} diff --git a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php index 1efd932be9014..7ff0d11483f0d 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; diff --git a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php index 658164a37150d..13e02f87aad73 100644 --- a/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php +++ b/src/Symfony/Component/Translation/Tests/Extractor/PhpAstExtractorTest.php @@ -13,225 +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\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 TranslatableMessageVisitor()), - ]); - $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', - ], - '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 TranslatableMessageVisitor()), - ]); - $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(): array + public static function resourcesProvider(): \Generator { - $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(), ['translatable.html.php', 'translatable-fqn.html.php', 'translatable-short.html.php', 'translatable-short-fqn.html.php', 'translation.html.php', 'validator-constraints.php'], true)) { + if ('php' === $fileInfo->getExtension()) { $phpFiles[] = $fileInfo->getPathname(); } $splFiles[] = $fileInfo->getFileInfo(); } - return [ - [$directory], - [$phpFiles], - [glob($directory.'*')], - [$splFiles], - [new \ArrayObject(glob($directory.'*'))], - [new \ArrayObject($splFiles)], - ]; + yield 'directory' => [self::FIXTURES_FOLDER]; + yield 'phpFiles' => [$phpFiles]; + yield 'glob' => [glob(self::FIXTURES_FOLDER.'*')]; + yield 'splFiles' => [$splFiles]; + 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..44241cb7eb75d --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/AbstractVisitorTest.php @@ -0,0 +1,27 @@ +getVisitor()]); + $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..b3a5e9f7f22ec --- /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..5aac0c6042002 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/FormTypeVisitorTest.php @@ -0,0 +1,52 @@ +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', + '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:29']], $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..9340b23a18269 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Extractor/Visitor/TransMethodVisitorTest.php @@ -0,0 +1,120 @@ +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', + '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', + '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')); + + $this->assertEquals(['sources' => [self::FIXTURES_FOLDER . 'translatable-backed-enum.html.php:17']], $catalogue->getMetadata('text_align.left.label')); + } +} 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..ad9c2e191701d --- /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-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']); + } +} 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..bdf454590417f --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/form-type-visitor/form-type.php @@ -0,0 +1,82 @@ +This template is used for translation message extraction tests +add('foo1', null, [ + 'label' => 'label.foo1', + 'placeholder' => 'placeholder.foo1', + 'help' => 'help.foo1', + ]); + } +} + +class ExtractionTestFormType 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 + '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 + ]) + ->add('skip4', null, [ + '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 options in variable should be found + $opts = ['label'=>'label.find4','placeholder'=>'placeholder.find4','help'=>'help.find4']; + $builder->add('find4', null, $opts); + + // 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', + ], + ]); + + // 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/not-translation-interface-trans-method.html.php b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/not-translation-interface-trans-method.html.php new file mode 100644 index 0000000000000..3c9885068737a --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Fixtures/extractor-php-ast/trans-method-visitor/not-translation-interface-trans-method.html.php @@ -0,0 +1,17 @@ +This template is used for translation message extraction tests +trans('skip'); 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), + }; + } +} 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'); ?> + +