Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

if ($container->hasDefinition('console.command.translation_extract')) {
$definition = $container->getDefinition('console.command.translation_extract');
$definition->replaceArgument(5, $container->getParameter('twig.default_path'));
Expand Down
28 changes: 26 additions & 2 deletions src/Symfony/Component/Translation/Extractor/PhpAstExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,19 @@ 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(',
'->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/i', file_get_contents($file));
&& preg_match($regex, file_get_contents($file));
}

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

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

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

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

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

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

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

return null;
}

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

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

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

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

namespace Symfony\Component\Translation\Extractor\Visitor;

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

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

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

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

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

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

return null;
}

public function leaveNode(Node $node): ?Node
{
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
use Symfony\Component\Translation\Extractor\Visitor\ConstraintVisitor;
use Symfony\Component\Translation\Extractor\Visitor\FormTypeVisitor;
use Symfony\Component\Validator\Constraints\IsbnValidator;
use Symfony\Component\Validator\Constraints\LengthValidator;
use Symfony\Component\Validator\Constraints\NotBlankValidator;
Expand Down
Loading
Loading