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

UtilTranslationsExtract::checkOptions()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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