Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit fb039e0

Browse filesBrowse files
committed
Rename translation:update to translation:extract
1 parent 9ec8912 commit fb039e0
Copy full SHA for fb039e0

File tree

10 files changed

+389
-282
lines changed
Filter options

10 files changed

+389
-282
lines changed

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ CHANGELOG
1616
* Add support for resetting container services after each messenger message
1717
* Add `configureContainer()`, `configureRoutes()`, `getConfigDir()` and `getBundlesPath()` to `MicroKernelTrait`
1818
* Add support for configuring log level, and status code by exception class
19+
* Deprecate `translation:update` command, use `translation:extract` instead
1920

2021
5.3
2122
---
+356Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
namespace Symfony\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Exception\InvalidArgumentException;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
use Symfony\Component\HttpKernel\KernelInterface;
22+
use Symfony\Component\Translation\Catalogue\MergeOperation;
23+
use Symfony\Component\Translation\Catalogue\TargetOperation;
24+
use Symfony\Component\Translation\Extractor\ExtractorInterface;
25+
use Symfony\Component\Translation\MessageCatalogue;
26+
use Symfony\Component\Translation\MessageCatalogueInterface;
27+
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
28+
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
29+
30+
/**
31+
* A command that parses templates to extract translation messages and adds them
32+
* into the translation files.
33+
*
34+
* @author Michel Salib <michelsalib@hotmail.com>
35+
*
36+
* @final
37+
*/
38+
class TranslationExtractCommand extends Command
39+
{
40+
private const ASC = 'asc';
41+
private const DESC = 'desc';
42+
private const SORT_ORDERS = [self::ASC, self::DESC];
43+
44+
protected static $defaultName = 'translation:extract';
45+
protected static $defaultDescription = 'Extract missing translations keys from code to translation files.';
46+
47+
private $writer;
48+
private $reader;
49+
private $extractor;
50+
private $defaultLocale;
51+
private $defaultTransPath;
52+
private $defaultViewsPath;
53+
private $transPaths;
54+
private $codePaths;
55+
56+
public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [])
57+
{
58+
parent::__construct();
59+
60+
$this->writer = $writer;
61+
$this->reader = $reader;
62+
$this->extractor = $extractor;
63+
$this->defaultLocale = $defaultLocale;
64+
$this->defaultTransPath = $defaultTransPath;
65+
$this->defaultViewsPath = $defaultViewsPath;
66+
$this->transPaths = $transPaths;
67+
$this->codePaths = $codePaths;
68+
}
69+
70+
/**
71+
* {@inheritdoc}
72+
*/
73+
protected function configure()
74+
{
75+
$this
76+
->setDefinition([
77+
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
78+
new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
79+
new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'),
80+
new InputOption('output-format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format (deprecated)'),
81+
new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf12'),
82+
new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'),
83+
new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'),
84+
new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'),
85+
new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'),
86+
new InputOption('xliff-version', null, InputOption::VALUE_OPTIONAL, 'Override the default xliff version (deprecated)'),
87+
new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically', 'asc'),
88+
new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
89+
])
90+
->setDescription(self::$defaultDescription)
91+
->setHelp(<<<'EOF'
92+
The <info>%command.name%</info> command extracts translation strings from templates
93+
of a given bundle or the default translations directory. It can display them or merge
94+
the new ones into the translation files.
95+
96+
When new translation strings are found it can automatically add a prefix to the translation
97+
message.
98+
99+
Example running against a Bundle (AcmeBundle)
100+
101+
<info>php %command.full_name% --dump-messages en AcmeBundle</info>
102+
<info>php %command.full_name% --force --prefix="new_" fr AcmeBundle</info>
103+
104+
Example running against default messages directory
105+
106+
<info>php %command.full_name% --dump-messages en</info>
107+
<info>php %command.full_name% --force --prefix="new_" fr</info>
108+
109+
You can sort the output with the <comment>--sort</> flag:
110+
111+
<info>php %command.full_name% --dump-messages --sort=asc en AcmeBundle</info>
112+
<info>php %command.full_name% --dump-messages --sort=desc fr</info>
113+
114+
You can dump a tree-like structure using the yaml format with <comment>--as-tree</> flag:
115+
116+
<info>php %command.full_name% --force --format=yaml --as-tree=3 en AcmeBundle</info>
117+
<info>php %command.full_name% --force --format=yaml --sort=asc --as-tree=3 fr</info>
118+
119+
EOF
120+
)
121+
;
122+
}
123+
124+
/**
125+
* {@inheritdoc}
126+
*/
127+
protected function execute(InputInterface $input, OutputInterface $output): int
128+
{
129+
$io = new SymfonyStyle($input, $output);
130+
$errorIo = $io->getErrorStyle();
131+
132+
// check presence of force or dump-message
133+
if (true !== $input->getOption('force') && true !== $input->getOption('dump-messages')) {
134+
$errorIo->error('You must choose one of --force or --dump-messages');
135+
136+
return 1;
137+
}
138+
139+
$format = $input->getOption('output-format') ?: $input->getOption('format');
140+
$xliffVersion = $input->getOption('xliff-version') ?? '1.2';
141+
142+
if ($input->getOption('xliff-version')) {
143+
trigger_deprecation('symfony/framework-bundle', '5.3', 'The "--xliff-version" option is deprecated, use "--format=xlf%d" instead.', 10 * $xliffVersion);
144+
}
145+
146+
if ($input->getOption('output-format')) {
147+
trigger_deprecation('symfony/framework-bundle', '5.3', 'The "--output-format" option is deprecated, use "--format=xlf%d" instead.', 10 * $xliffVersion);
148+
}
149+
150+
switch ($format) {
151+
case 'xlf20': $xliffVersion = '2.0';
152+
// no break
153+
case 'xlf12': $format = 'xlf';
154+
}
155+
156+
// check format
157+
$supportedFormats = $this->writer->getFormats();
158+
if (!\in_array($format, $supportedFormats, true)) {
159+
$errorIo->error(['Wrong output format', 'Supported formats are: '.implode(', ', $supportedFormats).', xlf12 and xlf20.']);
160+
161+
return 1;
162+
}
163+
164+
/** @var KernelInterface $kernel */
165+
$kernel = $this->getApplication()->getKernel();
166+
167+
// Define Root Paths
168+
$transPaths = $this->transPaths;
169+
if ($this->defaultTransPath) {
170+
$transPaths[] = $this->defaultTransPath;
171+
}
172+
$codePaths = $this->codePaths;
173+
$codePaths[] = $kernel->getProjectDir().'/src';
174+
if ($this->defaultViewsPath) {
175+
$codePaths[] = $this->defaultViewsPath;
176+
}
177+
$currentName = 'default directory';
178+
179+
// Override with provided Bundle info
180+
if (null !== $input->getArgument('bundle')) {
181+
try {
182+
$foundBundle = $kernel->getBundle($input->getArgument('bundle'));
183+
$bundleDir = $foundBundle->getPath();
184+
$transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations'];
185+
$codePaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates'];
186+
if ($this->defaultTransPath) {
187+
$transPaths[] = $this->defaultTransPath;
188+
}
189+
if ($this->defaultViewsPath) {
190+
$codePaths[] = $this->defaultViewsPath;
191+
}
192+
$currentName = $foundBundle->getName();
193+
} catch (\InvalidArgumentException $e) {
194+
// such a bundle does not exist, so treat the argument as path
195+
$path = $input->getArgument('bundle');
196+
197+
$transPaths = [$path.'/translations'];
198+
$codePaths = [$path.'/templates'];
199+
200+
if (!is_dir($transPaths[0])) {
201+
throw new InvalidArgumentException(sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0]));
202+
}
203+
}
204+
}
205+
206+
$io->title('Translation Messages Extractor and Dumper');
207+
$io->comment(sprintf('Generating "<info>%s</info>" translation files for "<info>%s</info>"', $input->getArgument('locale'), $currentName));
208+
209+
// load any messages from templates
210+
$extractedCatalogue = new MessageCatalogue($input->getArgument('locale'));
211+
$io->comment('Parsing templates...');
212+
$this->extractor->setPrefix($input->getOption('prefix'));
213+
foreach ($codePaths as $path) {
214+
if (is_dir($path) || is_file($path)) {
215+
$this->extractor->extract($path, $extractedCatalogue);
216+
}
217+
}
218+
219+
// load any existing messages from the translation files
220+
$currentCatalogue = new MessageCatalogue($input->getArgument('locale'));
221+
$io->comment('Loading translation files...');
222+
foreach ($transPaths as $path) {
223+
if (is_dir($path)) {
224+
$this->reader->read($path, $currentCatalogue);
225+
}
226+
}
227+
228+
if (null !== $domain = $input->getOption('domain')) {
229+
$currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain);
230+
$extractedCatalogue = $this->filterCatalogue($extractedCatalogue, $domain);
231+
}
232+
233+
// process catalogues
234+
$operation = $input->getOption('clean')
235+
? new TargetOperation($currentCatalogue, $extractedCatalogue)
236+
: new MergeOperation($currentCatalogue, $extractedCatalogue);
237+
238+
// Exit if no messages found.
239+
if (!\count($operation->getDomains())) {
240+
$errorIo->warning('No translation messages were found.');
241+
242+
return 0;
243+
}
244+
245+
$resultMessage = 'Translation files were successfully updated';
246+
247+
$operation->moveMessagesToIntlDomainsIfPossible('new');
248+
249+
// show compiled list of messages
250+
if (true === $input->getOption('dump-messages')) {
251+
$extractedMessagesCount = 0;
252+
$io->newLine();
253+
foreach ($operation->getDomains() as $domain) {
254+
$newKeys = array_keys($operation->getNewMessages($domain));
255+
$allKeys = array_keys($operation->getMessages($domain));
256+
257+
$list = array_merge(
258+
array_diff($allKeys, $newKeys),
259+
array_map(function ($id) {
260+
return sprintf('<fg=green>%s</>', $id);
261+
}, $newKeys),
262+
array_map(function ($id) {
263+
return sprintf('<fg=red>%s</>', $id);
264+
}, array_keys($operation->getObsoleteMessages($domain)))
265+
);
266+
267+
$domainMessagesCount = \count($list);
268+
269+
if ($sort = $input->getOption('sort')) {
270+
$sort = strtolower($sort);
271+
if (!\in_array($sort, self::SORT_ORDERS, true)) {
272+
$errorIo->error(['Wrong sort order', 'Supported formats are: '.implode(', ', self::SORT_ORDERS).'.']);
273+
274+
return 1;
275+
}
276+
277+
if (self::DESC === $sort) {
278+
rsort($list);
279+
} else {
280+
sort($list);
281+
}
282+
}
283+
284+
$io->section(sprintf('Messages extracted for domain "<info>%s</info>" (%d message%s)', $domain, $domainMessagesCount, $domainMessagesCount > 1 ? 's' : ''));
285+
$io->listing($list);
286+
287+
$extractedMessagesCount += $domainMessagesCount;
288+
}
289+
290+
if ('xlf' === $format) {
291+
$io->comment(sprintf('Xliff output version is <info>%s</info>', $xliffVersion));
292+
}
293+
294+
$resultMessage = sprintf('%d message%s successfully extracted', $extractedMessagesCount, $extractedMessagesCount > 1 ? 's were' : ' was');
295+
}
296+
297+
// save the files
298+
if (true === $input->getOption('force')) {
299+
$io->comment('Writing files...');
300+
301+
$bundleTransPath = false;
302+
foreach ($transPaths as $path) {
303+
if (is_dir($path)) {
304+
$bundleTransPath = $path;
305+
}
306+
}
307+
308+
if (!$bundleTransPath) {
309+
$bundleTransPath = end($transPaths);
310+
}
311+
312+
$this->writer->write($operation->getResult(), $format, ['path' => $bundleTransPath, 'default_locale' => $this->defaultLocale, 'xliff_version' => $xliffVersion, 'as_tree' => $input->getOption('as-tree'), 'inline' => $input->getOption('as-tree') ?? 0]);
313+
314+
if (true === $input->getOption('dump-messages')) {
315+
$resultMessage .= ' and translation files were updated';
316+
}
317+
}
318+
319+
$io->success($resultMessage.'.');
320+
321+
return 0;
322+
}
323+
324+
private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
325+
{
326+
$filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
327+
328+
// extract intl-icu messages only
329+
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
330+
if ($intlMessages = $catalogue->all($intlDomain)) {
331+
$filteredCatalogue->add($intlMessages, $intlDomain);
332+
}
333+
334+
// extract all messages and subtract intl-icu messages
335+
if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
336+
$filteredCatalogue->add($messages, $domain);
337+
}
338+
foreach ($catalogue->getResources() as $resource) {
339+
$filteredCatalogue->addResource($resource);
340+
}
341+
342+
if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
343+
foreach ($metadata as $k => $v) {
344+
$filteredCatalogue->setMetadata($k, $v, $intlDomain);
345+
}
346+
}
347+
348+
if ($metadata = $catalogue->getMetadata('', $domain)) {
349+
foreach ($metadata as $k => $v) {
350+
$filteredCatalogue->setMetadata($k, $v, $domain);
351+
}
352+
}
353+
354+
return $filteredCatalogue;
355+
}
356+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.