Skip to content

[Translation] Translatable objects #37670

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

Merged
merged 1 commit into from
Aug 28, 2020
Merged
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
3 changes: 3 additions & 0 deletions src/Symfony/Bridge/Twig/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ CHANGELOG
-----

* added the `workflow_transition()` function to easily retrieve a specific transition object
* added support for translating `Translatable` objects
* added the `t()` function to easily create `Translatable` objects
* Added support for extracting messages from the `t()` function

5.0.0
-----
Expand Down
28 changes: 27 additions & 1 deletion src/Symfony/Bridge/Twig/Extension/TranslationExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor;
use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser;
use Symfony\Bridge\Twig\TokenParser\TransTokenParser;
use Symfony\Component\Translation\Translatable;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;

// Help opcache.preload discover always-needed symbols
class_exists(TranslatorInterface::class);
Expand Down Expand Up @@ -54,6 +56,16 @@ public function getTranslator(): TranslatorInterface
return $this->translator;
}

/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('t', [$this, 'createTranslatable']),
];
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -91,8 +103,17 @@ public function getTranslationNodeVisitor(): TranslationNodeVisitor
return $this->translationNodeVisitor ?: $this->translationNodeVisitor = new TranslationNodeVisitor();
}

public function trans(?string $message, array $arguments = [], string $domain = null, string $locale = null, int $count = null): string
/**
* @param ?string|Translatable $message The message id (may also be an object that can be cast to string)
*/
public function trans($message, array $arguments = [], string $domain = null, string $locale = null, int $count = null): string
{
if ($message instanceof Translatable) {
$arguments += $message->getParameters();
$domain = $message->getDomain();
$message = $message->getMessage();
}

if (null === $message || '' === $message) {
return '';
}
Expand All @@ -103,4 +124,9 @@ public function trans(?string $message, array $arguments = [], string $domain =

return $this->getTranslator()->trans($message, $arguments, $domain, $locale);
}

public function createTranslatable(string $message, array $parameters = [], string $domain = 'messages'): Translatable
{
return new Translatable($message, $parameters, $domain);
}
}
37 changes: 37 additions & 0 deletions src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Twig\Environment;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Node;
use Twig\NodeVisitor\AbstractNodeVisitor;

Expand Down Expand Up @@ -66,6 +67,20 @@ protected function doEnterNode(Node $node, Environment $env): Node
$node->getNode('node')->getAttribute('value'),
$this->getReadDomainFromArguments($node->getNode('arguments'), 1),
];
} 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');

if ($nodeArguments->getIterator()->current() instanceof ConstantExpression) {
$this->messages[] = [
$this->getReadMessageFromArguments($nodeArguments, 0),
$this->getReadDomainFromArguments($nodeArguments, 2),
];
}
} elseif (
$node instanceof FilterExpression &&
'transchoice' === $node->getNode('filter')->getAttribute('value') &&
Expand Down Expand Up @@ -103,6 +118,28 @@ public function getPriority(): int
return 0;
}

private function getReadMessageFromArguments(Node $arguments, int $index): ?string
{
if ($arguments->hasNode('message')) {
$argument = $arguments->getNode('message');
} elseif ($arguments->hasNode($index)) {
$argument = $arguments->getNode($index);
} else {
return null;
}

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

private function getReadMessageFromNode(Node $node): ?string
{
if ($node instanceof ConstantExpression) {
return $node->getAttribute('value');
}

return null;
}

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 @@ -122,6 +122,23 @@ public function getTransTests()
// trans filter with null message
['{{ null|trans }}', ''],
['{{ foo|trans }}', '', ['foo' => null]],

// trans object
['{{ t("Hello")|trans }}', 'Hello'],
['{{ t(name)|trans }}', 'Symfony', ['name' => 'Symfony']],
['{{ t(hello)|trans({ \'%name%\': \'Symfony\' }) }}', 'Hello Symfony', ['hello' => 'Hello %name%']],
['{{ t(hello, { \'%name%\': \'Symfony\' })|trans }}', 'Hello Symfony', ['hello' => 'Hello %name%']],
['{{ t(hello, { \'%name%\': \'Another Name\' })|trans({ \'%name%\': \'Symfony\' }) }}', 'Hello Symfony', ['hello' => 'Hello %name%']],
['{% set vars = { \'%name%\': \'Symfony\' } %}{{ t(hello)|trans(vars) }}', 'Hello Symfony', ['hello' => 'Hello %name%']],
['{% set vars = { \'%name%\': \'Symfony\' } %}{{ t(hello, vars)|trans }}', 'Hello Symfony', ['hello' => 'Hello %name%']],
['{{ t("Hello")|trans(locale="fr") }}', 'Hello'],
['{{ t("Hello", {}, "messages")|trans(locale="fr") }}', 'Hello'],

// trans object with count
['{{ t("{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples")|trans(count=count) }}', 'There is 5 apples', ['count' => 5]],
['{{ t(text)|trans(count=5, arguments={\'%name%\': \'Symfony\'}) }}', 'There is 5 apples (Symfony)', ['text' => '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%)']],
['{{ t(text, {\'%name%\': \'Symfony\'})|trans(count=5) }}', 'There is 5 apples (Symfony)', ['text' => '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%)']],
['{{ t("{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples", {}, "messages")|trans(locale="fr", count=count) }}', 'There is 5 apples', ['count' => 5]],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public function getExtractData()
['{% trans from "domain" %}new key{% endtrans %}', ['new key' => 'domain']],
['{% set foo = "new key" | trans %}', ['new key' => 'messages']],
['{{ 1 ? "new key" | trans : "another key" | trans }}', ['new key' => 'messages', 'another key' => 'messages']],
['{{ t("new key") | trans() }}', ['new key' => 'messages']],
['{{ t("new key", {}, "domain") | trans() }}', ['new key' => 'domain']],
['{{ 1 ? t("new key") | trans : t("another key") | trans }}', ['new key' => 'messages', 'another key' => 'messages']],

// make sure 'trans_default_domain' tag is supported
['{% trans_default_domain "domain" %}{{ "new key"|trans }}', ['new key' => 'domain']],
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/Translation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ CHANGELOG
-----

* added `PseudoLocalizationTranslator`
* added `Translatable` objects that represent a message that can be translated
* added the `t()` function to easily create `Translatable` objects
* Added support for extracting messages from `Translatable` objects

5.1.0
-----
Expand Down
60 changes: 60 additions & 0 deletions src/Symfony/Component/Translation/Extractor/PhpExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,66 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
'(',
self::MESSAGE_TOKEN,
],
[
'new',
'Translatable',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
'new',
'Translatable',
'(',
self::MESSAGE_TOKEN,
],
[
'new',
'\\',
'Symfony',
'\\',
'Component',
'\\',
'Translation',
'\\',
'Translatable',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
'new',
'\\',
'Symfony',
'\\',
'Component',
'\\',
'Translation',
'\\',
'Translatable',
'(',
self::MESSAGE_TOKEN,
],
[
't',
'(',
self::MESSAGE_TOKEN,
',',
self::METHOD_ARGUMENTS_TOKEN,
',',
self::DOMAIN_TOKEN,
],
[
't',
'(',
self::MESSAGE_TOKEN,
],
];

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

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

use Symfony\Component\Translation\Translatable;

if (!function_exists('t')) {
/**
* @author Nate Wiebe <nate@northern.co>
*/
function t(string $message, array $parameters = [], string $domain = 'messages'): Translatable
{
return new Translatable($message, $parameters, $domain);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,39 @@ public function testExtraction($resource)
// Assert
$expectedCatalogue = [
'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',
'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',
'single-quoted key' => 'prefixsingle-quoted key',
'double-quoted key' => 'prefixdouble-quoted key',
'heredoc key' => 'prefixheredoc key',
Expand All @@ -54,6 +87,21 @@ public function testExtraction($resource)
'default domain' => 'prefixdefault 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',
'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',
Expand All @@ -65,6 +113,18 @@ public function testExtraction($resource)

$this->assertEquals($expectedCatalogue, $actualCatalogue);

$filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../fixtures/extractor/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/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/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/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'));
Expand All @@ -73,20 +133,21 @@ public function testExtraction($resource)
public function resourcesProvider()
{
$directory = __DIR__.'/../fixtures/extractor/';
$phpFiles = [];
$splFiles = [];
foreach (new \DirectoryIterator($directory) as $fileInfo) {
if ($fileInfo->isDot()) {
continue;
}
if ('translation.html.php' === $fileInfo->getBasename()) {
$phpFile = $fileInfo->getPathname();
if (\in_array($fileInfo->getBasename(), ['translatable.html.php', 'translatable-fqn.html.php', 'translatable-short.html.php', 'translation.html.php'], true)) {
$phpFiles[] = $fileInfo->getPathname();
}
$splFiles[] = $fileInfo->getFileInfo();
}

return [
[$directory],
[$phpFile],
[$phpFiles],
[glob($directory.'*')],
[$splFiles],
[new \ArrayObject(glob($directory.'*'))],
Expand Down
Loading