2014-01-19 23:09:51 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Bundle\FrameworkBundle\Command ;
2015-04-11 17:38:38 +01:00
use Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader ;
2015-05-09 15:43:35 +01:00
use Symfony\Component\Console\Style\SymfonyStyle ;
2014-01-19 23:09:51 +00:00
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputOption ;
2015-04-11 17:38:38 +01:00
use Symfony\Component\Console\Output\OutputInterface ;
use Symfony\Component\HttpKernel\Kernel ;
use Symfony\Component\Translation\Catalogue\MergeOperation ;
2014-01-19 23:09:51 +00:00
use Symfony\Component\Translation\MessageCatalogue ;
2014-03-03 16:28:48 +00:00
use Symfony\Component\Translation\Translator ;
2014-01-19 23:09:51 +00:00
/**
* Helps finding unused or missing translation messages in a given locale
* and comparing them with the fallback ones .
*
* @ author Florian Voutzinos < florian @ voutzinos . com >
*/
class TranslationDebugCommand extends ContainerAwareCommand
{
const MESSAGE_MISSING = 0 ;
const MESSAGE_UNUSED = 1 ;
const MESSAGE_EQUALS_FALLBACK = 2 ;
/**
* { @ inheritdoc }
*/
protected function configure ()
{
$this
2014-08-09 16:20:46 +01:00
-> setName ( 'debug:translation' )
-> setAliases ( array (
'translation:debug' ,
))
2014-01-19 23:09:51 +00:00
-> setDefinition ( array (
new InputArgument ( 'locale' , InputArgument :: REQUIRED , 'The locale' ),
2015-01-18 20:33:17 +00:00
new InputArgument ( 'bundle' , InputArgument :: OPTIONAL , 'The bundle name or directory where to load the messages, defaults to app/Resources folder' ),
2014-01-19 23:09:51 +00:00
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' ),
2015-04-11 17:38:38 +01:00
new InputOption ( 'all' , null , InputOption :: VALUE_NONE , 'Load messages from all registered bundles' ),
2014-01-19 23:09:51 +00:00
))
2015-01-18 20:33:17 +00:00
-> setDescription ( 'Displays translation messages information' )
2014-01-19 23:09:51 +00:00
-> setHelp ( <<< EOF
2014-03-03 16:28:48 +00:00
The < info >% command . name %</ info > command helps finding unused or missing translation
messages and comparing them with the fallback ones by inspecting the
2015-01-18 20:33:17 +00:00
templates and translation files of a given bundle or the app folder .
2014-01-19 23:09:51 +00:00
2014-03-03 16:28:48 +00:00
You can display information about bundle translations in a specific locale :
2014-01-19 23:09:51 +00:00
2015-01-04 09:52:37 +00:00
< info > php % command . full_name % en AcmeDemoBundle </ info >
2014-01-19 23:09:51 +00:00
You can also specify a translation domain for the search :
2015-01-04 09:52:37 +00:00
< info > php % command . full_name % -- domain = messages en AcmeDemoBundle </ info >
2014-01-19 23:09:51 +00:00
You can only display missing messages :
2015-01-04 09:52:37 +00:00
< info > php % command . full_name % -- only - missing en AcmeDemoBundle </ info >
2014-01-19 23:09:51 +00:00
You can only display unused messages :
2015-01-04 09:52:37 +00:00
< info > php % command . full_name % -- only - unused en AcmeDemoBundle </ info >
2014-03-13 04:05:55 +00:00
2015-01-18 20:33:17 +00:00
You can display information about app translations in a specific locale :
< info > php % command . full_name % en </ info >
2015-04-11 17:38:38 +01:00
You can display information about translations in all registered bundles in a specific locale :
< info > php % command . full_name % -- all en </ info >
2014-01-19 23:09:51 +00:00
EOF
)
;
}
/**
* { @ inheritdoc }
*/
protected function execute ( InputInterface $input , OutputInterface $output )
{
2015-05-09 15:43:35 +01:00
$output = new SymfonyStyle ( $input , $output );
2015-01-13 14:10:03 +00:00
if ( false !== strpos ( $input -> getFirstArgument (), ':d' )) {
2015-05-09 15:43:35 +01:00
$output -> caution ( 'The use of "translation:debug" command is deprecated since version 2.7 and will be removed in 3.0. Use the "debug:translation" instead.' );
2015-01-13 14:10:03 +00:00
}
2014-01-19 23:09:51 +00:00
$locale = $input -> getArgument ( 'locale' );
$domain = $input -> getOption ( 'domain' );
2015-04-11 17:38:38 +01:00
/** @var TranslationLoader $loader */
2014-01-19 23:09:51 +00:00
$loader = $this -> getContainer () -> get ( 'translation.loader' );
2015-04-11 17:38:38 +01:00
/** @var Kernel $kernel */
2015-01-18 20:33:17 +00:00
$kernel = $this -> getContainer () -> get ( 'kernel' );
// Define Root Path to App folder
2015-06-25 13:52:11 +01:00
$transPaths = array ( $kernel -> getRootDir () . '/Resources/' );
2015-01-18 20:33:17 +00:00
// Override with provided Bundle info
if ( null !== $input -> getArgument ( 'bundle' )) {
try {
2015-06-25 13:52:11 +01:00
$bundle = $kernel -> getBundle ( $input -> getArgument ( 'bundle' ));
$transPaths = array (
$bundle -> getPath () . '/Resources/' ,
sprintf ( '%s/Resources/%s/' , $kernel -> getRootDir (), $bundle -> getName ()),
);
2015-01-18 20:33:17 +00:00
} catch ( \InvalidArgumentException $e ) {
// such a bundle does not exist, so treat the argument as path
2015-06-25 13:52:11 +01:00
$transPaths = array ( $input -> getArgument ( 'bundle' ) . '/Resources/' );
2015-01-18 20:33:17 +00:00
2015-06-25 13:52:11 +01:00
if ( ! is_dir ( $transPaths [ 0 ])) {
throw new \InvalidArgumentException ( sprintf ( '"%s" is neither an enabled bundle nor a directory.' , $transPaths [ 0 ]));
2015-01-18 20:33:17 +00:00
}
}
2015-04-11 17:38:38 +01:00
} elseif ( $input -> getOption ( 'all' )) {
foreach ( $kernel -> getBundles () as $bundle ) {
2015-07-01 15:16:54 +01:00
$transPaths [] = $bundle -> getPath () . '/Resources/' ;
$transPaths [] = sprintf ( '%s/Resources/%s/' , $kernel -> getRootDir (), $bundle -> getName ());
2015-04-11 17:38:38 +01:00
}
2015-01-18 20:33:17 +00:00
}
2014-01-19 23:09:51 +00:00
// Extract used messages
2015-07-01 15:16:54 +01:00
$extractedCatalogue = $this -> extractMessages ( $locale , $transPaths );
2014-01-19 23:09:51 +00:00
// Load defined messages
2015-07-01 15:16:54 +01:00
$currentCatalogue = $this -> loadCurrentMessages ( $locale , $transPaths , $loader );
2014-01-19 23:09:51 +00:00
// 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 ])) {
2015-05-09 15:43:35 +01:00
$outputMessage = sprintf ( 'No defined or extracted messages for locale "%s"' , $locale );
2014-01-19 23:09:51 +00:00
if ( null !== $domain ) {
2015-05-09 15:43:35 +01:00
$outputMessage .= sprintf ( ' and domain "%s"' , $domain );
2015-04-11 17:38:38 +01:00
}
2014-01-19 23:09:51 +00:00
2015-05-09 15:43:35 +01:00
$output -> warning ( $outputMessage );
2014-01-19 23:09:51 +00:00
return ;
}
2015-04-11 17:38:38 +01:00
2014-01-19 23:09:51 +00:00
// Load the fallback catalogues
2015-07-01 15:16:54 +01:00
$fallbackCatalogues = $this -> loadFallbackCatalogues ( $locale , $transPaths , $loader );
2014-01-19 23:09:51 +00:00
// Display header line
2015-04-26 11:57:37 +01:00
$headers = array ( 'State' , 'Domain' , 'Id' , sprintf ( 'Message Preview (%s)' , $locale ));
2014-01-19 23:09:51 +00:00
foreach ( $fallbackCatalogues as $fallbackCatalogue ) {
$headers [] = sprintf ( 'Fallback Message Preview (%s)' , $fallbackCatalogue -> getLocale ());
}
2015-05-09 15:43:35 +01:00
$rows = array ();
2014-01-19 23:09:51 +00:00
// 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 ) {
2014-03-03 16:28:48 +00:00
if ( $fallbackCatalogue -> defines ( $messageId , $domain ) && $value === $fallbackCatalogue -> get ( $messageId , $domain )) {
2014-01-19 23:09:51 +00:00
$states [] = self :: MESSAGE_EQUALS_FALLBACK ;
2014-03-03 16:28:48 +00:00
2014-01-19 23:09:51 +00:00
break ;
}
}
2015-03-08 20:28:01 +00:00
$row = array ( $this -> formatStates ( $states ), $domain , $this -> formatId ( $messageId ), $this -> sanitizeString ( $value ));
2014-01-19 23:09:51 +00:00
foreach ( $fallbackCatalogues as $fallbackCatalogue ) {
$row [] = $this -> sanitizeString ( $fallbackCatalogue -> get ( $messageId , $domain ));
}
2015-05-09 15:43:35 +01:00
$rows [] = $row ;
2014-01-19 23:09:51 +00:00
}
2015-04-13 16:21:02 +01:00
}
2014-01-19 23:09:51 +00:00
2015-05-09 15:43:35 +01:00
$output -> table ( $headers , $rows );
2014-01-19 23:09:51 +00:00
}
private function formatState ( $state )
{
if ( self :: MESSAGE_MISSING === $state ) {
2015-09-28 14:27:37 +01:00
return '<error> missing </error>' ;
2014-01-19 23:09:51 +00:00
}
if ( self :: MESSAGE_UNUSED === $state ) {
2015-09-28 14:27:37 +01:00
return '<comment> unused </comment>' ;
2014-01-19 23:09:51 +00:00
}
if ( self :: MESSAGE_EQUALS_FALLBACK === $state ) {
2015-09-28 14:27:37 +01:00
return '<info> fallback </info>' ;
2014-01-19 23:09:51 +00:00
}
return $state ;
}
private function formatStates ( array $states )
{
$result = array ();
foreach ( $states as $state ) {
$result [] = $this -> formatState ( $state );
}
return implode ( ' ' , $result );
}
private function formatId ( $id )
{
2015-09-28 14:27:37 +01:00
return sprintf ( '<fg=cyan;options=bold>%s</>' , $id );
2014-01-19 23:09:51 +00:00
}
2014-03-03 16:28:48 +00:00
private function sanitizeString ( $string , $length = 40 )
2014-01-19 23:09:51 +00:00
{
$string = trim ( preg_replace ( '/\s+/' , ' ' , $string ));
2015-10-14 15:40:43 +01:00
if ( false !== $encoding = mb_detect_encoding ( $string , null , true )) {
2014-03-03 16:28:48 +00:00
if ( mb_strlen ( $string , $encoding ) > $length ) {
return mb_substr ( $string , 0 , $length - 3 , $encoding ) . '...' ;
2014-01-19 23:09:51 +00:00
}
2014-03-03 16:28:48 +00:00
} elseif ( strlen ( $string ) > $length ) {
return substr ( $string , 0 , $length - 3 ) . '...' ;
2014-01-19 23:09:51 +00:00
}
return $string ;
}
2015-04-11 17:38:38 +01:00
/**
* @ param string $locale
2015-07-01 15:16:54 +01:00
* @ param array $transPaths
2015-04-11 17:38:38 +01:00
*
* @ return MessageCatalogue
*/
2015-07-01 15:16:54 +01:00
private function extractMessages ( $locale , $transPaths )
2015-04-11 17:38:38 +01:00
{
$extractedCatalogue = new MessageCatalogue ( $locale );
2015-07-01 15:16:54 +01:00
foreach ( $transPaths as $path ) {
$path = $path . 'views' ;
if ( is_dir ( $path )) {
$this -> getContainer () -> get ( 'translation.extractor' ) -> extract ( $path , $extractedCatalogue );
}
2015-04-11 17:38:38 +01:00
}
return $extractedCatalogue ;
}
/**
* @ param string $locale
2015-07-01 15:16:54 +01:00
* @ param array $transPaths
2015-04-11 17:38:38 +01:00
* @ param TranslationLoader $loader
*
* @ return MessageCatalogue
*/
2015-07-01 15:16:54 +01:00
private function loadCurrentMessages ( $locale , $transPaths , TranslationLoader $loader )
2015-04-11 17:38:38 +01:00
{
$currentCatalogue = new MessageCatalogue ( $locale );
2015-07-01 15:16:54 +01:00
foreach ( $transPaths as $path ) {
$path = $path . 'translations' ;
if ( is_dir ( $path )) {
$loader -> loadMessages ( $path , $currentCatalogue );
}
2015-04-11 17:38:38 +01:00
}
return $currentCatalogue ;
}
/**
* @ param string $locale
2015-07-01 15:16:54 +01:00
* @ param array $transPaths
2015-04-11 17:38:38 +01:00
* @ param TranslationLoader $loader
*
* @ return MessageCatalogue []
*/
2015-07-01 15:16:54 +01:00
private function loadFallbackCatalogues ( $locale , $transPaths , TranslationLoader $loader )
2015-04-11 17:38:38 +01:00
{
$fallbackCatalogues = array ();
$translator = $this -> getContainer () -> get ( 'translator' );
if ( $translator instanceof Translator ) {
foreach ( $translator -> getFallbackLocales () as $fallbackLocale ) {
if ( $fallbackLocale === $locale ) {
continue ;
}
$fallbackCatalogue = new MessageCatalogue ( $fallbackLocale );
2015-07-01 15:16:54 +01:00
foreach ( $transPaths as $path ) {
$path = $path . 'translations' ;
if ( is_dir ( $path )) {
$loader -> loadMessages ( $path , $fallbackCatalogue );
}
}
2015-04-11 17:38:38 +01:00
$fallbackCatalogues [] = $fallbackCatalogue ;
}
}
return $fallbackCatalogues ;
}
2014-01-19 23:09:51 +00:00
}