diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php new file mode 100644 index 0000000000000..3d6214b6e2e11 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * @author Abdellatif Ait boudad + */ +class LoggingTranslatorPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasAlias('logger')) { + return; + } + + if ($container->getParameter('translator.logging')) { + $translatorAlias = $container->getAlias('translator'); + $definition = $container->getDefinition((string) $translatorAlias); + $class = $container->getParameterBag()->resolveValue($definition->getClass()); + + $refClass = new \ReflectionClass($class); + if ($refClass->implementsInterface('Symfony\Component\Translation\TranslatorInterface') && $refClass->implementsInterface('Symfony\Component\Translation\TranslatorBagInterface')) { + $container->getDefinition('translator.logging')->setDecoratedService('translator'); + } + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 0a7e2cc4cd495..36ce2901e0bae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -22,6 +22,16 @@ */ class Configuration implements ConfigurationInterface { + private $debug; + + /** + * @param bool $debug Whether debugging is enabled or not + */ + public function __construct($debug) + { + $this->debug = (bool) $debug; + } + /** * Generates the configuration tree builder. * @@ -441,6 +451,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode) ->canBeEnabled() ->children() ->scalarNode('fallback')->defaultValue('en')->end() + ->booleanNode('logging')->defaultValue($this->debug)->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 81dbb3c0c61c4..04c2076dc6b07 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -169,6 +169,14 @@ public function load(array $configs, ContainerBuilder $container) )); } + /** + * {@inheritdoc} + */ + public function getConfiguration(array $config, ContainerBuilder $container) + { + return new Configuration($container->getParameter('kernel.debug')); + } + /** * Loads Form configuration. * @@ -627,6 +635,8 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder } $translator->addMethodCall('setFallbackLocales', array($config['fallback'])); + $container->setParameter('translator.logging', $config['logging']); + // Discover translation directories $dirs = array(); if (class_exists('Symfony\Component\Validator\Validator')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 387031ddc35a0..2cde83fab6bc2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -20,6 +20,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingResolverPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslatorPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheWarmerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheClearerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; @@ -77,6 +78,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new AddConsoleCommandPass()); $container->addCompilerPass(new FormPass()); $container->addCompilerPass(new TranslatorPass()); + $container->addCompilerPass(new LoggingTranslatorPass()); $container->addCompilerPass(new AddCacheWarmerPass()); $container->addCompilerPass(new AddCacheClearerPass()); $container->addCompilerPass(new TranslationExtractorPass()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml index 300f24da7f6e7..1f4fd9e40715e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml @@ -6,6 +6,7 @@ Symfony\Bundle\FrameworkBundle\Translation\Translator + Symfony\Component\Translation\LoggingTranslator Symfony\Component\Translation\IdentityTranslator Symfony\Component\Translation\MessageSelector Symfony\Component\Translation\Loader\PhpFileLoader @@ -46,6 +47,12 @@ + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php new file mode 100644 index 0000000000000..9fdee8fd8edb4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; + +class LoggingTranslatorPassTest extends \PHPUnit_Framework_TestCase +{ + public function testProcess() + { + $definition = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $parameterBag = $this->getMock('Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface'); + + $container->expects($this->once()) + ->method('hasAlias') + ->will($this->returnValue(true)); + + $container->expects($this->once()) + ->method('getParameter') + ->will($this->returnValue(true)); + + $container->expects($this->once()) + ->method('getAlias') + ->will($this->returnValue('translation.default')); + + $container->expects($this->exactly(2)) + ->method('getDefinition') + ->will($this->returnValue($definition)); + + $definition->expects($this->once()) + ->method('getClass') + ->will($this->returnValue("%translator.class%")); + + $parameterBag->expects($this->once()) + ->method('resolveValue') + ->will($this->returnValue("Symfony\Bundle\FrameworkBundle\Translation\Translator")); + + $container->expects($this->once()) + ->method('getParameterBag') + ->will($this->returnValue($parameterBag)); + + $pass = new LoggingTranslatorPass(); + $pass->process($container); + } + + public function testThatCompilerPassIsIgnoredIfThereIsNotLoggerDefinition() + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $container->expects($this->once()) + ->method('hasAlias') + ->will($this->returnValue(false)); + + $pass = new LoggingTranslatorPass(); + $pass->process($container); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 4d41cf23551b8..31e761f28b3db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -19,7 +19,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase public function testDefaultConfig() { $processor = new Processor(); - $config = $processor->processConfiguration(new Configuration(), array(array('secret' => 's3cr3t'))); + $config = $processor->processConfiguration(new Configuration(true), array(array('secret' => 's3cr3t'))); $this->assertEquals( array_merge(array('secret' => 's3cr3t', 'trusted_hosts' => array()), self::getBundleDefaultConfig()), @@ -33,7 +33,7 @@ public function testDefaultConfig() public function testValidTrustedProxies($trustedProxies, $processedProxies) { $processor = new Processor(); - $configuration = new Configuration(); + $configuration = new Configuration(true); $config = $processor->processConfiguration($configuration, array(array( 'secret' => 's3cr3t', 'trusted_proxies' => $trustedProxies, @@ -62,7 +62,7 @@ public function getTestValidTrustedProxiesData() public function testInvalidTypeTrustedProxies() { $processor = new Processor(); - $configuration = new Configuration(); + $configuration = new Configuration(true); $processor->processConfiguration($configuration, array( array( 'secret' => 's3cr3t', @@ -77,7 +77,7 @@ public function testInvalidTypeTrustedProxies() public function testInvalidValueTrustedProxies() { $processor = new Processor(); - $configuration = new Configuration(); + $configuration = new Configuration(true); $processor->processConfiguration($configuration, array( array( 'secret' => 's3cr3t', @@ -123,6 +123,7 @@ protected static function getBundleDefaultConfig() 'translator' => array( 'enabled' => false, 'fallback' => 'en', + 'logging' => true, ), 'validation' => array( 'enabled' => false, diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index c0eeb7e94f15f..d85b70ad0e97d 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG ----- * added possibility to cache catalogues + * added TranslatorBagInterface + * added LoggingTranslator 2.5.0 ----- diff --git a/src/Symfony/Component/Translation/LoggingTranslator.php b/src/Symfony/Component/Translation/LoggingTranslator.php new file mode 100644 index 0000000000000..ae9be1f28fb42 --- /dev/null +++ b/src/Symfony/Component/Translation/LoggingTranslator.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Psr\Log\LoggerInterface; + +/** + * @author Abdellatif Ait boudad + */ +class LoggingTranslator implements TranslatorInterface +{ + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Translator $translator + * @param LoggerInterface $logger + */ + public function __construct($translator, LoggerInterface $logger) + { + if (!($translator instanceof TranslatorInterface && $translator instanceof TranslatorBagInterface)) { + throw new \InvalidArgumentException(sprintf('The Translator "%s" must implements TranslatorInterface and TranslatorBagInterface.', get_class($translator))); + } + + $this->translator = $translator; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function trans($id, array $parameters = array(), $domain = null, $locale = null) + { + $trans = $this->translator->trans($id, $parameters , $domain , $locale); + $this->log($id, $domain, $locale); + + return $trans; + } + + /** + * {@inheritdoc} + */ + public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) + { + $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); + $this->log($id, $domain, $locale); + + return $trans; + } + + /** + * {@inheritdoc} + * + * @api + */ + public function setLocale($locale) + { + $this->translator->setLocale($locale); + } + + /** + * {@inheritdoc} + * + * @api + */ + public function getLocale() + { + return $this->translator->getLocale(); + } + + /** + * Passes through all unknown calls onto the translator object. + */ + public function __call($method, $args) + { + return call_user_func_array(array($this->translator, $method), $args); + } + + /** + * Logs for missing translations. + * + * @param string $id + * @param string|null $domain + * @param string|null $locale + */ + private function log($id, $domain, $locale) + { + if (null === $locale) { + $locale = $this->getLocale(); + } + + if (null === $domain) { + $domain = 'messages'; + } + + $id = (string) $id; + $catalogue = $this->translator->getCatalogue($locale); + if ($catalogue->defines($id, $domain)) { + return; + } + + if ($catalogue->has($id, $domain)) { + $this->logger->debug('Translation use fallback catalogue.', array('id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale())); + } else { + $this->logger->warning('Translation not found.', array('id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale())); + } + } +} diff --git a/src/Symfony/Component/Translation/Tests/LoggingTranslatorTest.php b/src/Symfony/Component/Translation/Tests/LoggingTranslatorTest.php new file mode 100644 index 0000000000000..ab98d72e7425c --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/LoggingTranslatorTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Tests; + +use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\LoggingTranslator; +use Symfony\Component\Translation\Loader\ArrayLoader; + +class LoggingTranslatorTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!interface_exists('Psr\Log\LoggerInterface')) { + $this->markTestSkipped('The "LoggerInterface" is not available'); + } + } + + public function testTransWithNoTranslationIsLogged() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + $logger->expects($this->exactly(2)) + ->method('warning') + ->with('Translation not found.') + ; + + $translator = new Translator('ar'); + $loggableTranslator = new LoggingTranslator($translator, $logger); + $loggableTranslator->transChoice('some_message2', 10, array('%count%' => 10)); + $loggableTranslator->trans('bar'); + } + + public function testTransChoiceFallbackIsLogged() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + $logger->expects($this->once()) + ->method('debug') + ->with('Translation use fallback catalogue.') + ; + + $translator = new Translator('ar'); + $translator->setFallbackLocales(array('en')); + $translator->addLoader('array', new ArrayLoader()); + $translator->addResource('array', array('some_message2' => 'one thing|%count% things'), 'en'); + $loggableTranslator = new LoggingTranslator($translator, $logger); + $loggableTranslator->transChoice('some_message2', 10, array('%count%' => 10)); + } +} diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index b141ab5e8bacf..305a25d42d4b0 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\MessageSelector; use Symfony\Component\Translation\Loader\ArrayLoader; +use Symfony\Component\Translation\MessageCatalogue; class TranslatorTest extends \PHPUnit_Framework_TestCase { @@ -74,6 +75,16 @@ public function testSetValidLocale($locale) $this->assertEquals($locale, $translator->getLocale()); } + public function testGetCatalogue() + { + $translator = new Translator('en'); + + $this->assertEquals(new MessageCatalogue('en'), $translator->getCatalogue()); + + $translator->setLocale('fr'); + $this->assertEquals(new MessageCatalogue('fr'), $translator->getCatalogue('fr')); + } + public function testSetFallbackLocales() { $translator = new Translator('en'); diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 9483e6aa7f309..356a21b5923e1 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -22,7 +22,7 @@ * * @api */ -class Translator implements TranslatorInterface +class Translator implements TranslatorInterface, TranslatorBagInterface { /** * @var MessageCatalogueInterface[] @@ -256,6 +256,22 @@ public function transChoice($id, $number, array $parameters = array(), $domain = return strtr($this->selector->choose($catalogue->get($id, $domain), (int) $number, $locale), $parameters); } + /** + * {@inheritdoc} + */ + public function getCatalogue($locale = null) + { + if (null === $locale) { + $locale = $this->getLocale(); + } + + if (!isset($this->catalogues[$locale])) { + $this->loadCatalogue($locale); + } + + return $this->catalogues[$locale]; + } + /** * Gets the loaders. * diff --git a/src/Symfony/Component/Translation/TranslatorBagInterface.php b/src/Symfony/Component/Translation/TranslatorBagInterface.php new file mode 100644 index 0000000000000..e0312d9135576 --- /dev/null +++ b/src/Symfony/Component/Translation/TranslatorBagInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +/** + * TranslatorBagInterface + * + * @author Abdellatif Ait boudad + */ +interface TranslatorBagInterface +{ + /** + * Gets the catalogue by locale. + * + * @param string|null $locale The locale or null to use the default + * + * @return MessageCatalogueInterface + */ + public function getCatalogue($locale = null); +} diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 820a9a648d053..8824d293aacc3 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -21,11 +21,13 @@ "require-dev": { "symfony/config": "~2.0", "symfony/intl": "~2.3", - "symfony/yaml": "~2.2" + "symfony/yaml": "~2.2", + "psr/log": "~1.0" }, "suggest": { "symfony/config": "", - "symfony/yaml": "" + "symfony/yaml": "", + "psr/log": "To use logging capability in translator" }, "autoload": { "psr-0": { "Symfony\\Component\\Translation\\": "" }