L’une des meilleures façons pour qu’une application ait une audience plus large est de gérer plusieurs langues. Cela peut souvent se révéler être une tâche gigantesque, mais les fonctionnalités d’internationalisation et de localisation dans CakePHP rendront cela plus facile.
D’abord il est important de comprendre quelques terminologies. Internationalisation se réfère à la possibilité qu’a une application d’être localisée. Le terme localisation se réfère à l’adaptation qu’a une application de répondre aux besoins d’une langue (ou culture) spécifique (par ex: un « locale »). L’internationalisation et la localisation sont souvent abrégées en respectivement i18n et l10n; 18 et 10 correspondent au nombre de caractères entre le premier et le dernier caractère respectivement pour internationalisation et localisation.
Il n’y a que quelques étapes à franchir pour passer d’une application
mono-langue à une application multi-langue, la première est d’utiliser la
fonction __()
dans votre code. Ci-dessous un exemple d’un code pour
une application mono-langue:
<h2>Popular Articles</h2>
Pour internationaliser votre code, la seule chose à faire est d’entourer
la chaîne avec __()
comme ceci:
<h2><?= __('Popular Articles') ?></h2>
Si vous ne faîtes rien de plus, ces deux bouts de codes donneront un résultat
identique - ils renverront le même contenu au navigateur.
La fonction __()
traduira la chaîne passée si une traduction est
disponible, sinon elle la renverra non modifiée.
Les traductions peuvent être mises à disposition en utilisant des fichiers de langue stockés dans votre application. Le format par défaut pour ces fichiers est le format Gettext. Ces fichiers doivent être placés dans src/Locale/ et dans ce répertoire, il devrait y avoir un sous-dossier par langue que l’application doit prendre en charge:
/src
/Locale
/en_US
default.po
/en_GB
default.po
validation.po
/es
default.po
Le domaine par défaut est “default”, votre dossier locale
devrait donc
contenir au minimum le fichier default.po
(cf. ci-dessus). Un domaine se
réfère à un regroupement arbitraire de messages de traduction. Si aucun groupe
n’est utilisé, le groupe par défaut est sélectionné.
Les messages du coeur extraits de la libairie CakePHP peuvent être stockés séparément dans un fichier cake.po dans src/Locale/. La librairie localized de CakePHP contient des traductions des chaînes de caractère du coeur (du domaine cake) pour l’interface client. Pour utiliser ces fichiers, liez les ou copiez les au bon endroit : src/Locale/<locale>/cake.po. Si votre locale est incomplète ou incorrecte, vous pouvez nous envoyer une PR dans ce dépôt pour corriger les erreurs.
Les plugins peuvent également contenir des fichiers de traduction, la convention
est d’utiliser la version under_scored
du nom du plugin comme domaine de la
traduction des messages:
MyPlugin
/src
/Locale
/fr
my_plugin.po
/de
my_plugin.po
Les dossiers de traduction peuvent être composées d’un code à deux ou trois lettres ISO
de la langue ou du nom de la locale ICU, par exemple fr_FR
, es_AR
,
da_DK
, qui contient en même temps la langue et le pays où elle est parlée.
La liste complète est disponible sur https://www.localeplanet.com/icu/.
Un fichier de traduction pourrait ressembler à ceci:
msgid "My name is {0}"
msgstr "Je m'appelle {0}"
msgid "I'm {0,number} years old"
msgstr "J'ai {0,number} ans"
Note
Les traductions sont mises en cache - Assurez-vous de toujours vider le cache après
avoir apporté des modifications aux traductions! Vous pouvez soit utiliser
l’outil cache et exécuter par exemple
bin/cake cache clear _cake_core_
, soit vider manuellement le dossier
tmp/cache/persistent
(si vous utilisez une mise en cache basée sur des fichiers).
Pour créer les fichiers pot à partir de __() et des autres types de messages internationalisés qui se trouvent dans votre code, vous pouvez utiliser le shell i18n. Vous pouvez consulter le chapitre suivant pour en savoir plus.
La locale
par défaut se détermine dans le fichier config/app.php en
définissant App.defaultLocale
:
'App' => [
...
'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
...
]
Cela permet de contrôler plusieurs aspects de votre application, incluant la langue de traduction par défaut, le format des dates, des nombres, et devises à chaque fois qu’un de ces éléments s’affiche, en utilisant les bibliothèques de localisation fournies par CakePHP.
Pour changer la langue des chaines de caractères traduites, vous pouvez appeler cette méthode:
use Cake\I18n\I18n;
// Avant 3.5, utilisez I18n::locale()
I18n::setLocale('de_DE');
Cela changera également le formatage des nombres et des dates lorsque vous utilisez les outils de localisation.
CakePHP fournit plusieurs fonctions qui vous aideront à internationaliser votre
application. La plus fréquemment utilisée est __()
. Cette fonction
est utilisée pour récupérer un message de traduction simple ou retourner la
même chaine si aucune traduction n’est trouvée:
echo __('Popular Articles');
Si vous avez besoin de grouper vos messages, par exemple des traduction à
l’intérieur d’un plugin, vous pouvez utiliser la fonction __d()
pour récupérer les messages d’un autre domaine:
echo __d('my_plugin', 'Trending right now');
Note
Si vous souhaitez traduire vos plugins et qu’ils ont un « préfixe » de
namespace, vous devez nommer votre chaîne de domaine Namespace/PluginName
.
Cependant, notez que le chemin du fichier de langage sera, dans votre dossier de
plugin, plugins/Namespace/PluginName/src/Locale/plugin_name.po
.
Parfois les chaines de traduction peuvent être ambigües pour les personnes
les traduisant. Cela se produit lorsque deux chaines sont identiques mais
se réfèrent à des choses différentes. Par exemple “lettre” a plusieurs
significations en français. Pour résoudre ce problème, vous pouvez utiliser
la fonction __x()
:
echo __x('communication écrite', 'He read the first letter');
echo __x('apprentissage de l alphabet', 'He read the first letter');
Le premier argument est le contexte du message et le second est le message à traduire.
msgctxt "communication écrite"
msgid "He read the first letter"
msgstr "Il a lu le premier courrier"
Les fonctions de traduction vous permettent d’interpoler des variables dans les messages en utilisant des marqueurs définis dans le message lui-même ou dans la chaine traduite:
echo __("Hello, my name is {0}, I'm {1} years old", ['Sara', 12]);
Les marqueurs sont numériques et correspondent aux clés dans le tableau passé. Vous pouvez également passer à la fonction les variables en tant qu’arguments indépendants:
echo __("Small step for {0}, Big leap for {1}", 'Man', 'Humanity');
Toutes les fonctions de traduction intègrent le remplacement de placeholder:
__d('validation', 'The field {0} cannot be left empty', 'Name');
__x('alphabet', 'He read the letter {0}', 'Z');
le caractère '
(guillemet simple ou apostrophe) agit comme un caractère
d’échappement dans les messages de traduction. Chaque variable entourée de
guillemets simples ne sera pas remplacée et sera traitée en tant que texte
littéral. Par exemple:
__("This variable '{0}' be replaced.", 'will not');
En utilisant deux guillemets simples côte à côte, vos variables seront remplacées correctement:
__("This variable ''{0}'' be replaced.", 'will');
Ces fonctions profitent des avantages du MessageFormatter ICU pour que vous puissiez traduire des messages, des dates, des nombres et des devises en même temps:
echo __(
'Hi {0}, your balance on the {1,date} is {2,number,currency}',
['Charles', new FrozenTime('2014-01-13 11:12:00'), 1354.37]
);
// Retourne
Hi Charles, your balance on the Jan 13, 2014, 11:12 AM is $ 1,354.37
Les nombres dans les placeholders peuvent également être formatés avec un contrôle fin et précis sur la sortie:
echo __(
'You have traveled {0,number} kilometers in {1,number,integer} weeks',
[5423.344, 5.1]
);
// Retourne
You have traveled 5,423.34 kilometers in 5 weeks
echo __('There are {0,number,#,###} people on earth', 6.1 * pow(10, 9));
// Retourne
There are 6,100,000,000 people on earth
Voici la liste des balises spécifiques que vous pouvez mettre après le mot
number
:
integer
: Supprime la partie décimale
currency
: Ajoute le symbole de la devise locale et arrondit les décimales
percent
: Formate le nombre en pourcentage
Les dates peuvent également être formatées en utilisant le mot date
après le
nombre placeholder. Les options supplémentaires sont les suivantes:
short
medium
long
full
Le mot time
après le nombre placeholder est également accepté et il comprend
les mêmes options que date
.
Note
Les placeholders nommés sont supportés dans PHP 5.5+ et sont formatés comme
{name}
. Quand vous utilisez les placeholders nommés, passez les
variables dans un tableau en utilisant la paire de clé/valeur, par exemple
['name' => 'Sara', 'age' => 12]
.
Il est recommandé d’utiliser PHP 5.5 ou supérieur quand vous utilisez les
fonctionnalités d’internationalisation de CakePHP. L’extension php5-intl
doit être installée et la version ICU doit être supérieur à 48.x.y (pour
vérifier la version ICU Intl::getIcuVersion()
).
Une partie cruciale de l’internationalisation de votre application est de récupérer vos messages pluralisés correctement suivant les langues affichées. CakePHP fournit plusieurs possibilités pour sélectionner correctement les pluriels dans vos messages.
La première tire parti du format de message ICU
qui est fourni par défaut
dans les fonctions de traductions. Dans les fichiers de traduction vous
pourriez avoir les chaines suivantes
msgid "{0,plural,=0{No records found} =1{Found 1 record} other{Found # records}}"
msgstr "{0,plural,=0{Ningún resultado} =1{1 resultado} other{# resultados}}"
msgid "{placeholder,plural,=0{No records found} =1{Found 1 record} other{Found {1} records}}"
msgstr "{placeholder,plural,=0{Ningún resultado} =1{1 resultado} other{{1} resultados}}"
Et dans votre application utilisez le code suivant pour afficher l’une des traductions pour une telle chaine:
__('{0,plural,=0{No records found }=1{Found 1 record} other{Found # records}}', [0]);
// Retourne "Ningún resultado" puisque l'argument {0} est 0
__('{0,plural,=0{No records found} =1{Found 1 record} other{Found # records}}', [1]);
// Retourne "1 resultado" puisque l'argument {0} est 1
__('{placeholder,plural,=0{No records found} =1{Found 1 record} other{Found {1} records}}', [0, 'many', 'placeholder' => 2])
// Retourne "many resultados" puisque l'argument {placeholder} est 2 et
// l'argument {1} est 'many'
Regarder de plus près le format que nous avons juste utilisé, rendra évident la méthode de construction des messages:
{ [count placeholder],plural, case1{message} case2{message} case3{...} ... }
Le [count placeholder]
peut être le numéro de clé du tableau de n’importe
quelle variable passée à la fonction de traduction. Il sera utilisé pour
sélectionner la forme plurielle correcte.
Noter que pour faire référence à [count placeholder]
dans {message}
vous devez utiliser #
.
Vous pouvez bien entendu utiliser des id de messages plus simples si vous ne voulez pas taper la séquence plurielle complète dans votre code.
msgid "search.results"
msgstr "{0,plural,=0{Ningún resultado} =1{1 resultado} other{{1} resultados}}"
Ensuite utilisez la nouvelle chaine dans votre code:
__('search.results', [2, 2]);
// Retourne "2 resultados"
la dernière version a l’inconvénient que vous aurez besoin d’avoir un fichier de message de traduction même pour la langue par défaut, mais comporte l’avantage de rendre le code plus lisible et de laisser les chaines de sélection de plurielles compliquées dans les fichiers de traduction.
Parfois utiliser directement la correspondance des nombres vers les pluriels est impossible. Par exemple les langues telles que l’Arabe nécessitent un pluriel différent lorsque vous faites référence à une faible quantité et un pluriel différent pour une quantité plus importante. Dans ces cas vous pouvez utiliser la correspondance d’alias ICU. Au lieu d’écrire:
=0{No results} =1{...} other{...}
Vous pouvez faire:
zero{No Results} one{One result} few{...} many{...} other{...}
Assurez-vous de lire le Guide des Règles Plurielles des Langues pour obtenir une vue d’ensemble complète des alias que vous pouvez utiliser pour chaque langue.
Le second format de sélection plurielle accepté est d’utiliser les
fonctionnalités intégrées de Gettext. Dans ce cas, les pluriels seront
enregistrés dans le fichier .po
en créant une ligne de traduction séparée
pour chaque forme plurielle:
# Un identificateur de message pour le singulier
msgid "One file removed"
# Une autre pour le pluriel
msgid_plural "{0} files removed"
# Traduction au singulier
msgstr[0] "Un fichero eliminado"
# Traduction au pluriel
msgstr[1] "{0} ficheros eliminados"
Lorsque vous utilisez cet autre format, vous devez utiliser une autre fonction de traduction:
// Retourne: "10 ficheros eliminados"
$count = 10;
__n('One file removed', '{0} files removed', $count, $count);
// Il est également possible de l'utiliser dans un domaine
__dn('my_plugin', 'One file removed', '{0} files removed', $count, $count);
Le nombre à l’intérieur de msgstr[]
est le nombre assigné par Gettext pour
la forme plurielle de la langue. Certaines langues ont plus de deux formes
plurielles, le Croate par exemple:
msgid "One file removed"
msgid_plural "{0} files removed"
msgstr[0] "{0} datoteka je uklonjena"
msgstr[1] "{0} datoteke su uklonjene"
msgstr[2] "{0} datoteka je uklonjeno"
Merci de visiter la page des langues Launchpad pour une explication détaillée sur les nombres de formes plurielles de chaque langue.
Si vous devez vous écarter des conventions de CakePHP en ce qui concerne l’emplacement et la manière d’enregistrer les messages de traduction, vous pouvez créer votre propre loader de messages traduits. La manière la plus simple de créer votre propre traducteur est de définir un loader pour un seul domaine et une seule locale:
use Aura\Intl\Package;
I18n::setTranslator('animals', function () {
$package = new Package(
'default', // The formatting strategy (ICU)
'default' // The fallback domain
);
$package->setMessages([
'Dog' => 'Chien',
'Cat' => 'Chat',
'Bird' => 'Oiseau'
...
]);
return $package;
}, 'fr_FR');
Le code ci-dessus peut être ajouté à votre config/bootstrap.php pour
que les traductions soient ajoutées avant qu’une fonction de traduction ne soit
utilisée. Le minimum absolu nécessaire pour créer un traducteur est que la
fonction loader doit retourner un objet Aura\Intl\Package
. Une fois que le
code est en place vous pouvez utiliser les fonctions de traduction comme
d’habitude:
// Avant 3.5, utilisez I18n::locale()
I18n::setLocale('fr_FR');
__d('animals', 'Dog'); // Retourne "Chien"
Comme vous pouvez le voir, les objets Package
prennent les messages de
traduction sous forme de tableau. Vous pouvez passer la méthode
setMessages()
de la manière qui vous plait: avec du code en ligne, en
incluant un autre fichier, en appelant une autre fonction, etc. CakePHP fournit
quelques fonctions de loader que vous pouvez réutiliser si vous devez juste
changer l’endroit où sont chargés les messages.
Par exemple, vous pouvez toujours utiliser les fichiers .po
mais les charger
depuis un autre endroit:
use Cake\I18n\MessagesFileLoader as Loader;
// Charge les messages depuis src/Locale/folder/sub_folder/filename.po
I18n::setTranslator(
'animals',
new Loader('filename', 'folder/sub_folder', 'po'),
'fr_FR'
);
Il est possible de continuer à utiliser les mêmes conventions que CakePHP
utilise mais d’utiliser un autre parser de messages que PoFileParser
.
Par exemple, si vous vouliez charger les messages de traduction en utilisant
YAML
, vous auriez d’abord besoin de créer la classe du parser:
namespace App\I18n\Parser;
class YamlFileParser
{
public function parse($file)
{
return yaml_parse_file($file);
}
}
Le fichier doit être créé dans le dossier src/I18n/Parser de votre application. Ensuite, créez les fichiers de traduction sous src/Locale/fr_FR/animals.yaml
Dog: Chien
Cat: Chat
Bird: Oiseau
Enfin, configurez le loader de traduction pour le domaine et la locale:
use Cake\I18n\MessagesFileLoader as Loader;
// Avant 3.5, utilisez translator()
I18n::setTranslator(
'animals',
new Loader('animals', 'fr_FR', 'yaml'),
'fr_FR'
);
Configurer des traducteurs en appelant I18n::setTranslator()
pour chaque
domaine et locale que vous devez supporter peut être fastidieux, spécialement
si vous devez supporter plus que quelques locales. Pour éviter ce problème,
CakePHP vous permet de définir des loaders de traduction génériques pour chaque
domaine.
Imaginez que vous vouliez charger toutes les traductions pour le domaine par défaut et pour chaque langue depuis un service externe:
use Aura\Intl\Package;
I18n::config('default', function ($domain, $locale) {
$locale = Locale::parseLocale($locale);
$language = $locale['language'];
$messages = file_get_contents("http://example.com/translations/$lang.json");
return new Package(
'default', // Formatter
null, // Fallback (none for default domain)
json_decode($messages, true)
)
});
Le code ci-dessus appelle un service externe exemple pour charger un fichier
JSON avec les traductions puis construit uniquement un objet Package
pour chaque locale nécessaire dans l’application.
If you’d like to change how packages are loaded for all packages, that don’t
have specific loaders set you can replace the fallback package loader by using
the _fallback
package:
I18n::config('_fallback', function ($domain, $locale) {
// Custom code that yields a package here.
});
les tableaux utilisées pour setMessages()
peuvent être conçus pour ordonner
au traducteur d’enregistrer les messages sous différents domaines ou de
déclencher une sélection de pluriel de style Gettext. Ce qui suit est un exemple
d’enregistrement de traductions pour la même clé dans différents contextes:
[
'He reads the letter {0}' => [
'alphabet' => 'Él lee la letra {0}',
'written communication' => 'Él lee la carta {0}'
]
]
De même vous pouvez exprimer des pluriels de style Gettext en utilisant le tableau de messages avec une clé de tableau imbriqué par forme plurielle:
[
'I have read one book' => 'He leído un libro',
'I have read {0} books' => [
'He leído un libro',
'He leído {0} libros'
]
]
Dans les exemples précédents nous avons vu que les Packages sont construits en
utilisant default
en premier argument, et il était indiqué avec un
commentaire qu’il correspondait au formateur à utiliser.
Les formateurs sont des classes responsables d’interpoler les variables dans
les messages de traduction et sélectionner la forme plurielle correcte.
Si vous avez à faire une application datée, ou que vous n’avez pas besoin de la
puissance offerte par le formateur de message ICU, CakePHP fournit également le
formateur sprintf
:
return Package('sprintf', 'fallback_domain', $messages);
Les messages à traduire seront passés à la fonction sprintf()
pour
interpoler les variables:
__('Hello, my name is %s and I am %d years old', 'José', 29);
Il est possible de définir le formateur par défaut pour tous les traducteurs
créés par CakePHP avant qu’ils soient utilisés pour la première fois. Cela
n’inclut pas les traducteurs créés manuellement en utilisant les méthodes
setTranslator()
et config()
:
I18n::defaultFormatter('sprintf');
Lorsque vous affichez des dates et des nombres dans votre application, vous voudrez souvent qu’elles soient formatées conformément au format du pays ou de la région dans lequel vous souhaitez afficher la page.
Pour changer l’affichage des dates et des nombres, vous devez uniquement changer la locale et utiliser les bonnes classes:
use Cake\I18n\I18n;
use Cake\I18n\Time;
use Cake\I18n\Number;
// Avant 3.5, utilisez I18n::locale()
I18n::setLocale('fr-FR');
$date = new Time('2015-04-05 23:00:00');
echo $date; // Affiche 05/04/2015 23:00
echo Number::format(524.23); // Displays 524,23
Assurez vous de lire les sections Date & Time et Number pour en apprendre plus sur les options de formatage.
Par défaut, les dates renvoyées par l’ORM utilisent la classe
Cake\I18n\Time
, donc leur affichage direct dans votre application sera
affecté par le changement de la locale.
Quand vous acceptez les données localisées, c’est sympa d’accepter les informations de type datetime dans un format localisé pour l’utilisateur. Dans un controller, ou Middleware, vous pouvez configurer les types Date, Time, et DateTime pour parser les formats localisés:
use Cake\Database\TypeFactory;
// Permet de parser avec le format de locale par défaut.
TypeFactory::build('datetime')->useLocaleParser();
// Configure un parser personnalisé du format de datetime.
TypeFactory::build('datetime')->useLocaleParser()->setLocaleFormat('dd-M-y');
// Vous pouvez aussi utiliser les constantes IntlDateFormatter.
TypeFactory::build('datetime')->useLocaleParser()
->setLocaleFormat([IntlDateFormatter::SHORT, -1]);
Le parsing du format par défaut est le même que le format de chaîne par défaut.
Quand vous gérez des données d’utilisateurs situés dans différents fuseaux
horaires, vous avez besoin de convertir ces datetimes de la requête vers le
fuseau horaire de votre application. Vous pouvez utiliser setUserTimezone()
depuis un controller ou Middleware pour faire cela plus
simplement:
// Définir le fuseau horaire de l'utilisateur
TypeFactory::build('datetime')->setUserTimezone($user->timezone);
Une fois que c’est fait, quand votre application crée ou met à jour des entities
à partir de la requête, l’ORM va convertir automatiquement les valeurs datetime
du fuseau de l’utilisateur vers le fuseau de votre application. Cela garantit
que l’application travaillera toujours dans le fuseau horaire défini dans
App.defaultTimezone
.
Si votre application gère des données datetime dans de nombreuses actions, vous pouvez utiliser un middleware pour définir à la fois la conversion du décalage horaire et le format localisé:
namespace App\Middleware;
use Cake\Database\TypeFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class DatetimeMiddleare implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// Obtenir l'utilisateur depuis la requête.
// Cet exemple suppose que votre entity utilisteur a un attribut timezone.
$user = $request->getAttribute('identity');
if ($user) {
TypeFactory::build('datetime')
->useLocaleParser()
->setUserTimezone($user->timezone);
}
return $handler->handle($request);
}
}
En utilisant le LocaleSelectorFilter
dans votre application, CakePHP
définira automatiquement la locale en se basant sur l’utilisateur actuel:
// dans src/Application.php
use Cake\I18n\Middleware\LocaleSelectorMiddleware;
// Mise à jour de la méthode "middleware" pour ajouter le nouveau middleware
public function middleware($middleware)
{
// Add middleware and set the valid locales
$middleware->add(new LocaleSelectorMiddleware(['en_US', 'fr_FR']));
}
// Avant 3.3.0, il faut utiliser un DispatchFilter
// dans config/bootstrap.php
DispatcherFactory::add('LocaleSelector');
// Limite les locales à en_US et fr_FR uniquement
DispatcherFactory::add('LocaleSelector', ['locales' => ['en_US', 'fr_FR']]);
Le LocaleSelectorFilter
utilisera l’entête Accept-Language
pour définir
automatiquement la locale préférée de l’utilisateur. Vous pouvez utiliser
l’option de liste de locale pour limiter les locales pouvant être utilisées
automatiquement.
Si vous voulez traduire du contenu ou des entities, vous pouvez consulter le Behavior Translate.