From 597a3107bcd6ce184ea7a35108616a44c22e5232 Mon Sep 17 00:00:00 2001 From: florianv Date: Mon, 20 Jan 2014 00:09:51 +0100 Subject: [PATCH 1/2] Added a translation:debug command --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Command/TranslationDebugCommand.php | 225 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index ffb90d2ed6..9658d5729c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 2.5.0 ----- + * Added `translation:debug` command * Added `config:debug` command * Added `yaml:lint` command * Deprecated the `RouterApacheDumperCommand` which will be removed in Symfony 3.0. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php new file mode 100644 index 0000000000..ccdeb59e36 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Translation\Catalogue\MergeOperation; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * Helps finding unused or missing translation messages in a given locale + * and comparing them with the fallback ones. + * + * @author Florian Voutzinos + */ +class TranslationDebugCommand extends ContainerAwareCommand +{ + const MESSAGE_MISSING = 0; + const MESSAGE_UNUSED = 1; + const MESSAGE_EQUALS_FALLBACK = 2; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('translation:debug') + ->setDefinition(array( + new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), + new InputArgument('bundle', InputArgument::REQUIRED, 'The bundle name'), + new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'The messages domain'), + new InputOption('only-missing', null, InputOption::VALUE_NONE, 'Displays only missing messages'), + new InputOption('only-unused', null, InputOption::VALUE_NONE, 'Displays only unused messages'), + )) + ->setDescription('Displays translation messages informations') + ->setHelp(<<%command.name% command helps finding unused or missing translation messages and +comparing them with the fallback ones by inspecting the templates and translation files of a given bundle. + +You can display informations about a bundle translations in a specific locale: + +php %command.full_name% en AcmeDemoBundle + +You can also specify a translation domain for the search: + +php %command.full_name% --domain=messages en AcmeDemoBundle + +You can only display missing messages: + +php %command.full_name% --only-missing en AcmeDemoBundle + +You can only display unused messages: + +php %command.full_name% --only-unused en AcmeDemoBundle +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $locale = $input->getArgument('locale'); + $domain = $input->getOption('domain'); + $bundle = $this->getContainer()->get('kernel')->getBundle($input->getArgument('bundle')); + $loader = $this->getContainer()->get('translation.loader'); + + // Extract used messages + $extractedCatalogue = new MessageCatalogue($locale); + $this->getContainer()->get('translation.extractor') + ->extract($bundle->getPath().'/Resources/views/', $extractedCatalogue); + + // Load defined messages + $currentCatalogue = new MessageCatalogue($locale); + $loader->loadMessages($bundle->getPath().'/Resources/translations', $currentCatalogue); + + // Merge defined and extracted messages to get all message ids + $mergeOperation = new MergeOperation($extractedCatalogue, $currentCatalogue); + $allMessages = $mergeOperation->getResult()->all($domain); + if (null !== $domain) { + $allMessages = array($domain => $allMessages); + } + + // No defined or extracted messages + if (empty($allMessages) || null !== $domain && empty($allMessages[$domain])) { + $outputMessage = sprintf('No defined or extracted messages for locale "%s"', $locale); + + if (null !== $domain) { + $outputMessage .= sprintf(' and domain "%s"', $domain); + } + + $output->writeln($outputMessage); + + return; + } + + // Load the fallback catalogues + $fallbackCatalogues = array(); + foreach ($this->getContainer()->get('translator')->getFallbackLocales() as $fallbackLocale) { + if ($fallbackLocale === $locale) { + continue; + } + + $fallbackCatalogue = new MessageCatalogue($fallbackLocale); + $loader->loadMessages($bundle->getPath().'/Resources/translations', $fallbackCatalogue); + $fallbackCatalogues[] = $fallbackCatalogue; + } + + // Display legend + $output->writeln(sprintf('Legend: %s Missing message %s Unused message %s Equals fallback message', + $this->formatState(self::MESSAGE_MISSING), + $this->formatState(self::MESSAGE_UNUSED), + $this->formatState(self::MESSAGE_EQUALS_FALLBACK) + )); + + /** @var \Symfony\Component\Console\Helper\TableHelper $tableHelper */ + $tableHelper = $this->getHelperSet()->get('table'); + + // Display header line + $headers = array('State(s)', 'Id', sprintf('Message Preview (%s)', $locale)); + foreach ($fallbackCatalogues as $fallbackCatalogue) { + $headers[] = sprintf('Fallback Message Preview (%s)', $fallbackCatalogue->getLocale()); + } + $tableHelper->setHeaders($headers); + + // Iterate all message ids and determine their state + foreach ($allMessages as $domain => $messages) { + foreach (array_keys($messages) as $messageId) { + $value = $currentCatalogue->get($messageId, $domain); + $states = array(); + + if ($extractedCatalogue->defines($messageId, $domain)) { + if (!$currentCatalogue->defines($messageId, $domain)) { + $states[] = self::MESSAGE_MISSING; + } + } elseif ($currentCatalogue->defines($messageId, $domain)) { + $states[] = self::MESSAGE_UNUSED; + } + + if (!in_array(self::MESSAGE_UNUSED, $states) && true === $input->getOption('only-unused') + || !in_array(self::MESSAGE_MISSING, $states) && true === $input->getOption('only-missing')) { + continue; + } + + foreach ($fallbackCatalogues as $fallbackCatalogue) { + if ($fallbackCatalogue->defines($messageId, $domain) + && $value === $fallbackCatalogue->get($messageId, $domain)) { + $states[] = self::MESSAGE_EQUALS_FALLBACK; + break; + } + } + + $row = array($this->formatStates($states), $this->formatId($messageId), $this->sanitizeString($value)); + foreach ($fallbackCatalogues as $fallbackCatalogue) { + $row[] = $this->sanitizeString($fallbackCatalogue->get($messageId, $domain)); + } + + $tableHelper->addRow($row); + } + } + + $tableHelper->render($output); + } + + private function formatState($state) + { + if (self::MESSAGE_MISSING === $state) { + return 'x'; + } + + if (self::MESSAGE_UNUSED === $state) { + return 'o'; + } + + if (self::MESSAGE_EQUALS_FALLBACK === $state) { + return '='; + } + + return $state; + } + + private function formatStates(array $states) + { + $result = array(); + foreach ($states as $state) { + $result[] = $this->formatState($state); + } + + return implode(' ', $result); + } + + private function formatId($id) + { + return sprintf('%s', $id); + } + + private function sanitizeString($string, $lenght = 40) + { + $string = trim(preg_replace('/\s+/', ' ', $string)); + + if (function_exists('mb_strlen') && false !== $encoding = mb_detect_encoding($string)) { + if (mb_strlen($string, $encoding) > $lenght) { + return mb_substr($string, 0, $lenght - 3, $encoding).'...'; + } + } elseif (strlen($string) > $lenght) { + return substr($string, 0, $lenght - 3).'...'; + } + + return $string; + } +} From f039bde3d8a6c9b88b7b050a73b308fd0a3379f3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Mar 2014 17:28:48 +0100 Subject: [PATCH 2/2] [FrameworkBundle] fixed edge cases for translation:debug and tweaked the output --- .../Command/TranslationDebugCommand.php | 76 ++++++++++--------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index ccdeb59e36..a1f1360053 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -12,11 +12,13 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Translation\Catalogue\MergeOperation; +use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Translator; /** * Helps finding unused or missing translation messages in a given locale @@ -46,10 +48,11 @@ class TranslationDebugCommand extends ContainerAwareCommand )) ->setDescription('Displays translation messages informations') ->setHelp(<<%command.name% command helps finding unused or missing translation messages and -comparing them with the fallback ones by inspecting the templates and translation files of a given bundle. +The %command.name% command helps finding unused or missing translation +messages and comparing them with the fallback ones by inspecting the +templates and translation files of a given bundle. -You can display informations about a bundle translations in a specific locale: +You can display information about bundle translations in a specific locale: php %command.full_name% en AcmeDemoBundle @@ -81,12 +84,13 @@ EOF // Extract used messages $extractedCatalogue = new MessageCatalogue($locale); - $this->getContainer()->get('translation.extractor') - ->extract($bundle->getPath().'/Resources/views/', $extractedCatalogue); + $this->getContainer()->get('translation.extractor')->extract($bundle->getPath().'/Resources/views', $extractedCatalogue); // Load defined messages $currentCatalogue = new MessageCatalogue($locale); - $loader->loadMessages($bundle->getPath().'/Resources/translations', $currentCatalogue); + if (is_dir($bundle->getPath().'/Resources/translations')) { + $loader->loadMessages($bundle->getPath().'/Resources/translations', $currentCatalogue); + } // Merge defined and extracted messages to get all message ids $mergeOperation = new MergeOperation($extractedCatalogue, $currentCatalogue); @@ -110,32 +114,28 @@ EOF // Load the fallback catalogues $fallbackCatalogues = array(); - foreach ($this->getContainer()->get('translator')->getFallbackLocales() as $fallbackLocale) { - if ($fallbackLocale === $locale) { - continue; - } + $translator = $this->getContainer()->get('translator'); + if ($translator instanceof Translator) { + foreach ($translator->getFallbackLocales() as $fallbackLocale) { + if ($fallbackLocale === $locale) { + continue; + } - $fallbackCatalogue = new MessageCatalogue($fallbackLocale); - $loader->loadMessages($bundle->getPath().'/Resources/translations', $fallbackCatalogue); - $fallbackCatalogues[] = $fallbackCatalogue; + $fallbackCatalogue = new MessageCatalogue($fallbackLocale); + $loader->loadMessages($bundle->getPath().'/Resources/translations', $fallbackCatalogue); + $fallbackCatalogues[] = $fallbackCatalogue; + } } - // Display legend - $output->writeln(sprintf('Legend: %s Missing message %s Unused message %s Equals fallback message', - $this->formatState(self::MESSAGE_MISSING), - $this->formatState(self::MESSAGE_UNUSED), - $this->formatState(self::MESSAGE_EQUALS_FALLBACK) - )); - - /** @var \Symfony\Component\Console\Helper\TableHelper $tableHelper */ - $tableHelper = $this->getHelperSet()->get('table'); + /** @var \Symfony\Component\Console\Helper\Table $table */ + $table = new Table($output); // Display header line $headers = array('State(s)', 'Id', sprintf('Message Preview (%s)', $locale)); foreach ($fallbackCatalogues as $fallbackCatalogue) { $headers[] = sprintf('Fallback Message Preview (%s)', $fallbackCatalogue->getLocale()); } - $tableHelper->setHeaders($headers); + $table->setHeaders($headers); // Iterate all message ids and determine their state foreach ($allMessages as $domain => $messages) { @@ -157,9 +157,9 @@ EOF } foreach ($fallbackCatalogues as $fallbackCatalogue) { - if ($fallbackCatalogue->defines($messageId, $domain) - && $value === $fallbackCatalogue->get($messageId, $domain)) { + if ($fallbackCatalogue->defines($messageId, $domain) && $value === $fallbackCatalogue->get($messageId, $domain)) { $states[] = self::MESSAGE_EQUALS_FALLBACK; + break; } } @@ -169,25 +169,31 @@ EOF $row[] = $this->sanitizeString($fallbackCatalogue->get($messageId, $domain)); } - $tableHelper->addRow($row); + $table->addRow($row); } } - $tableHelper->render($output); + $table->render(); + + $output->writeln(''); + $output->writeln('Legend:'); + $output->writeln(sprintf(' %s Missing message', $this->formatState(self::MESSAGE_MISSING))); + $output->writeln(sprintf(' %s Unused message', $this->formatState(self::MESSAGE_UNUSED))); + $output->writeln(sprintf(' %s Same as the fallback message', $this->formatState(self::MESSAGE_EQUALS_FALLBACK))); } private function formatState($state) { if (self::MESSAGE_MISSING === $state) { - return 'x'; + return 'x'; } if (self::MESSAGE_UNUSED === $state) { - return 'o'; + return 'o'; } if (self::MESSAGE_EQUALS_FALLBACK === $state) { - return '='; + return '='; } return $state; @@ -208,16 +214,16 @@ EOF return sprintf('%s', $id); } - private function sanitizeString($string, $lenght = 40) + private function sanitizeString($string, $length = 40) { $string = trim(preg_replace('/\s+/', ' ', $string)); if (function_exists('mb_strlen') && false !== $encoding = mb_detect_encoding($string)) { - if (mb_strlen($string, $encoding) > $lenght) { - return mb_substr($string, 0, $lenght - 3, $encoding).'...'; + if (mb_strlen($string, $encoding) > $length) { + return mb_substr($string, 0, $length - 3, $encoding).'...'; } - } elseif (strlen($string) > $lenght) { - return substr($string, 0, $lenght - 3).'...'; + } elseif (strlen($string) > $length) { + return substr($string, 0, $length - 3).'...'; } return $string;