Skip to content

Commit b2c9cbc

Browse files
Merge pull request #19126 from MauricioFauth/i18n-trans-function
Add `t` (trans) function to Twig\I18nExtension
2 parents accafad + b920681 commit b2c9cbc

8 files changed

Lines changed: 671 additions & 1 deletion

File tree

psalm-baseline.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14959,6 +14959,12 @@
1495914959
<code><![CDATA[providerGetQueryFromRequest]]></code>
1496014960
</PossiblyUnusedMethod>
1496114961
</file>
14962+
<file src="tests/unit/Twig/Node/Expression/TransExpressionTest.php">
14963+
<PossiblyUnusedMethod>
14964+
<code><![CDATA[transExpressionsProvider]]></code>
14965+
<code><![CDATA[transExpressionsWithErrorProvider]]></code>
14966+
</PossiblyUnusedMethod>
14967+
</file>
1496214968
<file src="tests/unit/TwoFactorTest.php">
1496314969
<DeprecatedMethod>
1496414970
<code><![CDATA[Config::getInstance()]]></code>

src/Twig/I18nExtension.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
use PhpMyAdmin\Twig\Extensions\I18nExtension as TwigI18nExtension;
88
use PhpMyAdmin\Twig\Extensions\Node\TransNode;
9+
use PhpMyAdmin\Twig\Node\Expression\TransExpression;
910
use Twig\TwigFilter;
11+
use Twig\TwigFunction;
12+
13+
use function _gettext;
1014

1115
class I18nExtension extends TwigI18nExtension
1216
{
@@ -25,7 +29,13 @@ public function getFilters(): array
2529
{
2630
return [
2731
// This is just a performance override
28-
new TwigFilter('trans', '_gettext'),
32+
new TwigFilter('trans', _gettext(...)),
2933
];
3034
}
35+
36+
/** @inheritdoc */
37+
public function getFunctions(): array
38+
{
39+
return [new TwigFunction('t', null, ['node_class' => TransExpression::class])];
40+
}
3141
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Twig\Node\Expression;
6+
7+
use Twig\Compiler;
8+
use Twig\Error\SyntaxError;
9+
use Twig\Node\Expression\AbstractExpression;
10+
use Twig\Node\Expression\ConstantExpression;
11+
use Twig\Node\Node;
12+
13+
use function in_array;
14+
use function is_string;
15+
use function sprintf;
16+
use function str_replace;
17+
use function trim;
18+
19+
final class TransExpression extends AbstractExpression
20+
{
21+
public function __construct(string $name, Node $arguments, int $lineno)
22+
{
23+
parent::__construct(['arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno);
24+
}
25+
26+
/** @throws SyntaxError */
27+
public function compile(Compiler $compiler): void
28+
{
29+
$parameters = $this->getParameters();
30+
31+
if (isset($parameters['notes'])) {
32+
$this->compileNotes($compiler, $parameters);
33+
}
34+
35+
if (
36+
isset($parameters['singular'])
37+
|| isset($parameters['plural'])
38+
|| isset($parameters['count'])
39+
|| isset($parameters[1])
40+
|| isset($parameters[2])
41+
) {
42+
$this->compilePlural($compiler, $parameters);
43+
44+
return;
45+
}
46+
47+
if (isset($parameters['context'])) {
48+
$this->compileContext($compiler, $parameters);
49+
50+
return;
51+
}
52+
53+
$this->compileMessage($compiler, $parameters);
54+
}
55+
56+
/**
57+
* @return Node[]
58+
*
59+
* @throws SyntaxError
60+
*/
61+
private function getParameters(): array
62+
{
63+
$parameters = [];
64+
65+
/**
66+
* @var int|string $name
67+
* @var Node $argument
68+
*/
69+
foreach ($this->getNode('arguments') as $name => $argument) {
70+
if (! in_array($name, [0, 1, 2, 'message', 'singular', 'plural', 'count', 'context', 'notes'], true)) {
71+
throw $this->unknownArgumentSyntaxError($name);
72+
}
73+
74+
$parameters[$name] = $argument;
75+
}
76+
77+
return $parameters;
78+
}
79+
80+
/**
81+
* @param Node[] $parameters
82+
*
83+
* @throws SyntaxError
84+
*/
85+
private function compileNotes(Compiler $compiler, array $parameters): void
86+
{
87+
$notes = $this->getStringFromNode('notes', $parameters['notes'] ?? null);
88+
89+
// line breaks are not allowed because we want a single line comment
90+
$notes = trim(str_replace(["\n", "\r"], ' ', $notes));
91+
if ($notes === '') {
92+
throw $this->nonEmptyLiteralStringSyntaxError('notes');
93+
}
94+
95+
$compiler->raw("\n// l10n: " . $notes . "\n");
96+
}
97+
98+
/**
99+
* @param Node[] $parameters
100+
*
101+
* @throws SyntaxError
102+
*/
103+
private function compilePlural(Compiler $compiler, array $parameters): void
104+
{
105+
if (isset($parameters[0], $parameters['singular'])) {
106+
throw $this->duplicateArgumentSyntaxError('singular');
107+
}
108+
109+
if (isset($parameters[1], $parameters['plural'])) {
110+
throw $this->duplicateArgumentSyntaxError('plural');
111+
}
112+
113+
if (isset($parameters[2], $parameters['count'])) {
114+
throw $this->duplicateArgumentSyntaxError('count');
115+
}
116+
117+
$singular = $this->getStringFromNode('singular', $parameters[0] ?? $parameters['singular'] ?? null);
118+
$plural = $this->getStringFromNode('plural', $parameters[1] ?? $parameters['plural'] ?? null);
119+
$count = $parameters[2] ?? $parameters['count'] ?? null;
120+
if ($count === null) {
121+
throw $this->missingArgumentSyntaxError('count');
122+
}
123+
124+
$compiler->raw('\\_ngettext(');
125+
$compiler->string($singular);
126+
$compiler->raw(', ');
127+
$compiler->string($plural);
128+
$compiler->raw(', ');
129+
$compiler->subcompile($count);
130+
$compiler->raw(isset($parameters['notes']) ? ")\n" : ')');
131+
}
132+
133+
/**
134+
* @param Node[] $parameters
135+
*
136+
* @throws SyntaxError
137+
*/
138+
private function compileContext(Compiler $compiler, array $parameters): void
139+
{
140+
if (isset($parameters[0], $parameters['message'])) {
141+
throw $this->duplicateArgumentSyntaxError('message');
142+
}
143+
144+
$message = $this->getStringFromNode('message', $parameters[0] ?? $parameters['message'] ?? null);
145+
$context = $this->getStringFromNode('context', $parameters['context'] ?? null);
146+
147+
$compiler->raw('\\_pgettext(');
148+
$compiler->string($context);
149+
$compiler->raw(', ');
150+
$compiler->string($message);
151+
$compiler->raw(isset($parameters['notes']) ? ")\n" : ')');
152+
}
153+
154+
/**
155+
* @param Node[] $parameters
156+
*
157+
* @throws SyntaxError
158+
*/
159+
private function compileMessage(Compiler $compiler, array $parameters): void
160+
{
161+
if (isset($parameters[0], $parameters['message'])) {
162+
throw $this->duplicateArgumentSyntaxError('message');
163+
}
164+
165+
$message = $this->getStringFromNode('message', $parameters[0] ?? $parameters['message'] ?? null);
166+
167+
$compiler->raw('\\_gettext(');
168+
$compiler->string($message);
169+
$compiler->raw(isset($parameters['notes']) ? ")\n" : ')');
170+
}
171+
172+
/**
173+
* @psalm-return non-empty-string
174+
*
175+
* @throws SyntaxError
176+
*/
177+
private function getStringFromNode(int|string $name, Node|null $node): string
178+
{
179+
if (! ($node instanceof ConstantExpression)) {
180+
throw $this->nonEmptyLiteralStringSyntaxError($name);
181+
}
182+
183+
$value = $node->getAttribute('value');
184+
if (! is_string($value) || $value === '') {
185+
throw $this->nonEmptyLiteralStringSyntaxError($name);
186+
}
187+
188+
return $value;
189+
}
190+
191+
private function unknownArgumentSyntaxError(int|string $name): SyntaxError
192+
{
193+
return new SyntaxError(
194+
sprintf('Unknown argument "%s".', $name),
195+
$this->getTemplateLine(),
196+
$this->getSourceContext(),
197+
);
198+
}
199+
200+
private function duplicateArgumentSyntaxError(int|string $name): SyntaxError
201+
{
202+
return new SyntaxError(
203+
sprintf('Argument "%s" is defined twice.', $name),
204+
$this->getTemplateLine(),
205+
$this->getSourceContext(),
206+
);
207+
}
208+
209+
private function nonEmptyLiteralStringSyntaxError(int|string $name): SyntaxError
210+
{
211+
return new SyntaxError(
212+
sprintf('Value for argument "%s" must be a non-empty literal string.', $name),
213+
$this->getTemplateLine(),
214+
$this->getSourceContext(),
215+
);
216+
}
217+
218+
private function missingArgumentSyntaxError(int|string $name): SyntaxError
219+
{
220+
return new SyntaxError(
221+
sprintf('Value for argument "%s" is required.', $name),
222+
$this->getTemplateLine(),
223+
$this->getSourceContext(),
224+
);
225+
}
226+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Tests\Twig;
6+
7+
use PhpMyAdmin\Config;
8+
use PhpMyAdmin\Template;
9+
use PhpMyAdmin\Tests\AbstractTestCase;
10+
use PhpMyAdmin\Twig\I18nExtension;
11+
use PhpMyAdmin\Twig\Node\Expression\TransExpression;
12+
use PHPUnit\Framework\Attributes\CoversClass;
13+
use ReflectionProperty;
14+
use Twig\Loader\FilesystemLoader;
15+
16+
use const TEST_PATH;
17+
18+
#[CoversClass(I18nExtension::class)]
19+
#[CoversClass(TransExpression::class)]
20+
final class I18nExtensionTest extends AbstractTestCase
21+
{
22+
protected function setUp(): void
23+
{
24+
parent::setUp();
25+
26+
$twigEnvironment = Template::getTwigEnvironment(null, true);
27+
$twigEnvironment->setLoader(new FilesystemLoader(TEST_PATH . 'tests/unit/_data/templates'));
28+
(new ReflectionProperty(Template::class, 'twig'))->setValue(null, $twigEnvironment);
29+
}
30+
31+
protected function tearDown(): void
32+
{
33+
parent::tearDown();
34+
35+
(new ReflectionProperty(Template::class, 'twig'))->setValue(null, null);
36+
}
37+
38+
public function testMessage(): void
39+
{
40+
$expected = <<<'HTML'
41+
Message
42+
Message
43+
Message
44+
Message
45+
46+
HTML;
47+
48+
self::assertSame($expected, (new Template(new Config()))->render('i18n_extension/message', []));
49+
}
50+
51+
public function testContext(): void
52+
{
53+
$expected = <<<'HTML'
54+
Message
55+
Message
56+
Message
57+
Message
58+
59+
HTML;
60+
61+
self::assertSame($expected, (new Template(new Config()))->render('i18n_extension/context', []));
62+
}
63+
64+
public function testPlural(): void
65+
{
66+
$expected = <<<'HTML'
67+
One apple
68+
One apple
69+
One apple
70+
One apple
71+
One apple
72+
One apple
73+
74+
HTML;
75+
76+
self::assertSame(
77+
$expected,
78+
(new Template(new Config()))->render('i18n_extension/plural', ['number_of_apples' => 1]),
79+
);
80+
}
81+
82+
public function testPlural2(): void
83+
{
84+
$expected = <<<'HTML'
85+
2 apples
86+
2 apples
87+
2 apples
88+
2 apples
89+
2 apples
90+
2 apples
91+
92+
HTML;
93+
94+
self::assertSame(
95+
$expected,
96+
(new Template(new Config()))->render('i18n_extension/plural', ['number_of_apples' => 2]),
97+
);
98+
}
99+
}

0 commit comments

Comments
 (0)