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

UtilTranslationsExtract   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 184
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 100
c 4
b 0
f 0
dl 0
loc 184
rs 10
wmc 24

9 Methods

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