Passed
Push — master ( 6672c8...b060c8 )
by Arnaud
06:53
created

initTranslationComponents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 8
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->initTranslationComponents();
79
80
        $this->checkOptions($input);
81
82
        if ($input->getOption('theme')) {
83
            $layoutsPath = [$layoutsPath, $config->getThemeDirPath($input->getOption('theme'))];
84
        }
85
86
        $this->initTwigExtractor($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
        // processing translations catalogues
97
        try {
98
            $operation = $input->getOption('theme')
99
                ? new MergeOperation($currentCatalogue, $extractedCatalogue)
100
                : new TargetOperation($currentCatalogue, $extractedCatalogue);
101
        } catch (\Exception $e) {
102
            throw new RuntimeException($e->getMessage());
103
        }
104
105
        // show compiled list of messages
106
        if (true === $input->getOption('show')) {
107
            try {
108
                $this->dumpMessages($operation);
109
            } catch (\Exception $e) {
110
                throw new RuntimeException('Error while displaying messages: ' . $e->getMessage());
111
            }
112
        }
113
114
        // save the file
115
        if (true === $input->getOption('save')) {
116
            try {
117
                $this->saveDump($operation->getResult(), $input->getOption('format'), $translationsPath);
118
            } catch (\InvalidArgumentException $e) {
119
                throw new RuntimeException('Error while saving translation file: ' . $e->getMessage());
120
            }
121
        }
122
123
        return 0;
124
    }
125
126
    private function checkOptions(InputInterface $input): void
127
    {
128
        if (true !== $input->getOption('save') && true !== $input->getOption('show')) {
129
            throw new RuntimeException('You must choose to display (`--show`) and/or save (`--save`) the translations');
130
        }
131
        if (!\in_array($input->getOption('format'), $this->writer->getFormats(), true)) {
132
            throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', $this->writer->getFormats())));
133
        }
134
    }
135
136
    private function initTranslationComponents(): void
137
    {
138
        $this->reader = new TranslationReader();
139
        $this->reader->addLoader('po', new PoFileLoader());
140
        $this->reader->addLoader('yaml', new YamlFileLoader());
141
        $this->writer = new TranslationWriter();
142
        $this->writer->addDumper('po', new PoFileDumper());
143
        $this->writer->addDumper('yaml', new YamlFileDumper());
144
    }
145
146
    private function initTwigExtractor($layoutsPath = []): void
147
    {
148
        $twig = (new \Cecil\Renderer\Twig($this->getBuilder(), $layoutsPath))->getTwig();
149
        $this->extractor = new TwigExtractor($twig);
150
    }
151
152
    private function extractMessages(string $locale, $layoutsPath, string $prefix): MessageCatalogue
153
    {
154
        $extractedCatalogue = new MessageCatalogue($locale);
155
        $this->extractor->setPrefix($prefix);
156
        $layoutsPath = \is_array($layoutsPath) ? $layoutsPath : [$layoutsPath];
157
        foreach ($layoutsPath as $path) {
158
            $this->extractor->extract($path, $extractedCatalogue);
159
        }
160
161
        return $extractedCatalogue;
162
    }
163
164
    private function loadCurrentMessages(string $locale, string $translationsPath): MessageCatalogue
165
    {
166
        $currentCatalogue = new MessageCatalogue($locale);
167
        if (is_dir($translationsPath)) {
168
            $this->reader->read($translationsPath, $currentCatalogue);
169
        }
170
171
        return $currentCatalogue;
172
    }
173
174
    private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath): void
175
    {
176
        $this->io->writeln('Writing file...');
177
        $this->writer->write($messageCatalogue, $format, ['path' => $translationsPath]);
178
        $this->io->success('Translation file have been successfully updated.');
179
    }
180
181
    private function dumpMessages(OperationInterface $operation): void
182
    {
183
        $messagesCount = 0;
184
        $this->io->newLine();
185
        foreach ($operation->getDomains() as $domain) {
186
            $newKeys = array_keys($operation->getNewMessages($domain));
187
            $allKeys = array_keys($operation->getMessages($domain));
188
            $list = array_merge(
189
                array_diff($allKeys, $newKeys),
190
                array_map(fn ($key) => \sprintf('<fg=green>%s</>', $key), $newKeys),
191
                array_map(
192
                    fn ($key) => \sprintf('<fg=red>%s</>', $key),
193
                    array_keys($operation->getObsoleteMessages($domain))
194
                )
195
            );
196
            $domainMessagesCount = \count($list);
197
            sort($list);
198
            $this->io->listing($list);
199
            $messagesCount += $domainMessagesCount;
200
        }
201
202
        $this->io->success(
203
            \sprintf('%d message%s successfully extracted.', $messagesCount, $messagesCount > 1 ? 's were' : ' was')
204
        );
205
    }
206
}
207