PHP 5.3 : intl : Internationalisation et Localisation (partie 1)

10 novembre 2008i18n, l10n, php, php-5.3
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

Les exemples correspondant à ce point se trouvent dans le répertoire "intl".
Et pour des exemples plus simples, jetez un coup d'œil au sous-répertoire "simple-examples" ;-) (c'est d'ailleurs ceux-ci que j'utilise dans cet article, principalement).

Pour éviter que cette article ne soit trop long, j’ai choisi de le découper en deux parties : une que je publie aujourd’hui (lundi 10/11), et l’autre qui sera pour demain, mardi 11 novembre 2008[1].

Sommaire de cette première partie :


Une des problématiques que nous rencontrons de plus en plus fréquemment, alors que PHP s'utilise de plus en plus en entreprise, est la déclinaison de nos applications en plusieurs langues, qui regroupe deux notions :

  • L'internationalisation (<< i18n >>), qui est le fait de concevoir une application de manière à ce qu'elle puisse être déclinée pour plusieurs locales,
  • et la localisation (<< l10n >>), qui est le fait de décliner une application pour une culture -- ce qui a des impacts sur la langue, bien entendu, mais aussi sur les formats de date, les unités monétaires, ...

L'extension intl, qui était auparavant disponible sous forme de paquet PECL, est maintenant intégrée à PHP 5.3 ; elle nous facilitera la tâche pour la mise en place de sites multi-culturels.


IntlDateFormatter

Une des premières difficultés rencontrées lors du développement d'une application multilingue est l'affichage de dates : en France, nous utilisons un format du type << JJ/MM/AAAA >>, mais d'autres pays utilisent, par exemple, << YYYY-MM-DD >>, ou << MM/DD/YYYY >>, ou je ne sais encore quelle combinaison ou quel séparateur.
Les choses ne s'arrangent pas, bien évidemment, lorsque l'on veut écrire une date en toutes lettres !

Pour nous aider, PHP 5.3 introduit une nouvelle classe, conçue spécialement pour faciliter les affichage de dates, quelque soit la locale de votre site ou de vos visiteurs : IntlDateFormatter.

En utilisation dynamique, le principe est simple :

  • On instancie la classe IntDateFormatter, en lui passant en paramètres
    • la locale à utiliser,
    • le format à utiliser pour les dates,
    • et le format à utiliser pour les heures.
  • Et on fait ensuite appel à la méthode format, qui reçoit en paramètre un timestamp.


IntlDateFormatter : Exemple simple

Par exemple, avec la portion de code suivante :

$f = new IntlDateFormatter('fr_FR', IntlDateFormatter::FULL, IntlDateFormatter::FULL);
echo $f->format(time()) . "\n";

Nous obtiendrions la sortie suivante :

mercredi 15 octobre 2008 21:59:02 GMT+00:00

Cette classe peut aussi être utilisée de manière statique, en faisant appel à la méthode create pour obtenir une instance d'objet IntlDateFormatter :

echo IntlDateFormatter::create('fr_FR', IntlDateFormatter::FULL, IntlDateFormatter::FULL)->format(time(time())) . "\n";
echo IntlDateFormatter::create('fr_FR', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT)->format(time(time())) . "\n";

echo IntlDateFormatter::create('zh-Hant-TW', IntlDateFormatter::FULL, IntlDateFormatter::FULL)->format(time(time())) . "\n";
echo IntlDateFormatter::create('zh-Hant-TW', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT)->format(time(time())) . "\n";

echo IntlDateFormatter::create('en_US', IntlDateFormatter::FULL, IntlDateFormatter::FULL)->format(time(time())) . "\n";
echo IntlDateFormatter::create('en_US', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT)->format(time(time())) . "\n";

Et, encore une fois, l'affichage correspondant :

dimanche 9 novembre 2008 23:54:47 GMT+00:00
9 nov. 2008 23:54
2008?11?9???? ??11?54?47? GMT+00:00
2008/11/9 ?? 11:54
Sunday, November 9, 2008 11:54:47 PM GMT+00:00
Nov 9, 2008 11:54 PM

Autrement dit, nous pouvons afficher des dates localisées sans effort, y compris en langues à caractères non latins, uniquement en changeant la locale et les formats de date et d'heure !
N'y-a-t-il pas du progrès dans l'air ?


IntlDateFormatter : Illustrations

Et pour un exemple quelque peu plus complet, plus complexe, mais illustrant pleinement les fonctionnalités de la classe IntlDateFormatter, vous pouvez jeter un coup d'œil aux portions de code qui suivent :

Déclaration des locales avec lesquelles nous travaillerons :

$locales = array('fr_FR', 'zh-Hant-TW', 'en_US');

Même chose pour les formats de dates / heures que nous voulons obtenir en sortie :

$types = array(
    IntlDateFormatter::FULL => 'IntlDateFormatter::FULL',
    IntlDateFormatter::LONG => 'IntlDateFormatter::LONG',
    IntlDateFormatter::MEDIUM => 'IntlDateFormatter::MEDIUM',
    IntlDateFormatter::SHORT => 'IntlDateFormatter::SHORT',
);

Et double-parcours de ces deux listes, pour obtenir l'ensemble des combinaisons possibles :

foreach ($types as $datetypeVal => $datetypeLib) {
    echo "<pre>date: $datetypeLib :\n";
    foreach ($types as $timetypeVal => $timetypeLib) {
        echo "    time: $timetypeLib :\n";
        foreach ($locales as $locale) {
            $df = new IntlDateFormatter($locale, $datetypeVal, $timetypeVal);
            echo "        $locale : " . $df->format(time()) . "\n";
        }
    }
    echo '</pre>';
}

Ce qui nous donne, pour un format de date long, la sortie suivante :

date: IntlDateFormatter::FULL :
    time: IntlDateFormatter::FULL :
        fr_FR : dimanche 9 novembre 2008 23:55:59 GMT+00:00
        zh-Hant-TW : 2008?11?9???? ??11?55?59? GMT+00:00
        en_US : Sunday, November 9, 2008 11:55:59 PM GMT+00:00
    time: IntlDateFormatter::LONG :
        fr_FR : dimanche 9 novembre 2008 23:55:59 GMT+01:00
        zh-Hant-TW : 2008?11?9???? ??11?55?59? GMT+01:00
        en_US : Sunday, November 9, 2008 11:55:59 PM GMT+01:00
    time: IntlDateFormatter::MEDIUM :
        fr_FR : dimanche 9 novembre 2008 23:55:59
        zh-Hant-TW : 2008?11?9???? ?? 11:55:59
        en_US : Sunday, November 9, 2008 11:55:59 PM
    time: IntlDateFormatter::SHORT :
        fr_FR : dimanche 9 novembre 2008 23:55
        zh-Hant-TW : 2008?11?9???? ?? 11:55
        en_US : Sunday, November 9, 2008 11:55 PM

Et son équivalent en format de date court :

date: IntlDateFormatter::SHORT :
    time: IntlDateFormatter::FULL :
        fr_FR : 09/11/08 23:55:59 GMT+00:00
        zh-Hant-TW : 2008/11/9 ??11?55?59? GMT+00:00
        en_US : 11/9/08 11:55:59 PM GMT+00:00
    time: IntlDateFormatter::LONG :
        fr_FR : 09/11/08 23:55:59 GMT+01:00
        zh-Hant-TW : 2008/11/9 ??11?55?59? GMT+01:00
        en_US : 11/9/08 11:55:59 PM GMT+01:00
    time: IntlDateFormatter::MEDIUM :
        fr_FR : 09/11/08 23:55:59
        zh-Hant-TW : 2008/11/9 ?? 11:55:59
        en_US : 11/9/08 11:55:59 PM
    time: IntlDateFormatter::SHORT :
        fr_FR : 09/11/08 23:55
        zh-Hant-TW : 2008/11/9 ?? 11:55
        en_US : 11/9/08 11:55 PM

(Je n'ai pas reproduit toute la sortie du programme ; il vous reste de quoi expérimenter ;-) )


NumberFormatter

PHP 5.3 offre le même type de fonctionnalité pour la gestion de l'affichage de nombres, en utilisant, cette fois-ci, la classe NumberFormatter.
Note : une somme d'argent est un "nombre" -- particulier peut-être, mais un nombre tout de même !

Le fonctionnement de cette classe est strictement identique à celui que nous avons vu pour IntlDateFormatter ; je ne rentrerai donc pas dans les détails.
Passons donc tout de suite à quelques exemples illustrant les possibilités offertes par NumberFormatter :

Formatons un grand nombre, en utilisant une locale française :

echo NumberFormatter::create('fr_FR', NumberFormatter::DECIMAL)->format(123456789.987654) . "\n";
echo NumberFormatter::create('fr_FR', NumberFormatter::CURRENCY)->format(123456789.987654) . "\n";
echo NumberFormatter::create('fr_FR', NumberFormatter::SPELLOUT)->format(123456789.987654) . "\n";

Nous avons demandé trois sorties :

  • décimale, avec séparateurs de milliers et de décimales,
  • monétaire ; pour rappel, la France est passée à l'euro ^^
  • et en toutes lettres.

Ce qui donne :

123 456 789,988
123 456 789,99 €
cent vingt-trois million quatre cents cinquante-six mille sept cents quatre-vingt-neuf virgule neuf huit sept six cinq quatre

Notons au passage que pour les chiffres après la virgule, en toutes lettres, ce n'est pas encore tout à fait ça...
...Heureusement, ce n'est probablement la fonctionnalité que nous serons le plus souvent amené à utiliser au sein de nos applications !

Et si nous lançons la même portion de code avec une locale américaine :

echo NumberFormatter::create('en_US', NumberFormatter::DECIMAL)->format(123456789.987654) . "\n";
echo NumberFormatter::create('en_US', NumberFormatter::CURRENCY)->format(123456789.987654) . "\n";
echo NumberFormatter::create('en_US', NumberFormatter::SPELLOUT)->format(123456789.987654) . "\n";

nous obtenons :

123,456,789.988
$123,456,789.99
one hundred and twenty-three million, four hundred and fifty-six thousand, seven hundred and eighty-nine point nine eight seven six five four

Au niveau des différences, on remarquera en particulier :

  • L'utilisation d'un espace comme séparateurs de millers en français, et d'une virgule outre-atlantique,
  • l'emploi d'une virgule comme marqueur de décimales en français, et d'un point aux états-unis,
  • l'affichage automatique de la bonne unité monétaire, à la bonne position,
  • et, bien évidemment, que la langue n'est pas la même !

Suis-je le seul à être intéressé ?


MessageFormatter

Ceux d'entre nous qui utilisent parfois des fichiers de ressources pour la localisation des messages de leur application apprécieront la nouvelle classe MessageFormatter : à partir de chaînes de caractères contenant des marqueurs, elle permet de générer des phrases localisées.


Afficher un message

Un exemple illustrera mes propos :
Définissons une chaîne de caractères pour deux locales :

$format_FR = 'Le {0,date,full} à {1,time,short}, {2,number,integer} pommes coûtent {3,number,currency}';
$format_US = 'On {0,date,full} at {1,time,short}, {2,number,integer} apples cost {3,number,currency}';

Et les données que nous voulons injecter dans cette chaîne (notez que l'ordre de définition des données dans ce tableau correspond à celui des nombres en première position de chacun des marqueurs dans les chaînes au-dessus, des fois que l'ordre de ces marqueurs change d'une locale à l'autre) :

$data = array(time(), time(), 5, 3.1415);

Maintenant, remplaçons les marqueurs par les valeurs :

echo MessageFormatter::formatMessage('fr_FR', $format_FR, $data) . "\n";
echo MessageFormatter::formatMessage('en_US', $format_US, $data) . "\n";

Ce qui donne l'affichage suivant :

Le jeudi 16 octobre 2008 à 07:42, 5 pommes coûtent 3,14 €
On Thursday, October 16, 2008 at 7:42 AM, 5 apples cost $3.14

En somme, nous regroupons en un seul appel l'utilisation des deux classes que nous avions vu juste au-dessus pour l'affichage de dates et de nombres ^^.
Les responsables de la localisation de vos applications (et les développeurs devant mettre en place son internationalisation préalable) apprécieront, je n'en doute pas un instant !

Pour la liste des types de marqueurs possibles, vous pouvez consulter la documentation de la bibliothèque ICU, sur laquelle l'extension intl est basée.


Extraire des informations d'un message

La classe MessageFormatter permet aussi d'effectuer l'opération inverse : extraire des informations d'un message.

Par exemple, pour extraire les informations d'une chaine à partir d'une locale et du format :

$parts = MessageFormatter::parseMessage('fr_FR',
    'Le {0,date,full} à {1,time,short}, {2,number,integer} pommes coûtent {3,number,currency}',
    'Le jeudi 28 août 2008 à 20:33, 5 pommes coûtent 3,14 €');
var_dump($parts);

Ce qui donne :

array
  0 => int 1219874400
  1 => int 70380
  2 => int 5
  3 => float 3.14

Par contre, dans le cas d'une chaîne invalide (ne correspondant pas à la locale, ici) :

$parts = MessageFormatter::parseMessage('en_EN',
    'Le {0,date,full} à {1,time,short}, {2,number,integer} pommes coûtent {3,number,currency}',
    'Le jeudi 28 août 2008 à 20:33, 5 pommes coûtent 3,14 €');
var_dump($parts);

L'extraction des données échoue :

boolean false

Heureusement, c'est plus souvent dans l'autre sens que nous utiliserons les fonctionnalités de cette nouvelle classe.

Quoi qu'il en soit, je n'ai probablement qu'une seule chose à dire, ici : utilisez MessageFormatter !


Nous voici arrivés à la fin de la première partie de cet article. A demain pour la seconde, qui introduira les classes Locale et Collator !


Note

[1] Je sais, c'est férié, et j'avais pris l'habitude de ne publier que les jours ouvrés -- mais tant pis, ce n'est pas une raison pour ne pas parler de PHP 5.3 ^^