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 597a310

Browse filesBrowse files
florianvfabpot
authored andcommitted
Added a translation:debug command
1 parent 2a15923 commit 597a310
Copy full SHA for 597a310

File tree

Expand file treeCollapse file tree

2 files changed

+226
-0
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+226
-0
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
@@ -4,6 +4,7 @@ CHANGELOG
44
2.5.0
55
-----
66

7+
* Added `translation:debug` command
78
* Added `config:debug` command
89
* Added `yaml:lint` command
910
* Deprecated the `RouterApacheDumperCommand` which will be removed in Symfony 3.0.
+225Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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\Translation\Catalogue\MergeOperation;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\OutputInterface;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Translation\MessageCatalogue;
20+
21+
/**
22+
* Helps finding unused or missing translation messages in a given locale
23+
* and comparing them with the fallback ones.
24+
*
25+
* @author Florian Voutzinos <florian@voutzinos.com>
26+
*/
27+
class TranslationDebugCommand extends ContainerAwareCommand
28+
{
29+
const MESSAGE_MISSING = 0;
30+
const MESSAGE_UNUSED = 1;
31+
const MESSAGE_EQUALS_FALLBACK = 2;
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
protected function configure()
37+
{
38+
$this
39+
->setName('translation:debug')
40+
->setDefinition(array(
41+
new InputArgument('locale', InputArgument::REQUIRED, 'The locale'),
42+
new InputArgument('bundle', InputArgument::REQUIRED, 'The bundle name'),
43+
new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'The messages domain'),
44+
new InputOption('only-missing', null, InputOption::VALUE_NONE, 'Displays only missing messages'),
45+
new InputOption('only-unused', null, InputOption::VALUE_NONE, 'Displays only unused messages'),
46+
))
47+
->setDescription('Displays translation messages informations')
48+
->setHelp(<<<EOF
49+
The <info>%command.name%</info> command helps finding unused or missing translation messages and
50+
comparing them with the fallback ones by inspecting the templates and translation files of a given bundle.
51+
52+
You can display informations about a bundle translations in a specific locale:
53+
54+
<info>php %command.full_name% en AcmeDemoBundle</info>
55+
56+
You can also specify a translation domain for the search:
57+
58+
<info>php %command.full_name% --domain=messages en AcmeDemoBundle</info>
59+
60+
You can only display missing messages:
61+
62+
<info>php %command.full_name% --only-missing en AcmeDemoBundle</info>
63+
64+
You can only display unused messages:
65+
66+
<info>php %command.full_name% --only-unused en AcmeDemoBundle</info>
67+
EOF
68+
)
69+
;
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
protected function execute(InputInterface $input, OutputInterface $output)
76+
{
77+
$locale = $input->getArgument('locale');
78+
$domain = $input->getOption('domain');
79+
$bundle = $this->getContainer()->get('kernel')->getBundle($input->getArgument('bundle'));
80+
$loader = $this->getContainer()->get('translation.loader');
81+
82+
// Extract used messages
83+
$extractedCatalogue = new MessageCatalogue($locale);
84+
$this->getContainer()->get('translation.extractor')
85+
->extract($bundle->getPath().'/Resources/views/', $extractedCatalogue);
86+
87+
// Load defined messages
88+
$currentCatalogue = new MessageCatalogue($locale);
89+
$loader->loadMessages($bundle->getPath().'/Resources/translations', $currentCatalogue);
90+
91+
// Merge defined and extracted messages to get all message ids
92+
$mergeOperation = new MergeOperation($extractedCatalogue, $currentCatalogue);
93+
$allMessages = $mergeOperation->getResult()->all($domain);
94+
if (null !== $domain) {
95+
$allMessages = array($domain => $allMessages);
96+
}
97+
98+
// No defined or extracted messages
99+
if (empty($allMessages) || null !== $domain && empty($allMessages[$domain])) {
100+
$outputMessage = sprintf('<info>No defined or extracted messages for locale "%s"</info>', $locale);
101+
102+
if (null !== $domain) {
103+
$outputMessage .= sprintf(' <info>and domain "%s"</info>', $domain);
104+
}
105+
106+
$output->writeln($outputMessage);
107+
108+
return;
109+
}
110+
111+
// Load the fallback catalogues
112+
$fallbackCatalogues = array();
113+
foreach ($this->getContainer()->get('translator')->getFallbackLocales() as $fallbackLocale) {
114+
if ($fallbackLocale === $locale) {
115+
continue;
116+
}
117+
118+
$fallbackCatalogue = new MessageCatalogue($fallbackLocale);
119+
$loader->loadMessages($bundle->getPath().'/Resources/translations', $fallbackCatalogue);
120+
$fallbackCatalogues[] = $fallbackCatalogue;
121+
}
122+
123+
// Display legend
124+
$output->writeln(sprintf('Legend: %s Missing message %s Unused message %s Equals fallback message',
125+
$this->formatState(self::MESSAGE_MISSING),
126+
$this->formatState(self::MESSAGE_UNUSED),
127+
$this->formatState(self::MESSAGE_EQUALS_FALLBACK)
128+
));
129+
130+
/** @var \Symfony\Component\Console\Helper\TableHelper $tableHelper */
131+
$tableHelper = $this->getHelperSet()->get('table');
132+
133+
// Display header line
134+
$headers = array('State(s)', 'Id', sprintf('Message Preview (%s)', $locale));
135+
foreach ($fallbackCatalogues as $fallbackCatalogue) {
136+
$headers[] = sprintf('Fallback Message Preview (%s)', $fallbackCatalogue->getLocale());
137+
}
138+
$tableHelper->setHeaders($headers);
139+
140+
// Iterate all message ids and determine their state
141+
foreach ($allMessages as $domain => $messages) {
142+
foreach (array_keys($messages) as $messageId) {
143+
$value = $currentCatalogue->get($messageId, $domain);
144+
$states = array();
145+
146+
if ($extractedCatalogue->defines($messageId, $domain)) {
147+
if (!$currentCatalogue->defines($messageId, $domain)) {
148+
$states[] = self::MESSAGE_MISSING;
149+
}
150+
} elseif ($currentCatalogue->defines($messageId, $domain)) {
151+
$states[] = self::MESSAGE_UNUSED;
152+
}
153+
154+
if (!in_array(self::MESSAGE_UNUSED, $states) && true === $input->getOption('only-unused')
155+
|| !in_array(self::MESSAGE_MISSING, $states) && true === $input->getOption('only-missing')) {
156+
continue;
157+
}
158+
159+
foreach ($fallbackCatalogues as $fallbackCatalogue) {
160+
if ($fallbackCatalogue->defines($messageId, $domain)
161+
&& $value === $fallbackCatalogue->get($messageId, $domain)) {
162+
$states[] = self::MESSAGE_EQUALS_FALLBACK;
163+
break;
164+
}
165+
}
166+
167+
$row = array($this->formatStates($states), $this->formatId($messageId), $this->sanitizeString($value));
168+
foreach ($fallbackCatalogues as $fallbackCatalogue) {
169+
$row[] = $this->sanitizeString($fallbackCatalogue->get($messageId, $domain));
170+
}
171+
172+
$tableHelper->addRow($row);
173+
}
174+
}
175+
176+
$tableHelper->render($output);
177+
}
178+
179+
private function formatState($state)
180+
{
181+
if (self::MESSAGE_MISSING === $state) {
182+
return '<fg=red;options=bold>x</fg=red;options=bold>';
183+
}
184+
185+
if (self::MESSAGE_UNUSED === $state) {
186+
return '<fg=yellow;options=bold>o</fg=yellow;options=bold>';
187+
}
188+
189+
if (self::MESSAGE_EQUALS_FALLBACK === $state) {
190+
return '<fg=green;options=bold>=</fg=green;options=bold>';
191+
}
192+
193+
return $state;
194+
}
195+
196+
private function formatStates(array $states)
197+
{
198+
$result = array();
199+
foreach ($states as $state) {
200+
$result[] = $this->formatState($state);
201+
}
202+
203+
return implode(' ', $result);
204+
}
205+
206+
private function formatId($id)
207+
{
208+
return sprintf('<fg=cyan;options=bold>%s</fg=cyan;options=bold>', $id);
209+
}
210+
211+
private function sanitizeString($string, $lenght = 40)
212+
{
213+
$string = trim(preg_replace('/\s+/', ' ', $string));
214+
215+
if (function_exists('mb_strlen') && false !== $encoding = mb_detect_encoding($string)) {
216+
if (mb_strlen($string, $encoding) > $lenght) {
217+
return mb_substr($string, 0, $lenght - 3, $encoding).'...';
218+
}
219+
} elseif (strlen($string) > $lenght) {
220+
return substr($string, 0, $lenght - 3).'...';
221+
}
222+
223+
return $string;
224+
}
225+
}

0 commit comments

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