Aujourd’hui nous allons voir comment faire la fameuse recette de l’internationalisation mais on va rajouter quelques ingrédients pour la rendre savoureuse !
Les ingrédients :
1. WordPress avec un thème from scratch et un moteur de templating Twig facilement utilisable avec une lib comme Timber.
2. Un terminal avec gettext de disponible.
3. Des textes à traduire, sous toutes leurs formes (gettext(‘translatable.key’), __(‘translatable.key’)).
4. Le plugin Loco Translate.
Préparation :
Tout d’abord vous prenez votre WordPress nouvellement configuré, avec un beau thème bien intégré et la plupart de vos clés de traductions dans des fichiers .twig et/ou .php
Vous avez développé votre thème et il est prêt à être traduit, générons ensemble les fichiers .pot .po et .mo adequat.
Un fichier POT contient l’ensemble des clés et des messages par défaut de votre internationnalisation. Il sert principalement d’index à la génération des fichiers .po pour chacune des langues. Les fichier .POT et .PO ressemblent à ceci:
msgid "404.title"
msgstr ""
msgid "404.text"
msgstr ""
msgid "404.back"
msgstr ""
Les fichiers .MO sont eux des binaires résultant des opérations de compilation décrite plus loin dans cet article.
1ère solution : Poedit !
Personnellement je la zappe d’entrée de jeu quand elle me demande de payer 30€ pour extraire les traductibles de mes fichiers .twig
2ème solution : Poedit avec un custom parser
J’ai testé cette solution: https://github.com/umpirsky/Twig-Gettext-Extractor, malheureusement après bon nombre de manipulations, impossible d’extract les clés depuis mes fichiers .twig. De plus, en y réfléchissant un peu, ça ne me convient pas d’utiliser une application tiers juste pour ça ! Certes sur mon poste ça fonctionne, mais il suffit qu’un nouveau développeur monte sur le projet et on est reparti pour 1h de config de Poedit.
3ème solution : Les plugins existant
Il y aurait un plugin qui pourrait nous sortir de cette galère puisqu’il a également été conçu autour de Timber et donc de Twig: Theme Translation for Polylang Seul problème il ne fonctionne que si vous installez Polylang et nous préférons travailler avec WPML pour nos WordPress multilingue
4ème solution : Le retour aux bases
Bon après tout ce n’est que du gettext, je vais donc le faire moi même !
Et on pétrit le Twig
1er problème -> parser les fichiers .twig pour en extraire les clés, ce n’est pas évident car gettext ne supporte qu’un nombre limité de langages. Je me suis grandement inspiré de cette article pour y arriver. En gros, le principe repose sur l’idée de générer les caches des fichiers .twig qui eux sont en PHP puis de simplement lancer la commande gettext/
La seule réelle difficulté ici est de bien comprendre que votre Twig, sans le contexte approprié, n’est pas capable de résoudre les fonctions custom que vous avez créé ou dans notre cas que Timber a créé pour nous. Il faut donc les faker, cf. le script suivant :
$directory = sprintf('%s/../templates', __DIR__);
$cache = sprintf('%s/../cache', __DIR__);
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory),
RecursiveIteratorIterator::LEAVES_ONLY
);
if ( ! class_exists('Twig_Loader_Filesystem')) {
require_once __DIR__ . '/../../../../../vendor/autoload.php';
}
$loader = new Twig_Loader_Filesystem($directory);
$twig = new Twig_Environment(
$loader,
[
'cache' => $cache,
'auto_reload' => true,
]
);
$twig->addExtension(new Twig_Extensions_Extension_I18n());
/** Fake Functions */
$twig->addFunction(new \Twig_SimpleFunction('__', 'gettext'));
$twig->addFunction(new \Twig_SimpleFunction('TimberImage', function () {
return [
'src' => null
];
}));
$twig->addFunction(new \Twig_SimpleFunction('function', function ($fn) {
return null;
}));
$twig->addFunction(new \Twig_SimpleFunction('fn', function ($fn, $object) {
return null;
}));
/** Fake Filters */
$twig->addFilter(new \Twig_Filter('apply_filters', function ($filter) {
return null;
}));
/** @var SplFileInfo $file */
foreach ($iterator as $file) {
if ($file->isFile()) {
$twig->loadTemplate(str_replace($directory . '/', '', $file));
}
}
Ce script va générer un cache de chaque fichier qui ressemblera à quelque chose comme ça :
/* partials/search.twig */
class __TwigTemplate_fc3e2e6216ecf89c4009766801233e95cff16678418f936c65a26c091170b6c8 extends Twig_Template
{
public function __construct(Twig_Environment $env)
{
parent::__construct($env);
$this->parent = false;
$this->blocks = array();
}
protected function doDisplay(array $context, array $blocks = array())
{
// line 1
echo "<form role=\"search\" method=\"get\" id=\"searchform\" action=\"";
echo twig_escape_filter($this->env, ($context["link"] ?? null), "html", null, true);
echo "\">
<div class=\"trigger\">
<i class=\"icon icon-search\"></i>
<i class=\"icon icon-close\"></i>
</div>
<div class=\"fields\">
<input type=\"text\" value=\"\" name=\"search\" id=\"s\" placeholder=\"";
// line 7
echo twig_escape_filter($this->env, gettext("Search", "timber-foundation"));
echo "\">
<input type=\"submit\" id=\"searchsubmit\" value=\"\" class=\"button postfix\">
</div>
</form>";
}
public function getTemplateName()
{
return "partials/search.twig";
}
public function isTraitable()
{
return false;
}
public function getDebugInfo()
{
return array ( 29 => 7, 19 => 1,);
}
public function getSourceContext()
{
return new Twig_Source("", "partials/search.twig", "/Users/guillaume/Adfab/mytheme/web/app/themes/mytheme/templates/partials/search.twig");
}
}
Vous laissez bien reposer le Twig pendant ce temps on s’occupe du glaçage au gettext
Une fois votre cache généré vous avez fait le plus dur, il reste simplement à taper quelques lignes de commande pour générer les fichiers qui vont bien.
Pour ce faire nous créons un petit script shell qui fera le boulot, tout seul ! Il nous faut donc:
1 – Vider le cache des fichiers .twig
2 – Générer le cache des fichiers .twig
3 – S’assurer que le directory « languages/themes/ » existe sinon on le créer
4 – Extraire les clés de traduction de votre code pour constituer un fichier .pot
5 – Créer ou mettre à jour le fichier .po
6 – Générer un fichier .mo par rapport à votre fichier .po
On veut également générer un fichier .po qui aura cette allure : mytheme-fr_FR.po
Pour faire court, voici le script :
#!/usr/bin/env bash
rm -rf cache/*
php bin/i18n.php
if [ -z "$1" ]; then
echo "need a local"
exit
fi
echo "Creating folder: ../../languages/themes"
mkdir -p ../../languages/themes
find . -iname "*.php" | grep -iv './node_modules' | xargs xgettext --keyword=gettext --keyword=__ --keyword=_ --from-code=UTF-8 -d mytheme -L PHP -c --force-po -F --package-name=mytheme --package-version=1.0 --msgid-bugs-address=guillaume.lacourt@adfab.fr -o ../../languages/themes/mytheme.pot
for folder in $(find ../../languages/themes -maxdepth 1 -type d | awk -F/ '{print $NF}')
do
if [ "${folder}" != ../../languages/themes ]; then
if [[ -f ../../languages/themes/mytheme-$1.po ]]; then
echo "Merging for ${folder}"
msgmerge -U ../../languages/themes/mytheme-$1.po ../../languages/themes/mytheme.pot
else
echo "Initializing for ${folder}"
msginit --locale=${folder} --output-file=../../languages/themes/mytheme-$1.po --input=../../languages/themes/mytheme.pot
fi
echo "Compiling .mo for ${folder}"
msgfmt -c -o ../../languages/themes/mytheme-$1.mo ../../languages/themes/mytheme-$1.po
fi
done
Maintenant depuis votre thème => bin/i18n fr_FR
Ce script est largement perfectible : n’hésitez pas à me remonter vos remarques ou améliorations, idem pour vos questions !
J’ai volontairement passé le plus d’arguments possibles en accord avec la doc.
Par contre dans mon exemple je spécifie un domaine avec l’option « -d mytheme » ce qui implique de reprendre vos occurrences __(‘my.translation’) afin de lui ajouter un domaine : __(‘my.translation’, ‘mytheme’)
Dernier point : vous installez Loco Translate (composer require wpackagist-plugin/loco-translate), puis dans l’interface d’amin vous verrez vos fichiers de trads et vous pourrez tranquillement traduire votre WordPress dans toutes les langues que vous souhaitez
On enfourne !
Comme vous pouvez le constater ci-dessus il ne reste plus qu’a renseigner les traductions. Dans un premier temps nous renseignons les traductions de notre client puis, lorsqu’il souhaite en rajouter, nous ajoutons les traductions statiques dans notre code et au moment du déploiement le script ci dessus les ajoutent en faisant un merge. Pour ce faire nous ne versionons que le fichier .pot qui sera la référence pour toutes les langues et toutes les traductions. Les fichiers .po sont eux ajoutés aux shared du projet via un simple symlink et sont modifiés ou pas au moment du déploiement.
Y a plus qu’a comme dirait l’autre !
Voila, une recette simple et efficace pour bien internationaliser son wordpress.
NB : entre le moment ou cet article a été écrit et maintenant nous avons rencontrés quelques soucis avec l’utilitaire msgfmt qui génère une erreur si le msgid et msgstr ne finissent pas tous les deux par un « \n » (ce qui malheureusement arrivent fréquemment avec Loco Translate). J’ai rajouté donc un petit script PHP qui corrige l’effet pour permettre au script shell de s’exécuter correctement le voici:
<?php
$poFiles = sprintf('%s/../../../languages/themes/*.po', __DIR__);
foreach (glob($poFiles) as $poFile) {
$rows = file($poFile);
for ($key = 0; $key < count($rows); $key++) {
if (substr($rows[$key], 0, 5) === 'msgid' && $key !== 0) {
while (substr($rows[$key], 0, 6) !== 'msgstr') {
$key++;
}
while ( ! empty($rows[$key])) {
$rows[$key] = str_replace('\n', '', $rows[$key]);
$key++;
}
}
}
file_put_contents($poFile, join('', $rows));
}
Dans le script shell vous n’avez plus qu’a rajouter cette ligne php bin/clean.php
avant l’extraction des traductions.
Encore une fois, n’hésitez pas si vous avez des questions ou des remarques !
Bonne dégustation.