|
| 1 | +<?php |
| 2 | + |
| 3 | +/* |
| 4 | + * This file is part of the Symfony package. |
| 5 | + * |
| 6 | + * (c) Fabien Potencier <[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 | +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 <[email protected]> |
| 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