Skip to content

Commit 21152d3

Browse files
committed
Cleaned up and improved
1 parent 7fdbeb2 commit 21152d3

File tree

6 files changed

+108
-38
lines changed

6 files changed

+108
-38
lines changed

composer.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
"require-dev": {
1313
"ergebnis/composer-normalize": "^2.48",
1414
"friendsofphp/php-cs-fixer": "^3.91",
15+
"phpstan/phpstan": "^2.1",
1516
"phpunit/phpunit": "^12.5",
16-
"vincentlanglet/twig-cs-fixer": "^3.11",
17-
"phpstan/phpstan": "^2.1"
17+
"vincentlanglet/twig-cs-fixer": "^3.11"
1818
},
1919
"autoload-dev": {
2020
"psr-4": {
2121
"Drupal\\drupal_translation_extractor\\": "src/",
22-
"Drupal\\locale\\": "vendor/drupal/core/modules/locale/src/",
23-
"Drupal\\drupal_translation_extractor\\Test\\": "tests/"
22+
"Drupal\\drupal_translation_extractor\\Test\\": "tests/",
23+
"Drupal\\locale\\": "vendor/drupal/core/modules/locale/src/"
2424
}
2525
},
2626
"config": {

drupal_translation_extractor.services.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ services:
3838

3939
Drupal\drupal_translation_extractor\Translation\Dumper\PoFileDumper:
4040

41+
Drupal\drupal_translation_extractor\Translation\Reader\PoTranslationReader:
42+
4143
drupal_translation_extractor.translation_writer:
4244
class: Symfony\Component\Translation\Writer\TranslationWriter
4345
calls:
@@ -48,3 +50,6 @@ services:
4850
'@Drupal\drupal_translation_extractor\Translation\Dumper\PoFileDumper',
4951
],
5052
]
53+
54+
drupal_translation_extractor.translation_reader:
55+
class: Drupal\Component\Gettext\PoStreamReader

drush.services.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ services:
22
drupal_translation_extractor.translation_extract:
33
class: Drupal\drupal_translation_extractor\Command\TranslationExtractCommand
44
arguments:
5+
- "@drupal_translation_extractor.translation_writer"
6+
- "@drupal_translation_extractor.translation_reader"
7+
- "@drupal_translation_extractor.extractor"
58
- "@extension.path.resolver"
69
- "@locale.storage"
7-
- "@drupal_translation_extractor.extractor"
8-
- "@drupal_translation_extractor.translation_writer"
9-
- "@Drupal\\drupal_translation_extractor\\Translation\\Dumper\\PoFileDumper"
1010
tags:
1111
- { name: console.command }

src/Command/TranslationExtractCommand.php

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace Drupal\drupal_translation_extractor\Command;
66

7+
use Drupal\Component\Gettext\PoStreamReader;
78
use Drupal\Core\Extension\ExtensionPathResolver;
9+
use Drupal\drupal_translation_extractor\Exception\InvalidArgumentException;
810
use Drupal\drupal_translation_extractor\Translation\Dumper\PoItem;
911
use Drupal\locale\StringStorageInterface;
1012
use Symfony\Component\Console\Attribute\AsCommand;
@@ -42,10 +44,11 @@ final class TranslationExtractCommand extends Command
4244
private const NO_FILL_PREFIX = "\0NoFill\0";
4345

4446
public function __construct(
47+
private readonly TranslationWriterInterface $writer,
48+
private PoStreamReader $reader,
49+
private readonly ExtractorInterface $extractor,
4550
private readonly ExtensionPathResolver $extensionPathResolver,
4651
private readonly StringStorageInterface $stringStorage,
47-
private readonly ExtractorInterface $extractor,
48-
private readonly TranslationWriterInterface $writer,
4952
) {
5053
parent::__construct();
5154

@@ -59,7 +62,6 @@ protected function configure(): void
5962
$this
6063
->setDefinition([
6164
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
62-
// new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
6365
new InputOption('prefix', null, InputOption::VALUE_REQUIRED, 'Override the default prefix', '__'),
6466
new InputOption('no-fill', null, InputOption::VALUE_NONE, 'Extract translation keys without filling in values'),
6567
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format', 'po'),
@@ -70,36 +72,33 @@ protected function configure(): void
7072
new InputOption('sort', null, InputOption::VALUE_REQUIRED, 'Return list of messages sorted alphabetically'),
7173
new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
7274

73-
new InputOption('source', null, InputOption::VALUE_REQUIRED, 'Source path.'),
75+
new InputArgument('source', InputArgument::REQUIRED, 'Source path.'),
7476
new InputOption('output', null, InputOption::VALUE_REQUIRED, 'Output path. Required if --force is specified.'),
77+
new InputOption('fill-from-string-storage', null, InputOption::VALUE_NONE, 'Fill translations with values from string storage.'),
7578
])
7679
->setHelp(<<<'EOF'
7780
The <info>%command.name%</info> command extracts translation strings from templates
78-
of a given bundle or the default translations directory. It can display them or merge
81+
of a given module or theme or another path. It can display them or merge
7982
the new ones into the translation files.
8083
8184
When new translation strings are found it can automatically add a prefix to the translation
8285
message. However, if the <comment>--no-fill</comment> option is used, the <comment>--prefix</comment>
8386
option has no effect, since the translation values are left empty.
8487
85-
Example running against a Bundle (AcmeBundle)
88+
Example running against a module (my_module)
8689
87-
<info>php %command.full_name% --dump-messages en AcmeBundle</info>
88-
<info>php %command.full_name% --force --prefix="new_" fr AcmeBundle</info>
90+
<info>php %command.full_name% --dump-messages en module:my_module</info>
91+
<info>php %command.full_name% --force --prefix="new_" fr module:my_module</info>
8992
90-
Example running against default messages directory
93+
Example running against a theme (my_theme)
9194
92-
<info>php %command.full_name% --dump-messages en</info>
93-
<info>php %command.full_name% --force --prefix="new_" fr</info>
95+
<info>php %command.full_name% --dump-messages en theme:my_theme</info>
96+
<info>php %command.full_name% --force --prefix="new_" fr theme:my_theme</info>
9497
9598
You can sort the output with the <comment>--sort</> flag:
9699
97-
<info>php %command.full_name% --dump-messages --sort=asc en AcmeBundle</info>
98-
<info>php %command.full_name% --force --sort=desc fr</info>
99-
100-
You can dump a tree-like structure using the yaml format with <comment>--as-tree</> flag:
101-
102-
<info>php %command.full_name% --force --format=yaml --as-tree=3 en AcmeBundle</info>
100+
<info>php %command.full_name% --dump-messages --sort=asc en …</info>
101+
<info>php %command.full_name% --force --sort=desc fr …</info>
103102

104103
EOF
105104
)
@@ -152,9 +151,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
152151
$extractedCatalogue = $this->extractMessages($input->getArgument('locale'), $codePaths, $prefix);
153152

154153
$io->comment('Loading translated messages...');
155-
$currentCatalogue = true === $input->getOption('no-fill')
156-
? $extractedCatalogue
157-
: $this->loadTranslatedMessages($extractedCatalogue);
154+
$outputPath = $this->getOutputPath($input, $sourceInfo + [
155+
'locale' => $input->getArgument('locale'),
156+
]);
157+
158+
$currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $outputPath);
159+
if (true === $input->getOption('fill-from-string-storage')) {
160+
// Merge in translations from string storage.
161+
$currentCatalogue = new MergeOperation(
162+
$this->loadTranslatedMessages($extractedCatalogue),
163+
$currentCatalogue)->getResult();
164+
}
158165

159166
if (null !== $domain = $input->getOption('domain')) {
160167
if ('' === $domain) {
@@ -211,7 +218,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
211218
sort($list);
212219
}
213220

214-
$io->section(\sprintf('Messages extracted for domain "<info>%s</info>" (%d message%s)', $domain, $domainMessagesCount, $domainMessagesCount > 1 ? 's' : ''));
221+
$io->section(\sprintf('Messages extracted for context "<info>%s</info>" (%d message%s)', PoItem::formatContext($domain), $domainMessagesCount, $domainMessagesCount > 1 ? 's' : ''));
215222
$io->listing($list);
216223

217224
$extractedMessagesCount += $domainMessagesCount;
@@ -238,9 +245,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
238245
$this->removeNoFillTranslations($operationResult);
239246
}
240247

241-
$outputPath = $this->getOutputPath($input, $sourceInfo + [
242-
'locale' => $operationResult->getLocale(),
243-
]);
244248
$dumperOptions = [
245249
'path' => dirname($outputPath),
246250
'output_name' => basename($outputPath),
@@ -379,27 +383,53 @@ private function filterDuplicateTransPaths(array $transPaths): array
379383
return $filteredPaths;
380384
}
381385

386+
/**
387+
* Load current messages from po file.
388+
*/
389+
private function loadCurrentMessages(string $locale, string $path): MessageCatalogue
390+
{
391+
if (!is_file($path)) {
392+
throw new InvalidArgumentException(sprintf('Message file "%s" does not exist.', $path));
393+
}
394+
395+
$currentCatalogue = new MessageCatalogue($locale);
396+
$this->reader->setURI($path);
397+
$this->reader->open();
398+
while ($item = $this->reader->readItem()) {
399+
if (!empty($item->getTranslation())) {
400+
$currentCatalogue->set(
401+
PoItem::joinStrings((array) $item->getSource()),
402+
PoItem::joinStrings((array) $item->getTranslation()),
403+
PoItem::fromContext($item->getContext()),
404+
);
405+
}
406+
}
407+
$this->reader->close();
408+
409+
return $currentCatalogue;
410+
}
411+
382412
private function loadTranslatedMessages(MessageCatalogue $extractedCatalogue): MessageCatalogue
383413
{
384-
$currentCatalogue = new MessageCatalogue($extractedCatalogue->getLocale());
414+
$translatedMessages = new MessageCatalogue($extractedCatalogue->getLocale());
385415

386416
foreach ($extractedCatalogue->getDomains() as $domain) {
387417
$translations = $this->stringStorage->getTranslations([
388-
'language' => $currentCatalogue->getLocale(),
418+
'language' => $translatedMessages->getLocale(),
389419
'context' => PoItem::formatContext($domain),
390420
]);
391421
// Index by source
392422
$translations = array_column($translations, null, 'source');
393423
foreach ($extractedCatalogue->all($domain) as $source => $_) {
394424
if ($translation = ($translations[$source] ?? null)) {
395425
if ($string = $translation->getString()) {
396-
$currentCatalogue->set($source, $string, $domain);
426+
$translatedMessages->set($source, $string, $domain);
397427
}
398428
}
399429
}
400430
}
401431

402-
return $currentCatalogue;
432+
return $translatedMessages;
403433
}
404434

405435
private function removeNoFillTranslations(MessageCatalogueInterface $operation): void
@@ -421,15 +451,25 @@ private function removeNoFillTranslations(MessageCatalogueInterface $operation):
421451
private function getRootCodePaths(InputInterface $input): array
422452
{
423453
$info = [];
424-
$source = $input->getOption('source');
454+
$source = $input->getArgument('source');
455+
456+
if (empty($source)) {
457+
throw new InvalidArgumentException('Argument "source" is required.');
458+
}
425459

426460
// Expand module and theme paths.
427461
$source = preg_replace_callback(
428462
'/(module|theme):([a-z0-9_]+)/i',
429463
function (array $matches) use (&$info): string {
430464
$info[$matches[1]] = $matches[2];
431465

432-
return $this->extensionPathResolver->getPath($matches[1], $matches[2]);
466+
$path = $this->extensionPathResolver->getPath($matches[1], $matches[2]);
467+
468+
if (empty($path)) {
469+
throw new InvalidArgumentException(sprintf('Invalid %s: %s', $matches[1], $matches[2]));
470+
}
471+
472+
return $path;
433473
},
434474
$source,
435475
);

src/Translation/Dumper/PoItem.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ public function __toString()
1919
{
2020
$string = parent::__toString();
2121

22+
$flags = [];
2223
if ($this->fuzzy) {
24+
$flags[] = 'fuzzy';
25+
}
26+
if (!empty($flags)) {
2327
// https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/gettext.html#Fuzzy-Entries
24-
$string = '#, fuzzy'."\n".$string;
28+
$string = '#, '.implode(',', $flags)."\n".$string;
2529
}
2630

2731
return $string;
@@ -45,6 +49,11 @@ public static function splitStrings(string $string, int $numberOfStrings = 1): a
4549
return $strings;
4650
}
4751

52+
public static function fromContext(?string $context): string
53+
{
54+
return $context ?: self::NO_CONTEXT;
55+
}
56+
4857
public static function formatContext(?string $domain): string
4958
{
5059
return (null === $domain || self::NO_CONTEXT === $domain) ? '' : $domain;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Drupal\drupal_translation_extractor\Translation\Reader;
4+
5+
use Symfony\Component\Translation\Loader\PoFileLoader;
6+
use Symfony\Component\Translation\MessageCatalogue;
7+
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
8+
9+
class PoTranslationReader implements TranslationReaderInterface
10+
{
11+
public function read(string $directory, MessageCatalogue $catalogue): void
12+
{
13+
$loader = new PoFileLoader();
14+
$loader->load($directory, $catalogue->getLocale());
15+
}
16+
}

0 commit comments

Comments
 (0)