Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Adding a checkbox on the translation panel + custom save controller
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ProfilerController extends Controller
{
/**
* Save the selected translation to resources.
*
* @Route("/{token}/translation/save", name="_profiler_save_translations")
*
* @return Response A Response instance
*/
public function saveAction(Request $request, $token)
{
if (!$request->isXmlHttpRequest()) {
return $this->redirectToRoute('_profiler', ['token' => $token]);
}
$profiler = $this->get('profiler');
$profiler->disable();
$selected = $request->request->get('selected');
if (!$selected || count($selected) == 0) {
return new Response('No key selected.');
}
$profile = $profiler->loadProfile($token);
$all = $profile->getCollector('translation');
$toSave = array_intersect_key($all->getMessages(), array_flip($selected));
// @todo Save the $toSave messages!
// I'm using a custim Loco service, doing API calls for each message.
if (true) {
return new Response(sprintf("%s translation keys saved!", count($selected)));
} else {
return new Response("Can't save the translations.");
}
}
}
# Add
_profiler_custom:
resource: "@AppBundle/Controller/ProfilerController.php"
type: annotation
prefix: /_profiler
services:
# Custom data_collector to use our own template
app.data_collector.translation:
class: 'Symfony\Component\Translation\DataCollector\TranslationDataCollector'
arguments: ['@translator.data_collector']
tags:
- { name: 'data_collector', template: "profiler/translation.html.twig", id: "translation", priority: 200 }
{% 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>Translation Stats</h2>
<table>
<tbody>
<tr>
<th>Defined messages</th>
<td><pre>{{ collector.countdefines }}</pre></td>
</tr>
<tr>
<th scope="col" style="width: 30%">Fallback messages</th>
<td scope="col" style="width: 60%"><pre>{{ collector.countFallbacks }}</pre></td>
</tr>
<tr>
<th>Missing messages</th>
<td><pre>{{ collector.countMissings }}</pre></td>
</tr>
</tbody>
</table>
<form action="{{ path('_profiler_save_translations', {'token': token}) }}" method="post"
id="translations-list" onsubmit="javascript:saveTranslations(this);return false;" >
<table>
<tr>
<th>Select</th>
<th>State</th>
<th>Locale</th>
<th>Domain</th>
<th>Id</th>
<th>Message Preview</th>
</tr>
{% for key, message in collector.messages %}
<tr>
<td>
{% if message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_MISSING') %}
<input type="checkbox" name="translationKey" value="{{ key }}">
{% else %}
<input type="checkbox" disabled>
{% endif %}
</td>
<td><code>{{ translator.state(message) }}</code></td>
<td><code>{{ message.locale }}</code></td>
<td><code>{{ message.domain }}</code></td>
<td>
<code>{{ message.id }}</code>
{% if message.count > 1 %}<br><small style="color: gray;">(used {{ message.count }} times)</small>{% endif %}
{% if message.transChoiceNumber is not null %}<br><small style="color: gray;">(use pluralization)</small>{% endif %}
<div>
[<a href="#" onclick="return openParameters(this);" style="text-decoration: none;"
title="Toggle parameters display" data-target-id="parameters-{{ loop.index }}" >
<img alt="+" src="data:image/gif;base64,R0lGODlhEgASAMQTANft99/v+Ga44bHb8ITG52S44dXs9+z1+uPx+YvK6WC24G+944/M6W28443L6dnu+Ge54v/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABMALAAAAAASABIAQAVS4DQBTiOd6LkwgJgeUSzHSDoNaZ4PU6FLgYBA5/vFID/DbylRGiNIZu74I0h1hNsVxbNuUV4d9SsZM2EzWe1qThVzwWFOAFCQFa1RQq6DJB4iIQA7" style="display: inline; width: 12px; height: 12px;" />
<img alt="-" src="data:image/gif;base64,R0lGODlhEgASAMQSANft94TG57Hb8GS44ez1+mC24IvK6ePx+Wa44dXs92+942e54o3L6W2844/M6dnu+P/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAASABIAQAVCoCQBTBOd6Kk4gJhGBCTPxysJb44K0qD/ER/wlxjmisZkMqBEBW5NHrMZmVKvv9hMVsO+hE0EoNAstEYGxG9heIhCADs=" style="display: none; width: 12px; height: 12px;" />
<span style="vertical-align:top">Parameters</span>
</a>]
<div id="parameters-{{ loop.index }}" style="display: none;">
{% for parameters in message.parameters %}
{% if parameters|length > 0 %}
{{ dump(parameters) }}
{% else %}
{{ dump(null) }}
{% endif %}
{% endfor %}
</div>
</div>
</td>
<td><code>{{ message.translation }}</code></td>
</tr>
{% endfor %}
</table>
<div id="translationResult">
<button type="submit" class="sf-button">
<span class="border-l">
<span class="border-r">
<span class="btn-bg">Add selected translations to catalogs</span>
</span>
</span>
</button>
</div>
</form>
<script type="text/javascript">
function openParameters(link) {
"use strict";
var imgs = link.children,
target = link.getAttribute('data-target-id');
Sfjs.toggle(target, imgs[0], imgs[1]);
};
var serializeQueryString = function(obj, prefix) {
var str = [];
for(var p in obj) {
if (obj.hasOwnProperty(p)) {
var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p];
str.push(typeof v == "object" ? serializeQueryString(v, k) : encodeURIComponent(k) + "=" + encodeURIComponent(v));
}
}
return str.join("&");
};
// We need to hack a bit Sfjs.request because it does not support POST requests
// May not work for ActiveXObject('Microsoft.XMLHTTP'); :(
(function(open) {
XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
open.call(this, method, url, async, user, pass);
if (method.toLowerCase() === 'post') {
this.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
}
};
})(XMLHttpRequest.prototype.open);
var saveTranslations = function(form) {
"use strict";
var inputs = form.translationKey;
var selected = [];
if (!inputs.value) {
for (var val in inputs) {
if (inputs.hasOwnProperty(val) && inputs[val].value) {
if (inputs[val].checked) {
selected.push(inputs[val].value);
}
}
}
} else if (inputs.checked) {
selected.push(inputs.value);
}
Sfjs.request(
form.action,
function(xhr) {
// Success
document.getElementById('translationResult').innerHTML = xhr.responseText;
},
function(xhr) {
// Error
document.getElementById('translationResult').innerHTML = xhr.responseText;
},
serializeQueryString({selected: selected}),
{ method: 'POST' }
);
return false;
};
</script>
{% 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 %}
@phihos

This comment has been minimized.

Copy link

@phihos phihos commented Jan 25, 2017

For those who just want to update the corresponding translation files in app/Resources/translations:

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Yaml\Yaml;

class ProfilerController extends Controller
{
    /**
     * Save the selected translation to resources.
     *
     * @Route("/{token}/translation/save", name="_profiler_save_translations")
     *
     * @return Response A Response instance
     */
    public function saveAction(Request $request, $token)
    {
        if (!$request->isXmlHttpRequest()) {
            return $this->redirectToRoute('_profiler', ['token' => $token]);
        }
        $profiler = $this->get('profiler');
        $profiler->disable();
        $selected = $request->request->get('selected');
        if (!$selected || count($selected) == 0) {
            return new Response('No key selected.');
        }
        $profile = $profiler->loadProfile($token);
        $all = $profile->getCollector('translation');
        $toSave = array_intersect_key($all->getMessages(), array_flip($selected));

        // open message files, add the new ids and save
        $baseTransPath = $this->container->get('kernel')->getRootDir() . DIRECTORY_SEPARATOR . 'Resources' .
            DIRECTORY_SEPARATOR . 'translations';
        foreach ($toSave as $translation) {
            $transFilePath = $baseTransPath . DIRECTORY_SEPARATOR . $translation['domain'] . '.' . $translation['locale'] . '.yml';
            if (!is_file($transFilePath)) {
                return new Response("Can't save the translations. No such file $transFilePath");
            }
            $yaml = Yaml::parse(file_get_contents($transFilePath));
            if (!array_key_exists($translation['id'], $yaml)) {
                $yaml[$translation['id']] = 'translate_me!';
            }
            ksort($yaml);   // sort for alphabetic order of translation ids
            file_put_contents($transFilePath, Yaml::dump($yaml));
        }

        return new Response(sprintf("%s translation keys saved!", count($selected)));
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment