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

UtilTranslationsExtract::initializeTwigExtractor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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