Skip to content

Commit bc7aaf8

Browse files
committed
itk_translation_extractor
1 parent 17cb121 commit bc7aaf8

File tree

12 files changed

+233
-107
lines changed

12 files changed

+233
-107
lines changed

Taskfile.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ tasks:
2727
vars:
2828
TASK_ARGS: run --rm phpfpm vendor/bin/phpunit
2929

30+
xdebug:test:
31+
cmds:
32+
- PHP_XDEBUG_MODE=debug PHP_XDEBUG_WITH_REQUEST=yes PHP_IDE_CONFIG=serverName=localhost docker compose run --env PHP_XDEBUG_MODE --env PHP_XDEBUG_WITH_REQUEST --env PHP_IDE_CONFIG --rm phpfpm vendor/bin/phpunit {{.CLI_ARGS}}
33+
3034
coding-standards:apply:
3135
desc: "Apply coding standards"
3236
cmds:
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace Drupal\itk_translation_extractor\Translation\Extractor\Visitor;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitor;
7+
use Symfony\Component\Translation\Extractor\Visitor\AbstractVisitor as BaseAbstractVisitor;
8+
use Symfony\Component\Translation\MessageCatalogue;
9+
10+
abstract class AbstractVisitor extends BaseAbstractVisitor implements NodeVisitor
11+
{
12+
use ArrayValueTrait;
13+
14+
private MessageCatalogue $catalogue;
15+
16+
public function initialize(MessageCatalogue $catalogue, \SplFileInfo $file, string $messagePrefix): void
17+
{
18+
parent::initialize($catalogue, $file, $messagePrefix);
19+
// Lift the private property from the parent.
20+
$this->catalogue = $catalogue;
21+
}
22+
23+
protected function addMetadataToCatalogue(string $message, array $values, string $domain = 'messages'): void
24+
{
25+
$metadata = $this->catalogue->getMetadata($message, $domain) ?? [];
26+
$metadata += $values;
27+
$this->catalogue->setMetadata($message, $metadata, $domain);
28+
}
29+
30+
public function beforeTraverse(array $nodes): ?Node
31+
{
32+
return null;
33+
}
34+
35+
public function enterNode(Node $node): ?Node
36+
{
37+
return null;
38+
}
39+
40+
public function afterTraverse(array $nodes): ?Node
41+
{
42+
return null;
43+
}
44+
45+
protected function getArrayArgument(
46+
Node\Expr\CallLike|Node\Attribute|Node\Expr\New_ $node,
47+
int|string $index,
48+
bool $indexIsRegex = false,
49+
): ?Node\Expr\Array_ {
50+
if (\is_string($index)) {
51+
return $this->getArrayNamedArgument($node, $index, $indexIsRegex);
52+
}
53+
54+
$args = $node instanceof Node\Expr\CallLike ? $node->getRawArgs() : $node->args;
55+
56+
if (($arg = $args[$index] ?? null) instanceof Node\Arg) {
57+
return $arg?->value instanceof Node\Expr\Array_ ? $arg->value : null;
58+
}
59+
60+
return null;
61+
}
62+
63+
protected function getArrayNamedArgument(
64+
Node\Expr\CallLike|Node\Attribute $node,
65+
?string $argumentName = null,
66+
bool $isArgumentNamePattern = false,
67+
): ?Node\Expr\Array_ {
68+
$args = $node instanceof Node\Expr\CallLike ? $node->getArgs() : $node->args;
69+
70+
foreach ($args as $arg) {
71+
if (!$isArgumentNamePattern && $arg->name?->toString() === $argumentName) {
72+
return $arg->value instanceof Node\Expr\Array_ ? $arg->value : null;
73+
} elseif ($isArgumentNamePattern && preg_match($argumentName,
74+
$arg->name?->toString() ?? '') > 0) {
75+
return $arg->value instanceof Node\Expr\Array_ ? $arg->value : null;
76+
}
77+
}
78+
79+
return null;
80+
}
81+
82+
protected function getArrayStringValue(Node\Expr\Array_ $array, string $key): ?string
83+
{
84+
foreach ($array->items as $item) {
85+
if ($item->key instanceof Node\Scalar\String_ && $item->key->value === $key) {
86+
return $item->value instanceof Node\Scalar\String_ ? $item->value->value : null;
87+
}
88+
}
89+
90+
return null;
91+
}
92+
}

src/Translation/Extractor/Visitor/ArrayValueTrait.php

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,6 @@
22

33
namespace Drupal\itk_translation_extractor\Translation\Extractor\Visitor;
44

5-
use PhpParser\Node;
6-
75
trait ArrayValueTrait
86
{
9-
private function getArrayArgument(
10-
Node\Expr\CallLike|Node\Attribute|Node\Expr\New_ $node,
11-
int|string $index,
12-
bool $indexIsRegex = false,
13-
): ?Node\Expr\Array_ {
14-
if (\is_string($index)) {
15-
return $this->getArrayNamedArgument($node, $index, $indexIsRegex);
16-
}
17-
18-
$args = $node instanceof Node\Expr\CallLike ? $node->getRawArgs() : $node->args;
19-
20-
if (($arg = $args[$index] ?? null) instanceof Node\Arg) {
21-
return $arg?->value instanceof Node\Expr\Array_ ? $arg->value : null;
22-
}
23-
24-
return null;
25-
}
26-
27-
private function getArrayNamedArgument(
28-
Node\Expr\CallLike|Node\Attribute $node,
29-
?string $argumentName = null,
30-
bool $isArgumentNamePattern = false,
31-
): ?Node\Expr\Array_ {
32-
$args = $node instanceof Node\Expr\CallLike ? $node->getArgs() : $node->args;
33-
34-
foreach ($args as $arg) {
35-
if (!$isArgumentNamePattern && $arg->name?->toString() === $argumentName) {
36-
return $arg->value instanceof Node\Expr\Array_ ? $arg->value : null;
37-
} elseif ($isArgumentNamePattern && preg_match($argumentName,
38-
$arg->name?->toString() ?? '') > 0) {
39-
return $arg->value instanceof Node\Expr\Array_ ? $arg->value : null;
40-
}
41-
}
42-
43-
return null;
44-
}
45-
46-
private function getArrayStringValue(Node\Expr\Array_ $array, string $key): ?string
47-
{
48-
foreach ($array->items as $item) {
49-
if ($item->key instanceof Node\Scalar\String_ && $item->key->value === $key) {
50-
return $item->value instanceof Node\Scalar\String_ ? $item->value->value : null;
51-
}
52-
}
53-
54-
return null;
55-
}
567
}

src/Translation/Extractor/Visitor/TransMethodVisitor.php

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,14 @@
44

55
use Drupal\itk_translation_extractor\Translation\Helper;
66
use PhpParser\Node;
7-
use PhpParser\NodeVisitor;
8-
use Symfony\Component\Translation\Extractor\Visitor\AbstractVisitor;
97

108
/**
119
* Lifted from \Symfony\Component\Translation\Extractor\Visitor\TransMethodVisitor.
1210
*
1311
* @see \Symfony\Component\Translation\Extractor\Visitor\TransMethodVisitor.
1412
*/
15-
final class TransMethodVisitor extends AbstractVisitor implements NodeVisitor
13+
final class TransMethodVisitor extends AbstractVisitor
1614
{
17-
use ArrayValueTrait;
18-
19-
public function beforeTraverse(array $nodes): ?Node
20-
{
21-
return null;
22-
}
23-
24-
public function enterNode(Node $node): ?Node
25-
{
26-
return null;
27-
}
28-
2915
public function leaveNode(Node $node): ?Node
3016
{
3117
if (!$node instanceof Node\Expr\MethodCall && !$node instanceof Node\Expr\FuncCall) {
@@ -53,13 +39,29 @@ public function leaveNode(Node $node): ?Node
5339
foreach ($messages as $message) {
5440
$this->addMessageToCatalogue($message, $context ?? Helper::UNDEFINED_DOMAIN, $node->getStartLine());
5541
}
56-
}
42+
} elseif ('formatPlural' === $name) {
43+
// https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21StringTranslation%21TranslationInterface.php/function/TranslationInterface%3A%3AformatPlural/11.x
44+
$firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node);
5745

58-
return null;
59-
}
46+
if (!$singular = $this->getStringArguments($node, 1 < $firstNamedArgumentIndex ? 1 : 'singular')) {
47+
return null;
48+
}
49+
if (!$plural = $this->getStringArguments($node, 2 < $firstNamedArgumentIndex ? 2 : 'plural')) {
50+
return null;
51+
}
52+
53+
$context = null;
54+
if ($options = $this->getArrayArgument($node, 4 < $firstNamedArgumentIndex ? 4 : 'options')) {
55+
$context = $this->getArrayStringValue($options, 'context');
56+
}
57+
$context ??= Helper::UNDEFINED_DOMAIN;
58+
59+
foreach ($singular as $index => $message) {
60+
$this->addMessageToCatalogue($message, $context, $node->getStartLine());
61+
$this->addMetadataToCatalogue($message, ['plurals' => [$message, $plural[$index]]], $context);
62+
}
63+
}
6064

61-
public function afterTraverse(array $nodes): ?Node
62-
{
6365
return null;
6466
}
6567
}

src/Translation/Extractor/Visitor/TranslatableMarkupVisitor.php

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Drupal\itk_translation_extractor\Translation\Helper;
66
use PhpParser\Node;
77
use PhpParser\NodeVisitor;
8-
use Symfony\Component\Translation\Extractor\Visitor\AbstractVisitor;
98

109
/**
1110
* Lifted from \Symfony\Component\Translation\Extractor\Visitor\TranslatableMessageVisitor.
@@ -14,18 +13,6 @@
1413
*/
1514
final class TranslatableMarkupVisitor extends AbstractVisitor implements NodeVisitor
1615
{
17-
use ArrayValueTrait;
18-
19-
public function beforeTraverse(array $nodes): ?Node
20-
{
21-
return null;
22-
}
23-
24-
public function enterNode(Node $node): ?Node
25-
{
26-
return null;
27-
}
28-
2916
public function leaveNode(Node $node): ?Node
3017
{
3118
if (!$node instanceof Node\Expr\New_) {
@@ -36,30 +23,45 @@ public function leaveNode(Node $node): ?Node
3623
return null;
3724
}
3825

39-
if (!\in_array('TranslatableMarkup', $className->getParts(), true)) {
40-
return null;
41-
}
26+
if (\in_array('TranslatableMarkup', $className->getParts(), true)) {
27+
$firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node);
4228

43-
$firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node);
29+
if (!$messages = $this->getStringArguments($node, 0 < $firstNamedArgumentIndex ? 0 : 'string')) {
30+
return null;
31+
}
4432

45-
if (!$messages = $this->getStringArguments($node, 0 < $firstNamedArgumentIndex ? 0 : 'string')) {
46-
return null;
47-
}
33+
$context = null;
34+
if ($options = $this->getArrayArgument($node, 2 < $firstNamedArgumentIndex ? 2 : 'options')) {
35+
$context = $this->getArrayStringValue($options, 'context');
36+
}
4837

49-
$context = null;
50-
if ($options = $this->getArrayArgument($node, 2 < $firstNamedArgumentIndex ? 2 : 'options')) {
51-
$context = $this->getArrayStringValue($options, 'context');
38+
foreach ($messages as $message) {
39+
$this->addMessageToCatalogue($message, $context ?? Helper::UNDEFINED_DOMAIN, $node->getStartLine());
40+
}
5241
}
5342

54-
foreach ($messages as $message) {
55-
$this->addMessageToCatalogue($message, $context ?? Helper::UNDEFINED_DOMAIN, $node->getStartLine());
56-
}
43+
if (\in_array('PluralTranslatableMarkup', $className->getParts(), true)) {
44+
$firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node);
5745

58-
return null;
59-
}
46+
if (!$singular = $this->getStringArguments($node, 1 < $firstNamedArgumentIndex ? 1 : 'singular')) {
47+
return null;
48+
}
49+
if (!$plural = $this->getStringArguments($node, 2 < $firstNamedArgumentIndex ? 2 : 'plural')) {
50+
return null;
51+
}
52+
53+
$context = null;
54+
if ($options = $this->getArrayArgument($node, 4 < $firstNamedArgumentIndex ? 4 : 'options')) {
55+
$context = $this->getArrayStringValue($options, 'context');
56+
}
57+
$context ??= Helper::UNDEFINED_DOMAIN;
58+
59+
foreach ($singular as $index => $message) {
60+
$this->addMessageToCatalogue($message, $context, $node->getStartLine());
61+
$this->addMetadataToCatalogue($message, ['plurals' => [$message, $plural[$index]]], $context);
62+
}
63+
}
6064

61-
public function afterTraverse(array $nodes): ?Node
62-
{
6365
return null;
6466
}
6567
}
Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Drupal\itk_translation_extractor\Test\unit\Extractor;
5+
namespace Drupal\itk_translation_extractor\Test\Unit\Extractor;
66

77
use Drupal\itk_translation_extractor\Translation\Extractor\PhpExtractor;
88
use Drupal\itk_translation_extractor\Translation\Extractor\Visitor\TranslatableMarkupVisitor;
@@ -20,7 +20,7 @@ public function testTransMethod(): void
2020
];
2121
$extractor = new PhpExtractor($visitors);
2222
$resource = [
23-
__DIR__.'/resources/MyClass.php',
23+
__DIR__.'/resources/src/MyClass.php',
2424
];
2525
$locale = 'da';
2626
$messages = new MessageCatalogue($locale);
@@ -37,13 +37,62 @@ public function testTransMethod(): void
3737
$this->assertContains('another context', $domains);
3838
}
3939

40+
public function testTransMethodDrupal(): void
41+
{
42+
$visitors = [
43+
new TransMethodVisitor(),
44+
];
45+
$extractor = new PhpExtractor($visitors);
46+
$resource = [
47+
__DIR__.'/resources/src/MyClassDrupal.php',
48+
];
49+
$locale = 'da';
50+
$messages = new MessageCatalogue($locale);
51+
$extractor->extract($resource, $messages);
52+
53+
$domains = $messages->getDomains();
54+
55+
$this->assertCount(3, $domains);
56+
$this->assertContains(Helper::UNDEFINED_DOMAIN, $domains);
57+
$this->assertCount(1, $messages->all(Helper::UNDEFINED_DOMAIN));
58+
$this->assertCount(1, $messages->all('the context'));
59+
$this->assertContains('the context', $domains);
60+
$this->assertCount(1, $messages->all('another context'));
61+
$this->assertContains('another context', $domains);
62+
}
63+
4064
public function testTranslatableMarkup(): void
4165
{
4266
$visitors = [
4367
new TranslatableMarkupVisitor(),
4468
];
4569
$resource = [
46-
__DIR__.'/resources/MyClass.php',
70+
__DIR__.'/resources/src/MyClass.php',
71+
];
72+
$locale = 'da';
73+
74+
$extractor = new PhpExtractor($visitors);
75+
$messages = new MessageCatalogue($locale);
76+
$extractor->extract($resource, $messages);
77+
78+
$domains = $messages->getDomains();
79+
80+
$this->assertCount(3, $domains);
81+
$this->assertContains(Helper::UNDEFINED_DOMAIN, $domains);
82+
$this->assertCount(1, $messages->all(Helper::UNDEFINED_DOMAIN));
83+
$this->assertContains('the context', $domains);
84+
$this->assertCount(1, $messages->all('the context'));
85+
$this->assertContains('another context', $domains);
86+
$this->assertCount(1, $messages->all('another context'));
87+
}
88+
89+
public function testTranslatableMarkupDrupal(): void
90+
{
91+
$visitors = [
92+
new TranslatableMarkupVisitor(),
93+
];
94+
$resource = [
95+
__DIR__.'/resources/src/MyClassDrupal.php',
4796
];
4897
$locale = 'da';
4998

0 commit comments

Comments
 (0)