Passed
Pull Request — master (#2045)
by Arnaud
05:36
created

TranslationsExtract::initializeTwigExtractor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
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\OperationInterface;
25
use Symfony\Component\Translation\Catalogue\TargetOperation;
26
use Symfony\Component\Translation\Dumper\PoFileDumper;
27
use Symfony\Component\Translation\Dumper\YamlFileDumper;
28
use Symfony\Component\Translation\MessageCatalogue;
29
use Symfony\Component\Translation\MessageCatalogueInterface;
30
use Symfony\Component\Translation\Reader\TranslationReader;
31
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
32
use Symfony\Component\Translation\Writer\TranslationWriter;
33
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
34
use Symfony\Component\Translation\Loader\PoFileLoader;
35
use Symfony\Component\Translation\Loader\YamlFileLoader;
36
use Twig\Environment;
37
use Twig\Loader\FilesystemLoader;
38
39
class TranslationsExtract extends AbstractCommand
40
{
41
    private TranslationWriterInterface $writer;
42
    private TranslationReaderInterface $reader;
43
    private TwigExtractor $extractor;
44
45
    protected function configure(): void
46
    {
47
        $this
48
            ->setName('translations:extract')
49
            ->setDescription('Extracts translations from layouts')
50
            ->setDefinition([
51
                new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'),
52
                new InputOption('locale', null, InputOption::VALUE_OPTIONAL, 'The locale', 'fr'),
53
                new InputOption('show', null, InputOption::VALUE_NONE, 'Should the messages be displayed in the console'),
54
                new InputOption('save', null, InputOption::VALUE_NONE, 'Should the extract be done'),
55
                new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'po'),
56
                new InputOption('theme', null, InputOption::VALUE_OPTIONAL, 'Use if you want to translate a theme layout'),
57
            ])
58
            ->setHelp(
59
                <<<'EOF'
60
The <info>%command.name%</info> command extracts translation strings from your layouts.
61
It can display them or merge the new ones into the translation file.
62
When new translation strings are found it automatically add a <info>NEW_</info> prefix to the translation message.
63
64
Example running against working directory:
65
66
  <info>php %command.full_name% --show</info>
67
  <info>php %command.full_name% --save --locale=en</info>
68
69
You can extract translations from a given theme with <comment>--theme</> option:
70
71
  <info>php %command.full_name% --show --save --theme=hyde</info>
72
EOF
73
            )
74
        ;
75
    }
76
77
    protected function execute(InputInterface $input, OutputInterface $output): int
78
    {
79
        $config = $this->getBuilder()->getConfig();
80
        $layoutsPath = $config->getLayoutsPath();
81
        $translationsPath = $config->getTranslationsPath();
82
83
        $format = $input->getOption('format');
84
85
        $this->initializeTranslationComponents();
86
87
        $this->checkOptions($input, $format);
88
89
        if ($input->getOption('theme')) {
90
            $layoutsPath = $config->getThemeDirPath($input->getOption('theme'));
91
        }
92
93
        $this->initializeTwigExtractor($layoutsPath);
94
95
        $output->writeln(\sprintf('Generating "<info>%s</info>" translation file', $input->getOption('locale')));
96
97
        $output->writeln('Parsing templates...');
98
        $extractedCatalogue = $this->extractMessages($input->getOption('locale'), $layoutsPath, 'NEW_');
99
100
        $output->writeln('Loading translation file...');
101
        $currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath);
102
103
        try {
104
            $operation = new TargetOperation($currentCatalogue, $extractedCatalogue);
105
        } catch (\Exception $e) {
106
            throw new RuntimeException($e->getMessage());
107
        }
108
109
        // show compiled list of messages
110
        if (true === $input->getOption('show')) {
111
            try {
112
                $this->dumpMessages($operation);
113
            } catch (\Exception $e) {
114
                throw new RuntimeException('Error while displaying messages: ' . $e->getMessage());
115
            }
116
        }
117
118
        // save the file
119
        if (true === $input->getOption('save')) {
120
            try {
121
                $this->saveDump(
122
                    $operation->getResult(),
123
                    $format,
124
                    $translationsPath,
125
                    $config->getLanguageDefault()
126
                );
127
            } catch (\InvalidArgumentException $e) {
128
                throw new RuntimeException('Error while saving translation file: ' . $e->getMessage());
129
            }
130
        }
131
132
        return 0;
133
    }
134
135
    private function checkOptions(InputInterface $input, $format): void
136
    {
137
        if (true !== $input->getOption('save') && true !== $input->getOption('show')) {
138
            throw new RuntimeException('You must choose to display (`--show`) and/or save (`--save`) the translations');
139
        }
140
141
        // @phpstan-ignore-next-line
142
        if (!\in_array($format, $this->writer->getFormats(), true)) {
143
            throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', $this->writer->getFormats())));
144
        }
145
    }
146
147
    private function initializeTranslationComponents(): void
148
    {
149
        // readers
150
        $this->reader = new TranslationReader();
151
        $this->reader->addLoader('po', new PoFileLoader());
152
        $this->reader->addLoader('yaml', new YamlFileLoader());
153
        // writers
154
        $this->writer = new TranslationWriter();
155
        $this->writer->addDumper('po', new PoFileDumper());
156
        $this->writer->addDumper('yaml', new YamlFileDumper());
157
    }
158
159
    private function initializeTwigExtractor(string $layoutsPath): void
160
    {
161
        $twig = new Environment(new FilesystemLoader($layoutsPath));
162
        $twig->addExtension(new TranslationExtension());
163
        $twig->addExtension(new CoreExtension($this->getBuilder()));
164
        $this->extractor = new TwigExtractor($twig);
165
    }
166
167
    private function extractMessages(string $locale, string $codePath, string $prefix): MessageCatalogue
168
    {
169
        $extractedCatalogue = new MessageCatalogue($locale);
170
        $this->extractor->setPrefix($prefix);
171
        if (is_dir($codePath) || is_file($codePath)) {
172
            $this->extractor->extract($codePath, $extractedCatalogue);
173
        }
174
175
        return $extractedCatalogue;
176
    }
177
178
    private function loadCurrentMessages(string $locale, string $translationsPath): MessageCatalogue
179
    {
180
        $currentCatalogue = new MessageCatalogue($locale);
181
        if (is_dir($translationsPath)) {
182
            $this->reader->read($translationsPath, $currentCatalogue);
183
        }
184
185
        return $currentCatalogue;
186
    }
187
188
    private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath, string $defaultLocale): void
189
    {
190
        $this->io->writeln('Writing file...');
191
        $this->writer->write($messageCatalogue, $format, [
192
            'path' => $translationsPath,
193
            'default_locale' => $defaultLocale,
194
        ]);
195
        $this->io->success('Translation file have been successfully updated.');
196
    }
197
198
    private function dumpMessages(OperationInterface $operation): void
199
    {
200
        $messagesCount = 0;
201
        $this->io->newLine();
202
        foreach ($operation->getDomains() as $domain) {
203
            $newKeys = array_keys($operation->getNewMessages($domain));
204
            $allKeys = array_keys($operation->getMessages($domain));
205
            $list = array_merge(
206
                array_diff($allKeys, $newKeys),
207
                array_map(fn ($key) => \sprintf('<fg=green>%s</>', $key), $newKeys),
208
                array_map(
209
                    fn ($key) => \sprintf('<fg=red>%s</>', $key),
210
                    array_keys($operation->getObsoleteMessages($domain))
211
                )
212
            );
213
            $domainMessagesCount = \count($list);
214
            sort($list);
215
            $this->io->listing($list);
216
            $messagesCount += $domainMessagesCount;
217
        }
218
219
        $this->io->success(
220
            \sprintf(
221
                '%d message%s successfully extracted.',
222
                $messagesCount,
223
                $messagesCount > 1 ? 's were' : ' was'
224
            )
225
        );
226
    }
227
}
228