UtilTranslationsExtract   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 173
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 90
dl 0
loc 173
ccs 0
cts 95
cp 0
rs 10
c 1
b 0
f 0
wmc 24

9 Methods

Rating   Name   Duplication   Size   Complexity  
A initTranslationComponents() 0 8 1
A configure() 0 15 1
B execute() 0 52 8
A loadCurrentMessages() 0 8 2
A extractMessages() 0 10 3
A saveDump() 0 5 1
A dumpMessages() 0 23 3
A checkOptions() 0 7 4
A initTwigExtractor() 0 4 1
1
<?php
2
3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
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
/**
35
 * UtilTranslationsExtract command.
36
 *
37
 * This command extracts translation strings from templates and allows saving them into a translation file.
38
 * It can also display the extracted messages in the console.
39
 */
40
class UtilTranslationsExtract extends AbstractCommand
41
{
42
    private TranslationWriter $writer;
43
    private TranslationReader $reader;
44
    private TwigExtractor $extractor;
45
46
    protected function configure(): void
47
    {
48
        $this
49
            ->setName('util:translations:extract')
50
            ->setDescription('Extracts translations from templates')
51
            ->setDefinition([
52
                new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'),
53
                new InputOption('locale', null, InputOption::VALUE_REQUIRED, 'Set the locale', 'fr'),
54
                new InputOption('show', null, InputOption::VALUE_NONE, 'Display translation messages in the console, as a list'),
55
                new InputOption('save', null, InputOption::VALUE_NONE, 'Save translation messages into the translation file'),
56
                new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format', 'po'),
57
                new InputOption('theme', null, InputOption::VALUE_REQUIRED, 'Merge translation messages from a given theme'),
58
            ])
59
            ->setHelp(
60
                <<<'EOF'
61
The <info>%command.name%</> command extracts translation strings from your templates.
62
63
  <info>%command.full_name% --locale=code --show</>
64
  <info>%command.full_name% --locale=code --show path/to/the/working/directory</>
65
66
To <comment>save</comment> translations into the translation file, run:
67
68
  <info>%command.full_name% --locale=code --save</>
69
70
To save translations into a specific <comment>format</comment>, run:
71
72
  <info>%command.full_name% --locale=code --save --format=po</>
73
74
To extract and merge translations from a specific <comment>theme</comment>, run:
75
76
  <info>%command.full_name% --locale=code --show --theme=theme-name</>
77
EOF
78
            );
79
    }
80
81
    protected function execute(InputInterface $input, OutputInterface $output): int
82
    {
83
        $config = $this->getBuilder()->getConfig();
84
        $layoutsPath = $config->getLayoutsPath();
85
        $translationsPath = $config->getTranslationsPath();
86
87
        $this->initTranslationComponents();
88
89
        $this->checkOptions($input);
90
91
        if ($input->getOption('theme')) {
92
            $layoutsPath = [$layoutsPath, $config->getThemeDirPath($input->getOption('theme'))];
93
        }
94
95
        $this->initTwigExtractor($layoutsPath);
96
97
        $output->writeln(\sprintf('Generating "<info>%s</info>" translation file', $input->getOption('locale')));
98
99
        $output->writeln('Parsing templates...');
100
        $extractedCatalogue = $this->extractMessages($input->getOption('locale'), $layoutsPath, 'NEW_');
101
102
        $output->writeln('Loading translation file...');
103
        $currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath);
104
105
        // processing translations catalogues
106
        try {
107
            $operation = $input->getOption('theme')
108
                ? new MergeOperation($currentCatalogue, $extractedCatalogue)
109
                : new TargetOperation($currentCatalogue, $extractedCatalogue);
110
        } catch (\Exception $e) {
111
            throw new RuntimeException($e->getMessage());
112
        }
113
114
        // show compiled list of messages
115
        if (true === $input->getOption('show')) {
116
            try {
117
                $this->dumpMessages($operation);
118
            } catch (\Exception $e) {
119
                throw new RuntimeException('Error while displaying messages: ' . $e->getMessage());
120
            }
121
        }
122
123
        // save the file
124
        if (true === $input->getOption('save')) {
125
            try {
126
                $this->saveDump($operation->getResult(), $input->getOption('format'), $translationsPath);
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): 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
        if (!\in_array($input->getOption('format'), $this->writer->getFormats(), true)) {
141
            throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', $this->writer->getFormats())));
142
        }
143
    }
144
145
    private function initTranslationComponents(): void
146
    {
147
        $this->reader = new TranslationReader();
148
        $this->reader->addLoader('po', new PoFileLoader());
149
        $this->reader->addLoader('yaml', new YamlFileLoader());
150
        $this->writer = new TranslationWriter();
151
        $this->writer->addDumper('po', new PoFileDumper());
152
        $this->writer->addDumper('yaml', new YamlFileDumper());
153
    }
154
155
    private function initTwigExtractor($layoutsPath = []): void
156
    {
157
        $twig = (new \Cecil\Renderer\Twig($this->getBuilder(), $layoutsPath))->getTwig();
158
        $this->extractor = new TwigExtractor($twig);
159
    }
160
161
    private function extractMessages(string $locale, $layoutsPath, string $prefix): MessageCatalogue
162
    {
163
        $extractedCatalogue = new MessageCatalogue($locale);
164
        $this->extractor->setPrefix($prefix);
165
        $layoutsPath = \is_array($layoutsPath) ? $layoutsPath : [$layoutsPath];
166
        foreach ($layoutsPath as $path) {
167
            $this->extractor->extract($path, $extractedCatalogue);
168
        }
169
170
        return $extractedCatalogue;
171
    }
172
173
    private function loadCurrentMessages(string $locale, string $translationsPath): MessageCatalogue
174
    {
175
        $currentCatalogue = new MessageCatalogue($locale);
176
        if (is_dir($translationsPath)) {
177
            $this->reader->read($translationsPath, $currentCatalogue);
178
        }
179
180
        return $currentCatalogue;
181
    }
182
183
    private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath): void
184
    {
185
        $this->io->writeln('Writing file...');
186
        $this->writer->write($messageCatalogue, $format, ['path' => $translationsPath]);
187
        $this->io->success('Translation file have been successfully updated.');
188
    }
189
190
    private function dumpMessages(OperationInterface $operation): void
191
    {
192
        $messagesCount = 0;
193
        $this->io->newLine();
194
        foreach ($operation->getDomains() as $domain) {
195
            $newKeys = array_keys($operation->getNewMessages($domain));
196
            $allKeys = array_keys($operation->getMessages($domain));
197
            $list = array_merge(
198
                array_diff($allKeys, $newKeys),
199
                array_map(fn ($key) => \sprintf('<fg=green>%s</>', $key), $newKeys),
200
                array_map(
201
                    fn ($key) => \sprintf('<fg=red>%s</>', $key),
202
                    array_keys($operation->getObsoleteMessages($domain))
203
                )
204
            );
205
            $domainMessagesCount = \count($list);
206
            sort($list);
207
            $this->io->listing($list);
208
            $messagesCount += $domainMessagesCount;
209
        }
210
211
        $this->io->success(
212
            \sprintf('%d message%s successfully extracted.', $messagesCount, $messagesCount > 1 ? 's were' : ' was')
213
        );
214
    }
215
}
216