[Translation][Profiler] Added a Translation profiler.

This commit is contained in:
Abdellatif Ait boudad 2015-03-20 23:12:36 +00:00
parent 007386c5f2
commit c923b2ab88
7 changed files with 481 additions and 5 deletions

View File

@ -35,6 +35,7 @@ use Symfony\Component\Validator\Validation;
class FrameworkExtension extends Extension
{
private $formConfigEnabled = false;
private $translationConfigEnabled = false;
private $sessionConfigEnabled = false;
/**
@ -116,8 +117,8 @@ class FrameworkExtension extends Extension
$this->registerEsiConfiguration($config['esi'], $container, $loader);
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
$this->registerTranslatorConfiguration($config['translator'], $container);
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
if (isset($config['router'])) {
$this->registerRouterConfiguration($config['router'], $container, $loader);
@ -288,10 +289,15 @@ class FrameworkExtension extends Extension
$loader->load('profiling.xml');
$loader->load('collectors.xml');
if (true === $this->formConfigEnabled) {
if ($this->formConfigEnabled) {
$loader->load('form_debug.xml');
}
if ($this->translationConfigEnabled) {
$loader->load('translation_debug.xml');
$container->getDefinition('translator.data_collector')->setDecoratedService('translator');
}
$container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']);
$container->setParameter('profiler_listener.only_master_requests', $config['only_master_requests']);
@ -644,6 +650,7 @@ class FrameworkExtension extends Extension
if (!$this->isConfigEnabled($container, $config)) {
return;
}
$this->translationConfigEnabled = true;
// Use the "real" translator instead of the identity default
$container->setAlias('translator', 'translator.default');
@ -865,9 +872,9 @@ class FrameworkExtension extends Extension
/**
* Loads the serializer configuration.
*
* @param array $config A serializer configuration array
* @param array $config A serializer configuration array
* @param ContainerBuilder $container A ContainerBuilder instance
* @param XmlFileLoader $loader An XmlFileLoader instance
* @param XmlFileLoader $loader An XmlFileLoader instance
*/
private function registerSerializerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{

View File

@ -0,0 +1,19 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- DataCollectorTranslator -->
<service id="translator.data_collector" class="Symfony\Component\Translation\DataCollectorTranslator" public="false">
<argument type="service" id="translator.data_collector.inner" />
</service>
<!-- DataCollector -->
<service id="data_collector.translation" class="Symfony\Component\Translation\DataCollector\TranslationDataCollector">
<tag name="data_collector" template="@WebProfiler/Collector/translation.html.twig" id="translation" priority="255" />
<argument type="service" id="translator.data_collector" />
</service>
</services>
</container>

View File

@ -0,0 +1,93 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% import _self as translator %}
{% block toolbar %}
{% if collector.messages|length %}
{% set icon %}
<svg width="28" height="28" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 417 300" enable-background="new 0 0 417 300" xml:space="preserve"><g id="Layer_1_1_"><g id="outline_1_"><path fill="#B5B5B6" d="M275.9,145c0,18.2-14.799,33-33,33H120.701l-36.3,42l-0.3-42H40c-18.2,0-33-14.8-33-33V44c0-18.2,14.8-33,33-33h202.9c18.199,0,33,14.8,33,33V145L275.9,145z"/></g><g enable-background="new"><path fill="#FFFFFF" d="M194.501,146.962h-23.898l-9.5-24.715h-43.492l-8.98,24.715H85.326l42.379-108.805h23.23L194.501,146.962zM154.052,103.915L139.06,63.54l-14.695,40.375H154.052z"/></g></g><g id="Layer_2_1_"><g id="japanese"><g id="outline"><path fill="#414141" d="M141.451,214c0,18.2,14.8,33,33,33h122.2l36.301,42l0.301-42h44.1c18.201,0,33-14.8,33-33V113c0-18.2-14.799-33-33-33H174.453c-18.201,0-33,14.8-33,33L141.451,214L141.451,214z"/></g><g enable-background="new"><path fill="#FFFFFF" d="M312.158,143.327c-0.455,1.672-0.912,3.344-1.215,5.016c22.039,6.08,31.766,21.431,31.766,38.455c0,24.318-18.238,40.733-57.301,45.598c-1.217-3.952-5.016-11.248-7.904-15.352c27.359-3.04,45.295-12.159,45.295-29.791c0-5.016-1.672-16.871-18.088-22.19c-6.688,15.199-16.871,29.335-28.727,39.519c0.607,1.976,1.367,3.647,2.127,5.167l-15.654,10.032c-0.76-1.521-1.52-3.192-2.129-5.017c-7.6,4.256-15.959,6.992-24.471,6.992c-13.375,0-22.189-8.512-22.189-22.647c0-20.975,16.111-37.542,37.693-46.357c-0.305-6.536-0.305-13.223-0.305-20.215c-11.398,0.304-23.711,0.608-29.789,0.456l-0.912-17.783c6.99,0.152,19.758,0.152,31.006,0.152c0.305-6.536,0.457-14.135,0.76-20.519l23.863,1.824c-0.305,1.52-1.52,2.736-4.104,3.04c-0.457,4.408-0.76,10.184-1.217,15.047c16.568-0.76,37.391-2.736,54.262-6.384l1.672,18.391c-16.719,3.04-38.605,4.56-56.846,5.168c-0.15,5.319-0.303,10.487-0.303,15.503c6.383-1.52,15.654-2.432,22.799-1.976c0.607-2.28,1.063-4.56,1.215-6.84L312.158,143.327z M255.77,198.044c-1.672-8.056-2.736-17.479-3.496-27.814c-12.008,5.927-20.215,15.199-20.215,25.382c0,8.664,6.535,8.36,8.512,8.209C245.281,203.668,250.449,201.539,255.77,198.044zM286.473,162.021c-2.129-0.304-10.033,0.305-16.871,2.128c0.455,7.6,0.91,14.591,1.975,20.671C277.504,178.589,282.672,170.686,286.473,162.021z"/></g></g></g></svg>
{% if collector.countMissings %}
{% set status_color = "red" %}
{% elseif collector.countFallbacks %}
{% set status_color = "yellow" %}
{% endif %}
{% set error_count = collector.countMissings + collector.countFallbacks %}
<span class="sf-toolbar-status{% if status_color is defined %} sf-toolbar-status-{{ status_color }}{% endif %}">{{ error_count ?: collector.countdefines }}</span>
{% endset %}
{% set text %}
{% if collector.countMissings %}
<div class="sf-toolbar-info-piece">
<b>Missing messages</b>
<span class="sf-toolbar-status sf-toolbar-status-red">{{ collector.countMissings }}</span>
</div>
{% endif %}
{% if collector.countFallbacks %}
<div class="sf-toolbar-info-piece">
<b>Fallback messages</b>
<span class="sf-toolbar-status sf-toolbar-status-yellow">{{ collector.countFallbacks }}</span>
</div>
{% endif %}
{% if collector.countdefines %}
<div class="sf-toolbar-info-piece">
<b>Defined messages</b>
<span class="sf-toolbar-status sf-toolbar-status-green">{{ collector.countdefines }}</span>
</div>
{% endif %}
{% endset %}
{% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon"><svg width="35" height="28" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 417 300" enable-background="new 0 0 417 300" xml:space="preserve"><g id="Layer_1_1_"><g id="outline_1_"><path fill="#B5B5B6" d="M275.9,145c0,18.2-14.799,33-33,33H120.701l-36.3,42l-0.3-42H40c-18.2,0-33-14.8-33-33V44c0-18.2,14.8-33,33-33h202.9c18.199,0,33,14.8,33,33V145L275.9,145z"/></g><g enable-background="new"><path fill="#FFFFFF" d="M194.501,146.962h-23.898l-9.5-24.715h-43.492l-8.98,24.715H85.326l42.379-108.805h23.23L194.501,146.962zM154.052,103.915L139.06,63.54l-14.695,40.375H154.052z"/></g></g><g id="Layer_2_1_"><g id="japanese"><g id="outline"><path fill="#414141" d="M141.451,214c0,18.2,14.8,33,33,33h122.2l36.301,42l0.301-42h44.1c18.201,0,33-14.8,33-33V113c0-18.2-14.799-33-33-33H174.453c-18.201,0-33,14.8-33,33L141.451,214L141.451,214z"/></g><g enable-background="new"><path fill="#FFFFFF" d="M312.158,143.327c-0.455,1.672-0.912,3.344-1.215,5.016c22.039,6.08,31.766,21.431,31.766,38.455c0,24.318-18.238,40.733-57.301,45.598c-1.217-3.952-5.016-11.248-7.904-15.352c27.359-3.04,45.295-12.159,45.295-29.791c0-5.016-1.672-16.871-18.088-22.19c-6.688,15.199-16.871,29.335-28.727,39.519c0.607,1.976,1.367,3.647,2.127,5.167l-15.654,10.032c-0.76-1.521-1.52-3.192-2.129-5.017c-7.6,4.256-15.959,6.992-24.471,6.992c-13.375,0-22.189-8.512-22.189-22.647c0-20.975,16.111-37.542,37.693-46.357c-0.305-6.536-0.305-13.223-0.305-20.215c-11.398,0.304-23.711,0.608-29.789,0.456l-0.912-17.783c6.99,0.152,19.758,0.152,31.006,0.152c0.305-6.536,0.457-14.135,0.76-20.519l23.863,1.824c-0.305,1.52-1.52,2.736-4.104,3.04c-0.457,4.408-0.76,10.184-1.217,15.047c16.568-0.76,37.391-2.736,54.262-6.384l1.672,18.391c-16.719,3.04-38.605,4.56-56.846,5.168c-0.15,5.319-0.303,10.487-0.303,15.503c6.383-1.52,15.654-2.432,22.799-1.976c0.607-2.28,1.063-4.56,1.215-6.84L312.158,143.327z M255.77,198.044c-1.672-8.056-2.736-17.479-3.496-27.814c-12.008,5.927-20.215,15.199-20.215,25.382c0,8.664,6.535,8.36,8.512,8.209C245.281,203.668,250.449,201.539,255.77,198.044zM286.473,162.021c-2.129-0.304-10.033,0.305-16.871,2.128c0.455,7.6,0.91,14.591,1.975,20.671C277.504,178.589,282.672,170.686,286.473,162.021z"/></g></g></g></svg></span>
<strong>Translation</strong>
</span>
{% endblock %}
{% block panel %}
{% if collector.messages is empty %}
<h2>Translations</h2>
<p>
<em>No translations have been called.</em>
</p>
{% else %}
{{ block('panelContent') }}
{% endif %}
{% endblock %}
{% block panelContent %}
<h2>Called Translations</h2>
<ul>
<li><strong>Defined messages: {{ collector.countdefines }}</strong></li>
<li><strong>Fallback messages: {{ collector.countFallbacks }}</strong></li>
<li><strong>Missing messages: {{ collector.countMissings }}</strong></li>
</ul>
<table>
<tr>
<th>State</th>
<th>Locale</th>
<th>Domain</th>
<th>Id</th>
<th>Message Preview</th>
</tr>
{% for message in collector.messages %}
<tr>
<td><code>{{ translator.state(message) }}</code></td>
<td><code>{{ message.locale }}</code></td>
<td><code>{{ message.domain }}</code></td>
<td><code>{{ message.id }}</code></td>
<td><code>{{ message.translation }}</code></td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% macro state(translation) %}
{% if translation.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK') %}
same as fallback
{% elseif translation.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_MISSING') %}
missing
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,132 @@
<?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\Component\Translation\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Translation\DataCollectorTranslator;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var DataCollectorTranslator
*/
private $translator;
/**
* @param DataCollectorTranslator $translator
*/
public function __construct(DataCollectorTranslator $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function lateCollect()
{
$this->data = $this->computeCount();
$this->data['messages'] = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages());
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
}
/**
* @return array
*/
public function getMessages()
{
return isset($this->data['messages']) ? $this->data['messages'] : array();
}
/**
* @return int
*/
public function getCountMissings()
{
return isset($this->data[DataCollectorTranslator::MESSAGE_MISSING]) ? $this->data[DataCollectorTranslator::MESSAGE_MISSING] : 0;
}
/**
* @return int
*/
public function getCountFallbacks()
{
return isset($this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK]) ? $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] : 0;
}
/**
* @return int
*/
public function getCountDefines()
{
return isset($this->data[DataCollectorTranslator::MESSAGE_DEFINED]) ? $this->data[DataCollectorTranslator::MESSAGE_DEFINED] : 0;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'translation';
}
private function sanitizeCollectedMessages($messages)
{
foreach ($messages as $key => $message) {
$messages[$key]['translation'] = $this->sanitizeString($messages[$key]['translation']);
}
return $messages;
}
private function computeCount()
{
$count = array(
DataCollectorTranslator::MESSAGE_DEFINED => 0,
DataCollectorTranslator::MESSAGE_MISSING => 0,
DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0,
);
foreach ($this->translator->getCollectedMessages() as $message) {
++$count[$message['state']];
}
return $count;
}
private function sanitizeString($string, $length = 80)
{
$string = trim(preg_replace('/\s+/', ' ', $string));
if (function_exists('mb_strlen') && false !== $encoding = mb_detect_encoding($string)) {
if (mb_strlen($string, $encoding) > $length) {
return mb_substr($string, 0, $length - 3, $encoding).'...';
}
} elseif (strlen($string) > $length) {
return substr($string, 0, $length - 3).'...';
}
return $string;
}
}

View File

@ -0,0 +1,153 @@
<?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\Component\Translation;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface
{
const MESSAGE_DEFINED = 0;
const MESSAGE_MISSING = 1;
const MESSAGE_EQUALS_FALLBACK = 2;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var array
*/
private $messages = array();
/**
* @param Translator $translator
*/
public function __construct(TranslatorInterface $translator)
{
if (!($translator instanceof TranslatorInterface && $translator instanceof TranslatorBagInterface)) {
throw new \InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', get_class($translator)));
}
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
$trans = $this->translator->trans($id, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans);
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->collectMessage($locale, $domain, $id, $trans);
return $trans;
}
/**
* {@inheritdoc}
*
* @api
*/
public function setLocale($locale)
{
$this->translator->setLocale($locale);
}
/**
* {@inheritdoc}
*
* @api
*/
public function getLocale()
{
return $this->translator->getLocale();
}
/**
* {@inheritdoc}
*/
public function getCatalogue($locale = null)
{
return $this->translator->getCatalogue($locale);
}
/**
* Passes through all unknown calls onto the translator object.
*/
public function __call($method, $args)
{
return call_user_func_array(array($this->translator, $method), $args);
}
/**
* @return array
*/
public function getCollectedMessages()
{
return $this->messages;
}
/**
* @param string|null $locale
* @param string|null $domain
* @param string $id
* @param string $trans
*/
private function collectMessage($locale, $domain, $id, $translation)
{
if (null === $locale) {
$locale = $this->getLocale();
}
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
$catalogue = $this->translator->getCatalogue($locale);
if ($catalogue->defines($id, $domain)) {
$state = self::MESSAGE_DEFINED;
} elseif ($catalogue->has($id, $domain)) {
$state = self::MESSAGE_EQUALS_FALLBACK;
$fallbackCatalogue = $catalogue->getFallBackCatalogue();
while ($fallbackCatalogue) {
if ($fallbackCatalogue->defines($id, $domain)) {
$locale = $fallbackCatalogue->getLocale();
break;
}
}
} else {
$state = self::MESSAGE_MISSING;
}
$this->messages[] = array(
'locale' => $locale,
'domain' => $domain,
'id' => $id,
'translation' => $translation,
'state' => $state,
);
}
}

View File

@ -35,7 +35,7 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface
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)));
throw new \InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', get_class($translator)));
}
$this->translator = $translator;

View File

@ -0,0 +1,72 @@
<?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\Component\Translation\Tests;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\DataCollectorTranslator;
use Symfony\Component\Translation\Loader\ArrayLoader;
class DataCollectorTranslatorTest extends \PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (!class_exists('Symfony\Component\HttpKernel\DataCollector\DataCollector')) {
$this->markTestSkipped('The "DataCollector" is not available');
}
}
public function testCollectMessages()
{
$collector = $this->createCollector();
$collector->setFallbackLocales(array('fr'));
$collector->trans('foo');
$collector->trans('bar');
$collector->transChoice('choice', 0);
$expectedMessages = array();
$expectedMessages[] = array(
'id' => 'foo',
'translation' => 'foo (en)',
'locale' => 'en',
'domain' => 'messages',
'state' => DataCollectorTranslator::MESSAGE_DEFINED,
);
$expectedMessages[] = array(
'id' => 'bar',
'translation' => 'bar (fr)',
'locale' => 'fr',
'domain' => 'messages',
'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK,
);
$expectedMessages[] = array(
'id' => 'choice',
'translation' => 'choice',
'locale' => 'en',
'domain' => 'messages',
'state' => DataCollectorTranslator::MESSAGE_MISSING,
);
$this->assertEquals($expectedMessages, $collector->getCollectedMessages());
}
private function createCollector()
{
$translator = new Translator('en');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array('foo' => 'foo (en)'), 'en');
$translator->addResource('array', array('bar' => 'bar (fr)'), 'fr');
$collector = new DataCollectorTranslator($translator);
return $collector;
}
}