Skip to content

Commit 4f1f513

Browse files
committed
itk_translation_extractor
1 parent 6584705 commit 4f1f513

File tree

9 files changed

+170
-60
lines changed

9 files changed

+170
-60
lines changed

drush.services.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ services:
55
- "@twig"
66
- "@extension.path.resolver"
77
- "@language_manager"
8-
- "@file_system"
8+
- "@locale.storage"
9+
- "@Drupal\\itk_translation_extractor\\Translation\\Dumper\\PoFileDumper"
910
tags:
1011
- { name: console.command }

itk_translation_extractor.services.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ services:
22
_defaults:
33
autowire: true
44

5+
Drupal\itk_translation_extractor\Translation\Dumper\PoFileDumper:
6+
57
Drupal\itk_translation_extractor\ItkTranslationExtractorTwigExtension:
68
tags:
79
- { name: twig.extension }

src/Command/TranslationExtractCommand.php

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
namespace Drupal\itk_translation_extractor\Command;
66

77
use Drupal\Core\Extension\ExtensionPathResolver;
8-
use Drupal\Core\File\FileSystemInterface;
98
use Drupal\Core\Language\LanguageManagerInterface;
109
use Drupal\itk_translation_extractor\Translation\Dumper\PoFileDumper;
10+
use Drupal\itk_translation_extractor\Translation\Helper;
1111
use Drupal\itk_translation_extractor\Translation\TwigExtractor;
12+
use Drupal\locale\StringStorageInterface;
1213
use Symfony\Component\Console\Attribute\AsCommand;
1314
use Symfony\Component\Console\Command\Command;
1415
use Symfony\Component\Console\Input\InputArgument;
@@ -18,10 +19,8 @@
1819
use Symfony\Component\Console\Style\SymfonyStyle;
1920
use Symfony\Component\Translation\Catalogue\MergeOperation;
2021
use Symfony\Component\Translation\Catalogue\TargetOperation;
21-
use Symfony\Component\Translation\Loader\PoFileLoader;
2222
use Symfony\Component\Translation\MessageCatalogue;
2323
use Symfony\Component\Translation\MessageCatalogueInterface;
24-
use Symfony\Component\Translation\Reader\TranslationReader;
2524
use Symfony\Component\Translation\Writer\TranslationWriter;
2625
use Twig\Environment;
2726

@@ -57,27 +56,17 @@ final class TranslationExtractCommand extends Command
5756
*/
5857
private TwigExtractor $extractor;
5958

60-
/**
61-
* The reader.
62-
*/
63-
private TranslationReader $reader;
64-
6559
public function __construct(
66-
// #[Autowire(service: 'twig')]
6760
private readonly Environment $twig,
6861
private readonly ExtensionPathResolver $extensionPathResolver,
6962
private readonly LanguageManagerInterface $languageManager,
70-
FileSystemInterface $fileSystem,
63+
private readonly StringStorageInterface $stringStorage,
64+
PoFileDumper $poFileDumper,
7165
) {
7266
$this->extractor = new TwigExtractor($this->twig);
7367

74-
$this->reader = new TranslationReader();
75-
$this->reader->addLoader('po', new PoFileLoader());
76-
7768
$this->writer = new TranslationWriter();
78-
$this->writer->addDumper('po', new PoFileDumper(
79-
fileSystem: $fileSystem,
80-
));
69+
$this->writer->addDumper('po', $poFileDumper);
8170

8271
parent::__construct();
8372
}
@@ -212,7 +201,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
212201
// $currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $transPaths);
213202

214203
$io->comment('Loading translated messages...');
215-
$currentCatalogue = $this->loadTranslatedMessages($input->getArgument('locale'), $extractedCatalogue);
204+
$currentCatalogue = $this->loadTranslatedMessages($extractedCatalogue);
216205

217206
// if (null !== $domain = $input->getOption('domain')) {
218207
// $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain);
@@ -447,23 +436,29 @@ private function filterDuplicateTransPaths(array $transPaths): array
447436
return $filteredPaths;
448437
}
449438

450-
private function loadTranslatedMessages(string $locale, MessageCatalogue $extractedCatalogue)
451-
{
452-
$currentCatalogue = new MessageCatalogue($locale);
453-
454-
return $currentCatalogue;
455-
}
456-
457-
private function loadCurrentMessages(string $locale, array $transPaths): MessageCatalogue
439+
private function loadTranslatedMessages(MessageCatalogue $extractedCatalogue)
458440
{
459-
// @todo Load existing translations from database.
460-
$currentCatalogue = new MessageCatalogue($locale);
461-
foreach ($transPaths as $path) {
462-
if (is_dir($path)) {
463-
$this->reader->read($path, $currentCatalogue);
441+
$currentCatalogue = new MessageCatalogue($extractedCatalogue->getLocale());
442+
443+
foreach ($extractedCatalogue->getDomains() as $domain) {
444+
$translations = $this->stringStorage->getTranslations([
445+
'language' => $currentCatalogue->getLocale(),
446+
'context' => Helper::getContext($domain),
447+
]);
448+
// Index by source
449+
$translations = array_column($translations, null, 'source');
450+
foreach ($extractedCatalogue->all($domain) as $source => $_) {
451+
if ($translation = ($translations[$source] ?? null)) {
452+
if ($string = $translation->getString()) {
453+
$currentCatalogue->set($source, $string, $domain);
454+
$currentCatalogue->setMetadata($source, ['plurals' => $translation->getPlurals()], $domain);
455+
}
456+
}
464457
}
465458
}
466459

460+
// Load from database
461+
467462
return $currentCatalogue;
468463
}
469464

src/NodeVisitor/TranslationNodeVisitor.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
*/
2929
final class TranslationNodeVisitor implements NodeVisitorInterface
3030
{
31-
public const UNDEFINED_DOMAIN = '';
32-
3331
private bool $enabled = false;
3432
private array $messages = [];
3533

@@ -176,7 +174,7 @@ private function getReadDomainFromNode(Node $node): ?string
176174
}
177175
}
178176

179-
return self::UNDEFINED_DOMAIN;
177+
return null;
180178
}
181179

182180
private function getConcatValueFromNode(Node $node, ?string $value): ?string

src/Translation/Dumper/PoFileDumper.php

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
use Drupal\Component\Gettext\PoHeader;
66
use Drupal\Component\Gettext\PoItem;
77
use Drupal\Component\Gettext\PoStreamWriter;
8+
use Drupal\Core\Language\LanguageManagerInterface;
9+
use Drupal\itk_translation_extractor\Translation\Helper;
10+
use Drupal\locale\PluralFormulaInterface;
811
use Symfony\Component\Translation\Dumper\PoFileDumper as BasePoFileDumper;
912
use Symfony\Component\Translation\Exception\InvalidArgumentException;
1013
use Symfony\Component\Translation\MessageCatalogue;
@@ -14,6 +17,12 @@
1417
*/
1518
class PoFileDumper extends BasePoFileDumper
1619
{
20+
public function __construct(
21+
private readonly LanguageManagerInterface $languageManager,
22+
private readonly PluralFormulaInterface $pluralFormula,
23+
) {
24+
}
25+
1726
public function dump(MessageCatalogue $messages, array $options = []): void
1827
{
1928
if (!\array_key_exists('path', $options)) {
@@ -33,38 +42,38 @@ public function formatCatalogue(
3342
string $domain,
3443
array $options = [],
3544
): string {
45+
$language = $this->languageManager->getLanguage($messages->getLocale());
46+
if (!$language) {
47+
throw new InvalidArgumentException(sprintf('Invalid locale: %s', $messages->getLocale()));
48+
}
49+
$pluralFormula = $this->pluralFormula->getFormula($language->getId());
50+
$numberOfPlurals = count($pluralFormula);
51+
3652
$header = new PoHeader();
37-
// Set Plural-Forms
38-
$spec = '';
39-
switch ($messages->getLocale()) {
40-
case 'da':
41-
$spec = 'Plural-Forms: nplurals=2; plural=(n != 1);';
53+
if ($pluralForm = $this->getPluralForm($messages->getLocale())) {
54+
// Set Plural-Forms
55+
$header->setFromString(sprintf('Plural-Forms: %s;', $pluralForm));
4256
}
43-
$header->setFromString($spec);
44-
if ($languageName = ($options['language_name'] ?? null)) {
45-
$header->setLanguageName($languageName);
57+
if ($language) {
58+
$header->setLanguageName($language->getName());
4659
}
4760
if ($projectName = ($options['project_name'] ?? null)) {
4861
$header->setProjectName($projectName);
4962
}
50-
$uri = tempnam(sys_get_temp_dir(), 'po_');
5163

64+
$uri = tempnam(sys_get_temp_dir(), 'po_');
5265
$writer = new PoStreamWriter();
5366
$writer->setURI($uri);
5467
$writer->setHeader($header);
5568
$writer->open();
5669
foreach ($messages->getDomains() as $domain) {
5770
foreach ($messages->all($domain) as $source => $translation) {
5871
$metadata = $messages->getMetadata($source, $domain);
59-
// MessageCatalog has a special case for the empty domain.
60-
if ('' === $domain) {
61-
$metadata = $metadata[$domain][$source] ?? null;
62-
}
6372
$item = new PoItem();
73+
$item->setContext(Helper::getContext($domain));
6474
if ($plurals = ($metadata['plurals'] ?? null)) {
6575
$item->setPlural(true);
6676
$item->setSource($plurals);
67-
// @todo!!!
6877
$item->setTranslation($plurals);
6978
} else {
7079
$item->setSource($source);
@@ -81,4 +90,99 @@ public function formatCatalogue(
8190

8291
return $output;
8392
}
93+
94+
public function getLanguageName(string $langcode): ?string
95+
{
96+
return self::PLURAL_TABLE[$langcode][0] ?? null;
97+
}
98+
99+
/**
100+
* Get plural form.
101+
*/
102+
public function getPluralForm(string $langcode): ?string
103+
{
104+
return self::PLURAL_TABLE[$langcode][1] ?? null;
105+
}
106+
107+
// Lifted from
108+
// https://gitweb.git.savannah.gnu.org/gitweb/?p=gettext.git;a=blob;f=gettext-tools/src/plural-table.c;h=be87373a0aa59ef1eb04b0b1dba43dbb330f1afb;hb=HEAD
109+
// (found via
110+
// https://git.drupalcode.org/project/potx/-/blob/8.x-1.x/potx.inc?ref_type=heads#L655).
111+
private const array PLURAL_TABLE = [
112+
'ja' => ['Japanese', 'nplurals=1; plural=0;'],
113+
'vi' => ['Vietnamese', 'nplurals=1; plural=0;'],
114+
'ko' => ['Korean', 'nplurals=1; plural=0;'],
115+
'en' => ['English', 'nplurals=2; plural=(n != 1);'],
116+
'de' => ['German', 'nplurals=2; plural=(n != 1);'],
117+
'nl' => ['Dutch', 'nplurals=2; plural=(n != 1);'],
118+
'sv' => ['Swedish', 'nplurals=2; plural=(n != 1);'],
119+
'da' => ['Danish', 'nplurals=2; plural=(n != 1);'],
120+
'no' => ['Norwegian', 'nplurals=2; plural=(n != 1);'],
121+
'nb' => ['Norwegian Bokmal', 'nplurals=2; plural=(n != 1);'],
122+
'nn' => ['Norwegian Nynorsk', 'nplurals=2; plural=(n != 1);'],
123+
'fo' => ['Faroese', 'nplurals=2; plural=(n != 1);'],
124+
'es' => ['Spanish', 'nplurals=2; plural=(n != 1);'],
125+
'pt' => ['Portuguese', 'nplurals=2; plural=(n != 1);'],
126+
'it' => ['Italian', 'nplurals=2; plural=(n != 1);'],
127+
'bg' => ['Bulgarian', 'nplurals=2; plural=(n != 1);'],
128+
'el' => ['Greek', 'nplurals=2; plural=(n != 1);'],
129+
'fi' => ['Finnish', 'nplurals=2; plural=(n != 1);'],
130+
'et' => ['Estonian', 'nplurals=2; plural=(n != 1);'],
131+
'he' => ['Hebrew', 'nplurals=2; plural=(n != 1);'],
132+
'eo' => ['Esperanto', 'nplurals=2; plural=(n != 1);'],
133+
'hu' => ['Hungarian', 'nplurals=2; plural=(n != 1);'],
134+
'tr' => ['Turkish', 'nplurals=2; plural=(n != 1);'],
135+
'ca' => ['Catalan', 'nplurals=2; plural=(n != 1);'],
136+
'pt_BR' => ['Brazilian', 'nplurals=2; plural=(n > 1);'],
137+
'fr' => ['French', 'nplurals=2; plural=(n > 1);'],
138+
'lv' => [
139+
'Latvian',
140+
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);',
141+
],
142+
'ga' => ['Irish', 'nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;'],
143+
'ro' => [
144+
'Romanian',
145+
'nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;',
146+
],
147+
'lt' => [
148+
'Lithuanian',
149+
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);',
150+
],
151+
'ru' => [
152+
'Russian',
153+
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
154+
],
155+
'uk' => [
156+
'Ukrainian',
157+
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
158+
],
159+
'be' => [
160+
'Belarusian',
161+
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
162+
],
163+
'sr' => [
164+
'Serbian',
165+
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
166+
],
167+
'hr' => [
168+
'Croatian',
169+
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
170+
],
171+
'cs' => [
172+
'Czech',
173+
'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;',
174+
],
175+
'sk' => [
176+
'Slovak',
177+
'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;',
178+
],
179+
'pl' => [
180+
'Polish',
181+
'nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
182+
],
183+
'sl' => [
184+
'Slovenian',
185+
'nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);',
186+
],
187+
];
84188
}

src/Translation/Extractor/Visitor/TransMethodVisitor.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

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

5+
use Drupal\itk_translation_extractor\Translation\Helper;
56
use PhpParser\Node;
67
use PhpParser\NodeVisitor;
78
use Symfony\Component\Translation\Extractor\Visitor\AbstractVisitor;
@@ -44,13 +45,13 @@ public function leaveNode(Node $node): ?Node
4445
return null;
4546
}
4647

47-
$context = '';
48+
$context = null;
4849
if ($options = $this->getArrayArgument($node, 2 < $firstNamedArgumentIndex ? 2 : 'options')) {
49-
$context = $this->getArrayStringValue($options, 'context') ?? '';
50+
$context = $this->getArrayStringValue($options, 'context');
5051
}
5152

5253
foreach ($messages as $message) {
53-
$this->addMessageToCatalogue($message, $context, $node->getStartLine());
54+
$this->addMessageToCatalogue($message, $context ?? Helper::UNDEFINED_DOMAIN, $node->getStartLine());
5455
}
5556
}
5657

src/Translation/Extractor/Visitor/TranslatableMarkupVisitor.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

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

5+
use Drupal\itk_translation_extractor\Translation\Helper;
56
use PhpParser\Node;
67
use PhpParser\NodeVisitor;
78
use Symfony\Component\Translation\Extractor\Visitor\AbstractVisitor;
@@ -45,13 +46,13 @@ public function leaveNode(Node $node): ?Node
4546
return null;
4647
}
4748

48-
$context = '';
49+
$context = null;
4950
if ($options = $this->getArrayArgument($node, 2 < $firstNamedArgumentIndex ? 2 : 'options')) {
50-
$context = $this->getArrayStringValue($options, 'context') ?? '';
51+
$context = $this->getArrayStringValue($options, 'context');
5152
}
5253

5354
foreach ($messages as $message) {
54-
$this->addMessageToCatalogue($message, $context, $node->getStartLine());
55+
$this->addMessageToCatalogue($message, $context ?? Helper::UNDEFINED_DOMAIN, $node->getStartLine());
5556
}
5657

5758
return null;

src/Translation/Helper.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Drupal\itk_translation_extractor\Translation;
4+
5+
class Helper
6+
{
7+
public const string UNDEFINED_DOMAIN = '__no_context__';
8+
9+
public static function getContext(?string $domain): string
10+
{
11+
return null === $domain || self::UNDEFINED_DOMAIN === $domain ? '' : $domain;
12+
}
13+
}

0 commit comments

Comments
 (0)