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

11 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".

Voici la seconde partie de cet article traitant des fonctionnalités en rapport avec l'internationalisation et à la localisation que l'extension intl, maintenant intégrée à PHP, nous apporte -- la première partie ayant été publiée hier matin.

Sommaire de cette seconde partie :


Collator

PHP 5.3 défini une nouvelle classe, dont l'objectif est de faciliter les comparaisons de chaines de caractères : la classe Collator.
La notion de tri étant intimement liée à celle de comparaison, Collator permet aussi de faciliter les tris de listes, en se basant sur une locale.


Collator et comparaisons

Pour commencer, définissons une fonction utilitaire, prenant en paramètre une instance d'objet Collator et deux chaines de caractères ; cette méthode comparera les deux chaines à l'aide du Collator, et affichera le résultat de la comparaison : la première chaine est-elle << plus petite >> que la seconde ? << plus grande >> ? Ou sont-elles toutes les deux considérées comme << égales >> ?

function echoCompare(Collator $col, $str1, $str2) {
    $result = $col->compare($str1, $str2);
    if ($result < 0) {
        echo "'" . htmlentities($str1, ENT_COMPAT, 'UTF-8')
            . "' &lt; '" . htmlentities($str2, ENT_COMPAT, 'UTF-8') . "'\n";
    } else if ($result > 0) {
        echo "'" . htmlentities($str1, ENT_COMPAT, 'UTF-8')
            . "' &gt; '" . htmlentities($str2, ENT_COMPAT, 'UTF-8') . "'\n";
    } else {
        echo "'" . htmlentities($str1, ENT_COMPAT, 'UTF-8')
            . "' = '" . htmlentities($str2, ENT_COMPAT, 'UTF-8') . "'\n";
    }
}

Collator::NUMERIC_COLLATION

Passons maintenant à une première utilisation de la classe Collator, en travaillant avec une locale française, mais en précisant, à l'aide de la méthode setAttribute, que la comparaison en mode numérique doit être activée -- par opposition à une comparaison purement alphabétique :

$col = new Collator('fr_FR');
echoCompare($col, 'image-100.jpg', 'image-3.jpg');

$col = new Collator('fr_FR');
$col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
echoCompare($col, 'image-100.jpg', 'image-3.jpg');

Dans le premier cas, avec une collation "par défaut", "image-100" est considéré comme plus petit que "image-3".
En effet, "1" est plus petit que "3".

Dans le second cas, par contre, en travaillant avec une comparaison numérique, et non plus uniquement alphabétique basée sur les valeurs d'octets des caractères, c'est l'inverse qui est obtenu : 100 est supérieur à 3 : la comparaison s'est faite sur l'ensemble de la valeur numérique, et non plus caractère par caractère :

'image-100.jpg' < 'image-3.jpg'
'image-100.jpg' > 'image-3.jpg'

setStrength : Collator::PRIMARY

Passons maintenant à une comparaison de deux chaines, comportant éventuellement des majuscules :
Par défaut, les majuscules et les minuscules sont considérées comme étant des caractères différent, et cela se reflète sur la comparaison.

Par contre, nous pouvons spécifier des niveaux de "force" au niveau de la classe Collator, à l'aide de la méthode setStrength, qui accepte 5 forces différentes (de Collator::PRIMARY à Collator::QUATERNARY, et Collator::IDENTICAL).
Voyons ce qu'il se passe avec un niveau Collator::PRIMARY :

$col = new Collator('fr_FR');
echoCompare($col, 'appartement', 'appArtement');

$col = new Collator('fr_FR');
$col->setStrength(Collator::PRIMARY);
echoCompare($col, 'appartement', 'appArtement');

Cette portion de code nous donne la sortie suivante :

'appartement' < 'appArtement'
'appartement' = 'appArtement'

L'appel à setStrength en lui passant une force Collator::PRIMARY en paramètre rend la comparaison insensible à la casse -- du moins en français.

Un peu de la même manière, voyons ce qu'il se passe en comparant deux chaines de caractères, l'une contenant un caractère accentué, et l'autre pas :

$col = new Collator('fr_FR');
echoCompare($col, 'rentree', 'rentrée');

$col = new Collator('fr_FR');
$col->setStrength(Collator::PRIMARY);
echoCompare($col, 'rentree', 'rentrée');

La sortie obtenue devient la suivante :

'rentree' < 'rentrée'
'rentree' = 'rentrée'

Dans le premier cas, avec une comparaison "par défaut", les lettres accentuées sont considérées comme différentes de leurs "équivalents" non accentués.
Mais avec une comparaison de "force primaire", ces deux types de caractères, tout comme les majuscules et minuscules, deviennent (ou sont considérés comme ^^ ) identiques !

Collator::ALTERNATE_HANDLING

Pour continuer, voici un exemple de possibilité offerte par l'attribut Collator::ALTERNATE_HANDLING :

$col = new Collator('fr_FR');
echoCompare($col, 'SNCF', 'S.N.C.F');

$col = new Collator('fr_FR');
$col->setAttribute(Collator::ALTERNATE_HANDLING, Collator::SHIFTED);
echoCompare($col, 'SNCF', 'S.N.C.F');

Et les résultats obtenus :

'SNCF' > 'S.N.C.F'
'SNCF' = 'S.N.C.F'

En mode de comparaison par défaut, "SNCF" est différent de "S.N.C.F".
Mais en activant l'attribut Collator::ALTERNATE_HANDLING, on peut avoir une comparaison qui les considère comme identiques...

Et en ajoutant un réglage au niveau de la force :

$col = new Collator('fr_FR');
echoCompare($col, 'sncf', 'S.N.C.F');

$col = new Collator('fr_FR');
$col->setAttribute(Collator::ALTERNATE_HANDLING, Collator::SHIFTED);
$col->setStrength(Collator::PRIMARY);
echoCompare($col, 'sncf', 'S.N.C.F');

Nous obtenons :

'sncf' > 'S.N.C.F'
'sncf' = 'S.N.C.F'

C'es-à-dire une comparaison à la fois insensible à la casse, et aux points séparant éventuellement les lettres d'un acronyme !


Collator et tris

Maintenant que nous avons vu quelques unes des possibilités offertes par la classe Collator pour ce qui est des comparaisons de chaines de caractères, passons aux tris -- après tout, trier une liste revient uniquement à comparer ses éléments les uns par rapport aux autres, et les réordonner en fonction des résultats obtenus lors de ces comparaisons !

Voici un exemple de liste à trier :

$liste = array(
    'image-1.jpg',
    'photo-1.png',
    'image-100.jpg',
    'image-30.jpg',
    'imaGE-45.png',
    'phOto-3.jpg',
    'PHOTO-2.png',
);

Et si nous souhaitons l'afficher :

var_dump($liste);

Ce qui donne l'affichage suivant :

array
  0 => string 'image-1.jpg' (length=11)
  1 => string 'photo-1.png' (length=11)
  2 => string 'image-100.jpg' (length=13)
  3 => string 'image-30.jpg' (length=12)
  4 => string 'imaGE-45.png' (length=12)
  5 => string 'phOto-3.jpg' (length=11)
  6 => string 'PHOTO-2.png' (length=11)

Au départ, les éléments de notre liste sont affichés dans l'ordre de leur déclaration.

A présent, trions notre liste en utilisant une collation numérique :

$col = new Collator('fr_FR');
$col->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
$col->sort($liste);
var_dump($liste);

Nous n'avons qu'à configurer une instance de Collator, comme précédemment, et à appeler la méthode sort pour effectuer le tri en lui-même, ce qui donne :

array
  0 => string 'image-1.jpg' (length=11)
  1 => string 'image-30.jpg' (length=12)
  2 => string 'imaGE-45.png' (length=12)
  3 => string 'image-100.jpg' (length=13)
  4 => string 'photo-1.png' (length=11)
  5 => string 'PHOTO-2.png' (length=11)
  6 => string 'phOto-3.jpg' (length=11)

Notre tableau est devenu trié, comme nous le souhaitions, en se basant sur une comparaison tenant compte des nombres présents dans les chaines de caractères.

Ceci n'était qu'un exemple ; je vous laisse expérimenter pour voir ce qui est possible !
Et, au besoin, n'hésitez pas à consulter la documentation de la classe Collator :-)


Locale

Pour terminer avec les nouveautés de PHP 5.3 en rapport avec l'internationalisation, passons à la classe Locale.
Dans les grandes lignes, elle permet, comme son nom semble l'indiquer, de manipuler... des locales ^^


acceptFromHttp

Pour commencer, la méthode acceptFromHttp permet de récupérer depuis les en-têtes HTTP envoyées par le navigateur de votre utilisateur, la locale qu'il préfére :

$httpAccept = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
var_dump($httpAccept);
var_dump(Locale::acceptFromHttp($httpAccept));

Par exemple, j'ai tendance à paramétrer mon navigateur pour indiquer que je préfère recevoir des pages en français de France (du français, donc, mais avec les spécificités culturelles usuelles en France, qui peuvent être différentes de celles auxquelles sont habitués les Belges ou Canadiens francophones, par exemple -- ne serait-ce que pour l'unité monétaire), tout en indiquant que si << français >> n'est pas disponible, je suis d'accord pour recevoir, par ordre de priorités décroissantes, de l'anglais américain, puis de l'anglais quel qu'il soit, et, enfin, avec une priorité très faible, de l'allemand :

string 'fr-fr,fr;q=0.8,en-us;q=0.6,en;q=0.4,de;q=0.2' (length=44)
string 'fr_FR' (length=5)

Ma préférence allant au français de France, la locale extraite des en-têtes envoyées par mon navigateur est, fort logiquement, fr_FR.
Une simple méthode utilitaire, donc, mais qui vous évitera d'avoir à découper vous-même l'en-tête Accept-Language.


getAllVariants

Autre méthode, getAllVariants, pour récupérer les variantes composant une locale.
Par exemple, avec cette locale au nom compliqué :

var_dump(Locale::getAllVariants('sl_IT_NEDIS_ROJAZ_1901'));

Nous obtenons la sortie suivante :

array
  0 => string 'NEDIS' (length=5)
  1 => string 'ROJAZ' (length=5)
  2 => string '1901' (length=4)

Notre locale est composée de trois variantes.


getDisplayLanguage

Encore une méthode utilitaire : getDisplayLanguage, pour obtenir, localisé, le nom affichable de la langue correspondant à une locale :

Cette méthode prend en premier paramètre le code de la locale dont elle doit retourner la langue, et, éventuellement, en second paramètre, la locale à utiliser pour le nom en sortie :
(Si ce second paramètre n'est pas passé, c'est la locale actuellement en cours au niveau du système qui est utilisée.)

var_dump(Locale::getDisplayLanguage('fr_FR'));
var_dump(Locale::getDisplayLanguage('fr_FR', 'fr'));
var_dump(Locale::getDisplayLanguage('fr_FR', 'de'));

Ce qui donne à l'affichage :

string 'French' (length=6)
string 'français' (length=9)
string 'Französisch' (length=12)

Notez que la locale par défaut sur mon système au moment où j'ai exécuté cette portion de code était une locale en_US.

Plus besoin de stocker 36 libellés de langues en 36 langages dans vos fichiers de ressources ;-)
Et l'extension intl se charge pour vous de la majuscule dans certaines langues et pas dans d'autres -- elle est elle-même sensible à la locale, bien évidemment ^^


getPrimaryLanguage

La méthode getPrimaryLanguage permet d'extraire la langue principale correspondant à une locale :

var_dump(Locale::getPrimaryLanguage('fr_FR'));
var_dump(Locale::getPrimaryLanguage('en_US'));
var_dump(Locale::getPrimaryLanguage('de_DE'));

Ce qui renvoit :

string 'fr' (length=2)
string 'en' (length=2)
string 'de' (length=2)


getKeywords

Pour extraire les mots-clefs d'une définition de locale, utilisez la méthode getKeywords :

var_dump(Locale::getKeywords('de_DE@currency=EUR;collation=PHONEBOOK'));

Ce qui, avec cette définition de locale plus complète que celles utilisées jusqu'à présent, donne :

array
  'collation' => string 'PHONEBOOK' (length=9)
  'currency' => string 'EUR' (length=3)

Sont considérés comme "mots-clefs" l'unité monétaire, la collation, ...


parseLocale

Pour extraire les composantes d'une locale, utilisez la méthode parseLocale, et non un quelconque explode :

var_dump(Locale::parseLocale('fr_FR_POSIX'));

Pour obtenir, ici :

array
  'language' => string 'fr' (length=2)
  'region' => string 'FR' (length=2)
  'variant0' => string 'POSIX' (length=5)

Puisque notre locale correspondait à du français, en France, avec une variante POSIX.


getDefault et setDefault

Et enfin, pour finir, utilisons la méthode getDefault pour déterminer la locale actuelle du système, et setDefault pour en changer :

var_dump(Locale::getDefault());
var_dump(Locale::getDisplayLanguage('fr_FR'));
Locale::setDefault('fr_FR');
var_dump(Locale::getDefault());
var_dump(Locale::getDisplayLanguage('fr_FR'));

Ce qui donne, sur ma machine :

string 'en_US_POSIX' (length=11)
string 'French' (length=6)
string 'fr_FR' (length=5)
string 'français' (length=9)

En effet, j'ai installé un système anglais américain de base, et non français...
... Note : vous ne savez à peu près jamais comment sera paramétré le serveur sur lequel vous déploierez vos applications ; ces méthodes peuvent donc vous être fort utiles !


Les deux parties de cet article nous ont permis de découvrir tout un lot de nouvelles classes, qui nous faciliteront à l'avenir l'internationalisation et la localisation de nos applications PHP.
Je pense d'ailleurs déjà à quelques cas, dans mes développements de tous les jours, où ces classes me seraient bien utiles, dès aujourd'hui !

Et vous ?