Skip to content

[Translation][TwigBridge] Implementing variables extraction in "translation:update" #38884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
06fba65
Implementing variables extraction in "translation:update"
liarco Oct 29, 2020
e2934b5
Applying patch from fabbot
liarco Oct 29, 2020
bedb0be
Fixing regression: Twig t() now works when domain is set with trans_d…
liarco Oct 30, 2020
87d542f
Fixing variables metadata overwriting previously set data (PHP extrac…
liarco Oct 30, 2020
8142f75
Updating existing tests to include variables
liarco Oct 30, 2020
a333f13
Applying patch from fabbot
liarco Oct 30, 2020
38d9e55
Supporting both long and short array syntax (PHP extractor)
liarco Oct 30, 2020
edb5aa9
Improving/fixing existing tests and adding variables tests (PHP extra…
liarco Oct 30, 2020
31f96a0
Adding variables tests (Twig extractor)
liarco Oct 31, 2020
5c2e782
Clearer naming
liarco Oct 31, 2020
c3e2e88
Avoid overwriting metadata completely when adding variables note (Twi…
liarco Oct 31, 2020
c50ce67
Supporting "t()" functions (Twig) without "|trans()" filter (e.g. as …
liarco Oct 31, 2020
6e4013c
Fixing duplicate notes. Extractors now keep the higher variables coun…
liarco Oct 31, 2020
3e0bb8e
Improving readability
liarco Oct 31, 2020
deb72d3
Always update variables with latest from code
liarco Oct 31, 2020
e8cee75
Fixing bug preventing update of variables from source in some cases
liarco Nov 13, 2020
63a52b9
Fixing unintended CS errors (thanks @wouterj)
liarco Nov 13, 2020
2a10f37
Applying patch from fabbot (reviewed manually)
liarco Dec 18, 2020
0a5366f
Minor fixes
liarco Mar 9, 2021
46d26ec
Using constants for metadata key and prefix
liarco Mar 9, 2021
1612c08
Fixing CI
liarco Mar 9, 2021
8ec6086
Improving code quality
liarco Mar 9, 2021
0025c52
Fixing static analysis (Psalm)
liarco Mar 15, 2021
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
112 changes: 108 additions & 4 deletions src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Bridge\Twig\NodeVisitor;

use ArrayIterator;
use Symfony\Bridge\Twig\Node\TransNode;
use Twig\Environment;
use Twig\Node\Expression\Binary\ConcatBinary;
Expand All @@ -30,6 +31,33 @@ final class TranslationNodeVisitor extends AbstractNodeVisitor
public const UNDEFINED_DOMAIN = '_undefined';

private $enabled = false;

/**
* This class cannot read nodes backwards.
* We need a way to track when we encounter a "|trans()" filter containing
* a "t()" function in order to avoid processing that same function again
* as if it was used alone.
*/
private $skipTFunctionAfterFilter = false;

/**
* This array stores found messages.
*
* The data structure of this array is as follows:
*
* [
* 0 => [
* 0 => 'message',
* 1 => 'domain',
* 2 => [
* 0 => 'variable1',
* 1 => 'variable2',
* ...
* ]
* ],
* ...
* ]
*/
private $messages = [];

public function enable(): void
Expand Down Expand Up @@ -67,26 +95,57 @@ protected function doEnterNode(Node $node, Environment $env): Node
$this->messages[] = [
$node->getNode('node')->getAttribute('value'),
$this->getReadDomainFromArguments($node->getNode('arguments'), 1),
$this->getReadVariablesFromArguments($node->getNode('arguments'), 0),
];
} elseif (
$node instanceof FilterExpression &&
'trans' === $node->getNode('filter')->getAttribute('value') &&
$node->getNode('node') instanceof FunctionExpression &&
't' === $node->getNode('node')->getAttribute('name')
) {
$nodeArguments = $node->getNode('node')->getNode('arguments');
// extract t() nodes with a trans filter applied
$functionNodeArguments = $node->getNode('node')->getNode('arguments');
/** @var ArrayIterator $iterator */
$iterator = $functionNodeArguments->getIterator();

if ($nodeArguments->getIterator()->current() instanceof ConstantExpression) {
if ($iterator->current() instanceof ConstantExpression) {
$this->messages[] = [
$this->getReadMessageFromArguments($nodeArguments, 0),
$this->getReadDomainFromArguments($nodeArguments, 2),
$this->getReadMessageFromArguments($functionNodeArguments, 0),
$this->getReadDomainFromArguments($functionNodeArguments, 2),
$this->getReadVariablesFromArguments($functionNodeArguments, 1),
];

// Avoid processing this "t()" function twice
$this->skipTFunctionAfterFilter = true;
}
} elseif (
$node instanceof FunctionExpression &&
't' === $node->getAttribute('name')
) {
// extract t() nodes without a trans filter applied
if ($this->skipTFunctionAfterFilter) {
$this->skipTFunctionAfterFilter = false;

return $node;
}

$functionNodeArguments = $node->getNode('arguments');
/** @var ArrayIterator $iterator */
$iterator = $functionNodeArguments->getIterator();

if ($iterator->current() instanceof ConstantExpression) {
$this->messages[] = [
$this->getReadMessageFromArguments($functionNodeArguments, 0),
$this->getReadDomainFromArguments($functionNodeArguments, 2),
$this->getReadVariablesFromArguments($functionNodeArguments, 1),
];
}
} elseif ($node instanceof TransNode) {
// extract trans nodes
$this->messages[] = [
$node->getNode('body')->getAttribute('data'),
$node->hasNode('domain') ? $this->getReadDomainFromNode($node->getNode('domain')) : null,
$this->getReadVariablesFromArguments($node, 0),
];
} elseif (
$node instanceof FilterExpression &&
Expand Down Expand Up @@ -141,6 +200,51 @@ private function getReadMessageFromNode(Node $node): ?string
return null;
}

/**
* Extracts variable names from a @Node containing all arguments passed to
* a Twig function/filter.
*/
private function getReadVariablesFromArguments(Node $arguments, int $index): array
{
if ($arguments->hasNode('vars')) {
$argument = $arguments->getNode('vars');
} elseif ($arguments->hasNode($index)) {
$argument = $arguments->getNode($index);
} else {
return [];
}

return $this->getReadVariablesFromNode($argument);
}

/**
* Extracts variable names from a @Node representing the array of
* parameters passed to the translation function/filter.
*/
private function getReadVariablesFromNode(Node $node): array
{
if (\count($node) <= 0) {
return [];
}

$variables = [];
$isVariableName = true;

foreach ($node as $parameterToken) {
/*
* Variable names and values are direct children of the parent node (e.g. ['var1', 'value1', 'var2',
* 'value2', 'var3', 'value3']), so we can get all names by skipping the values every two cycles.
*/
if ($isVariableName ^= 1) {
continue;
}

$variables[] = $parameterToken->getAttribute('value');
}

return $variables;
}

private function getReadDomainFromArguments(Node $arguments, int $index): ?string
{
if ($arguments->hasNode('domain')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function testDefaultDomainAssignment(Node $node)
$visitor->enterNode($node, $env);
$visitor->leaveNode($node, $env);

$this->assertEquals([[self::$message, self::$domain]], $visitor->getMessages());
$this->assertEquals([[self::$message, self::$domain, []]], $visitor->getMessages());
}

/** @dataProvider getDefaultDomainAssignmentTestData */
Expand All @@ -73,7 +73,7 @@ public function testNewModuleWithoutDefaultDomainTag(Node $node)
$visitor->enterNode($node, $env);
$visitor->leaveNode($node, $env);

$this->assertEquals([[self::$message, null]], $visitor->getMessages());
$this->assertEquals([[self::$message, null, []]], $visitor->getMessages());
}

public function getDefaultDomainAssignmentTestData()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function testMessageExtractionWithInvalidDomainNode()
0
);

$this->testMessagesExtraction($node, [[$message, TranslationNodeVisitor::UNDEFINED_DOMAIN]]);
$this->testMessagesExtraction($node, [[$message, TranslationNodeVisitor::UNDEFINED_DOMAIN, []]]);
}

public function getMessagesExtractionTestData()
Expand All @@ -57,10 +57,10 @@ public function getMessagesExtractionTestData()
$domain = 'domain';

return [
[TwigNodeProvider::getTransFilter($message), [[$message, null]]],
[TwigNodeProvider::getTransTag($message), [[$message, null]]],
[TwigNodeProvider::getTransFilter($message, $domain), [[$message, $domain]]],
[TwigNodeProvider::getTransTag($message, $domain), [[$message, $domain]]],
[TwigNodeProvider::getTransFilter($message), [[$message, null, []]]],
[TwigNodeProvider::getTransTag($message), [[$message, null, []]]],
[TwigNodeProvider::getTransFilter($message, $domain), [[$message, $domain, []]]],
[TwigNodeProvider::getTransTag($message, $domain), [[$message, $domain, []]]],
];
}
}
Loading