Passed
Pull Request — master (#2045)
by Arnaud
06:23
created

TranslationsExtract::loadCurrentMessages()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 8
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Command;
15
16
use Cecil\Exception\RuntimeException;
17
use Cecil\Renderer\Extension\Core as CoreExtension;
18
use Symfony\Bridge\Twig\Extension\TranslationExtension;
19
use Symfony\Bridge\Twig\Translation\TwigExtractor;
20
use Symfony\Component\Console\Input\InputArgument;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Input\InputOption;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Translation\Catalogue\AbstractOperation;
25
use Symfony\Component\Translation\Catalogue\OperationInterface;
26
use Symfony\Component\Translation\Catalogue\TargetOperation;
27
use Symfony\Component\Translation\Dumper\PoFileDumper;
28
use Symfony\Component\Translation\Dumper\YamlFileDumper;
29
use Symfony\Component\Translation\MessageCatalogue;
30
use Symfony\Component\Translation\MessageCatalogueInterface;
31
use Symfony\Component\Translation\Reader\TranslationReader;
32
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
33
use Symfony\Component\Translation\Writer\TranslationWriter;
34
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
35
use Symfony\Component\Translation\Loader\PoFileLoader;
36
use Symfony\Component\Translation\Loader\YamlFileLoader;
37
use Twig\Environment;
38
use Twig\Loader\FilesystemLoader;
39
40
class TranslationsExtract extends AbstractCommand
41
{
42
    private const AVAILABLE_FORMATS = [
43
        'po' => ['po'],
44
        'yaml' => ['yaml'],
45
    ];
46
    private TranslationWriterInterface $writer;
47
    private TranslationReaderInterface $reader;
48
    private TwigExtractor $extractor;
49
50
    protected function configure(): void
51
    {
52
        $this
53
            ->setName('translations:extract')
54
            ->setDescription('Extracts translations from layouts')
55
            ->setDefinition([
56
                new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'),
57
                new InputOption('locale', null, InputOption::VALUE_OPTIONAL, 'The locale', 'fr'),
58
                new InputOption('show', null, InputOption::VALUE_NONE, 'Should the messages be displayed in the console'),
59
                new InputOption('save', null, InputOption::VALUE_NONE, 'Should the extract be done'),
60
                new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'po'),
61
                new InputOption('theme', null, InputOption::VALUE_OPTIONAL, 'Use if you want to translate a theme layout'),
62
            ])
63
            ->setHelp(
64
                <<<'EOF'
65
The <info>%command.name%</info> command extracts translation strings from your layouts. It can display them or merge
66
the new ones into the translation files.
67
When new translation strings are found it automatically add a <info>NEW_</info> prefix to the translation message.
68
69
Example running against working directory:
70
71
  <info>php %command.full_name% --show</info>
72
  <info>php %command.full_name% --save --locale=en</info>
73
74
You can extract translations from a given theme with <comment>--theme</> option:
75
76
  <info>php %command.full_name% --theme=hyde</info>
77
EOF
78
            )
79
        ;
80
    }
81
82
    protected function execute(InputInterface $input, OutputInterface $output): int
83
    {
84
        $format = $input->getOption('format');
85
        $domain = 'messages';
86
87
        if (true !== $input->getOption('save') && true !== $input->getOption('show')) {
88
            throw new RuntimeException('You must choose to display (`--show`) or save (`--save`) the translations');
89
        }
90
91
        $config = $this->getBuilder()->getConfig();
92
        $layoutsPath = $config->getLayoutsPath();
93
        $translationsPath = $config->getTranslationsPath();
94
95
        $this->initializeTranslationComponents();
96
97
        // @phpstan-ignore-next-line
98
        $supportedFormats = $this->writer->getFormats();
99
100
        if (!\in_array($format, $supportedFormats, true)) {
101
            throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', array_keys(self::AVAILABLE_FORMATS))));
102
        }
103
104
        if ($input->getOption('theme')) {
105
            $layoutsPath = $config->getThemeDirPath($input->getOption('theme'));
106
        }
107
108
        $this->initializeTwigExtractor($layoutsPath);
109
110
        $this->io->writeln(\sprintf('Generating "<info>%s</info>" translation files', $input->getOption('locale')));
111
        $this->io->writeln('Parsing templates...');
112
        $extractedCatalogue = $this->extractMessages($input->getOption('locale'), $layoutsPath, 'NEW_');
113
        $this->io->writeln('Loading translation files...');
114
        $currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath);
115
116
        $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain);
117
        $extractedCatalogue = $this->filterCatalogue($extractedCatalogue, $domain);
118
        try {
119
            $operation = $this->getOperation($currentCatalogue, $extractedCatalogue);
120
        } catch (\Exception $exception) {
121
            throw new RuntimeException($exception->getMessage());
122
        }
123
124
        // show compiled list of messages
125
        if (true === $input->getOption('show')) {
126
            try {
127
                $this->dumpMessages($operation);
128
            } catch (\Exception $e) {
129
                throw new RuntimeException('Error while displaying messages: ' . $e->getMessage());
130
            }
131
        }
132
133
        // save the files
134
        if (true === $input->getOption('save')) {
135
            try {
136
                $this->saveDump(
137
                    $operation->getResult(),
138
                    $format,
139
                    $translationsPath,
140
                    $config->getLanguageDefault()
141
                );
142
            } catch (\InvalidArgumentException $e) {
143
                throw new RuntimeException('Error while saving translations files: ' . $e->getMessage());
144
            }
145
        }
146
147
        return 0;
148
    }
149
150
    private function initializeTranslationComponents(): void
151
    {
152
        // readers
153
        $this->reader = new TranslationReader();
154
        $this->reader->addLoader('po', new PoFileLoader());
155
        $this->reader->addLoader('yaml', new YamlFileLoader());
156
        // writers
157
        $this->writer = new TranslationWriter();
158
        $this->writer->addDumper('po', new PoFileDumper());
159
        $this->writer->addDumper('yaml', new YamlFileDumper());
160
    }
161
162
    private function initializeTwigExtractor(string $layoutsPath): void
163
    {
164
        $twig = new Environment(new FilesystemLoader($layoutsPath));
165
        $twig->addExtension(new TranslationExtension());
166
        $twig->addExtension(new CoreExtension($this->getBuilder()));
167
        $this->extractor = new TwigExtractor($twig);
168
    }
169
170
    private function extractMessages(string $locale, string $codePath, string $prefix): MessageCatalogue
171
    {
172
        $extractedCatalogue = new MessageCatalogue($locale);
173
        $this->extractor->setPrefix($prefix);
174
        if (is_dir($codePath) || is_file($codePath)) {
175
            $this->extractor->extract($codePath, $extractedCatalogue);
176
        }
177
178
        return $extractedCatalogue;
179
    }
180
181
    private function loadCurrentMessages(string $locale, string $translationsPath): MessageCatalogue
182
    {
183
        $currentCatalogue = new MessageCatalogue($locale);
184
        if (is_dir($translationsPath)) {
185
            $this->reader->read($translationsPath, $currentCatalogue);
186
        }
187
188
        return $currentCatalogue;
189
    }
190
191
    private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
192
    {
193
        $filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
194
195
        // extract intl-icu messages only
196
        $intlDomain = $domain . MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
197
        if ($intlMessages = $catalogue->all($intlDomain)) {
198
            $filteredCatalogue->add($intlMessages, $intlDomain);
199
        }
200
201
        // extract all messages and subtract intl-icu messages
202
        if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
203
            $filteredCatalogue->add($messages, $domain);
204
        }
205
        foreach ($catalogue->getResources() as $resource) {
206
            $filteredCatalogue->addResource($resource);
207
        }
208
209
        if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
210
            foreach ($metadata as $k => $v) {
211
                $filteredCatalogue->setMetadata($k, $v, $intlDomain);
212
            }
213
        }
214
215
        if ($metadata = $catalogue->getMetadata('', $domain)) {
216
            foreach ($metadata as $k => $v) {
217
                $filteredCatalogue->setMetadata($k, $v, $domain);
218
            }
219
        }
220
221
        return $filteredCatalogue;
222
    }
223
224
    /**
225
     * Retrieves the operation that processes the current and extracted message catalogues.
226
     *
227
     * @throws \Exception If no translation messages are found.
228
     */
229
    private function getOperation(MessageCatalogue $currentCatalogue, MessageCatalogue $extractedCatalogue): AbstractOperation
230
    {
231
        $operation = $this->processCatalogues($currentCatalogue, $extractedCatalogue);
232
        if (!\count($operation->getDomains())) {
233
            throw new RuntimeException('No translation messages were found.');
234
        }
235
236
        return $operation;
237
    }
238
239
    private function processCatalogues(MessageCatalogueInterface $currentCatalogue, MessageCatalogueInterface $extractedCatalogue): AbstractOperation
240
    {
241
        return new TargetOperation($currentCatalogue, $extractedCatalogue);
242
    }
243
244
    private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath, string $defaultLocale): void
245
    {
246
        $this->io->newLine();
247
        $this->io->writeln('Writing files...');
248
249
        $this->writer->write($messageCatalogue, $format, [
250
            'path' => $translationsPath,
251
            'default_locale' => $defaultLocale,
252
        ]);
253
254
        $this->io->success('Translations files have been successfully updated.');
255
    }
256
257
    private function dumpMessages(OperationInterface $operation): void
258
    {
259
        $messagesCount = 0;
260
        $this->io->newLine();
261
        foreach ($operation->getDomains() as $domain) {
262
            $newKeys = array_keys($operation->getNewMessages($domain));
263
            $allKeys = array_keys($operation->getMessages($domain));
264
            $list = array_merge(
265
                array_diff($allKeys, $newKeys),
266
                array_map(fn ($key) => \sprintf('<fg=green>%s</>', $key), $newKeys),
267
                array_map(
268
                    fn ($key) => \sprintf('<fg=red>%s</>', $key),
269
                    array_keys($operation->getObsoleteMessages($domain))
270
                )
271
            );
272
            $domainMessagesCount = \count($list);
273
            sort($list); // default sort ASC
274
            $this->io->listing($list);
275
            $messagesCount += $domainMessagesCount;
276
        }
277
278
        $this->io->success(
279
            \sprintf(
280
                '%d message%s successfully extracted.',
281
                $messagesCount,
282
                $messagesCount > 1 ? 's were' : ' was'
283
            )
284
        );
285
    }
286
}
287