Php 5.3 : générer une liste de pays localisée
14 septembre 2009
Par Seb
Si vous êtes familier des formulaires de contact traduits en 26 langues – et même sans cela – vous vous êtes forcément déjà frotté au problème de la traduction des listes de noms de pays. En plus d’être fastidieux, cette opération nécessite d’être répétée pour chaque langue, et à moins d’avoir à votre disposition une base avec tous les noms de pays dans toutes les langues, c’est un véritable calvaire.
Depuis Php 5.3 (et pour toutes les versions compilées avec la librairie PECL Intl), nous avons à notre disposition la classe Locale qui nous propose notamment une fonction nommée getDisplayRegion(), que nous allons détourner un peu de sa fonction première ici.
Une liste pour les lister tous
Pour commencer, il nous faudra quand même une liste des pays, et notamment de leur code de représentation (ISO 3166-1 alpha-2 pour les intimes). Pour cela, 2 moyens : utiliser une base de donnée, ou une simple inclusion de fichier, au choix :
CREATE TABLE IF NOT EXISTS `pays` ( `code` smallint(3) unsigned NOT NULL, `iso2` char(2) NOT NULL, `nom` varchar(100) NOT NULL, PRIMARY KEY (`code`), KEY `code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Liste des pays'; INSERT INTO `pays` (`code`, `iso2`, `nom`) VALUES (4, 'af', 'Afghanistan'), (8, 'al', 'Albania, People\'s Socialist Republic Of'), ... (887, 'ye', 'Yemen'), (894, 'zm', 'Zambia, Republic Of');
Télécharger : Liste des pays – déclaration SQL
Ou :
<?php // Liste des pays $pays = array( 'af' => 'Afghanistan', 'al' => 'Albania, People\'s Socialist Republic Of', ... 'ye' => 'Yemen', 'zm' => 'Zambia, Republic Of' );
Télécharger : Liste des pays – inclusion Php
Les deux méthodes se valent, après c’est juste une question de goût… Pour notre exemple, nous allons utiliser l’inclusion Php, avec mise en cache.
Classe de gestion
Une fois que nous avons notre liste, nous allons créer une classe pour gérer toutes les opérations dont nous aurons besoin :
<?php
/**
* Classe de gestion des noms de pays dans diverses langues
* @version 1.0
*/
class ListingPays {
}
La première étape de la mise en place d’une classe est bien entendu son constructeur. Ici, nous allons permettre le passage d’une locale pour permettre de définir la langue d’affichage de la liste. On laissera aussi un mode automatique, qui tentera de détecter la locale du navigateur de l’internaute. Attention, tous les navigateurs ne fournissent pas cette information, il convient donc de l’utiliser en connaissance de cause.
/**
* La locale dans laquelle construire le listing
* @var string
*/
protected $_locale;
/**
* Constructeur de la classe
* @param string la locale à utiliser, ou NULL pour utiliser la locale de l'utilisateur (par défaut)
*/
public function __construct($locale = NULL)
{
// Si la locale n'est pas fournie
if (is_null($locale))
{
// On tente de la récupérer à partir du useragent de l'utilisateur, sinon on prend la locale par défaut
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) and strlen($_SERVER['HTTP_ACCEPT_LANGUAGE']) > 0)
{
$locale = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
else
{
$locale = Locale::getDefault();
}
}
// Mémorisation
$this->_locale = $locale;
}
Il va nous falloir ensuite charger la liste des pays. Pour cela, on utilisera une méthode statique avec mise en cache des informations, afin d’accélérer les opérations suivantes :
/**
* Cache de la liste des noms de pays
* @var array
*/
protected static $_list;
/**
* Obtention de la liste des pays
* @return array le listing des noms de pays, indexés par leur code ISO-2
*/
protected static function _getList()
{
// Si pas encore chargé
if (!isset(self::$_list))
{
// Inclusion et stockage
include('pays.php');
self::$_list = $pays;
}
// Renvoi
return self::$_list;
}
Pensez bien entendu à faire pointer le chargement du fichier vers le dossier où vous l’avez enregistré. Si vous préférez passer par une table MySQL, c’est ici que vous pouvez insérer votre code de chargement, à la place de l’include.
Construction de la liste
Maintenant que nous avons notre liste de pays et notre locale, il ne reste plus qu’à composer notre SELECT, via une méthode dédiée :
/**
* Construction du listing
* @param string $name le nom à donner au champ SELECT
* @param string $current la valeur actuellement sélectionnée
* @return string le code prêt pour affichage
*/
public function build($name = 'pays', $current = '')
{
// Récupération de la liste des pays
$list = self::_getList();
// Liste des noms traduits
$listIntl = array();
// Préparation du listing
$retour = array();
$retour[] = '<select name="'.$name.'" id="'.$name.'">';
// Parcours
foreach ($list as $iso => $nom)
{
// Capitalisation
$code = strtoupper($iso);
// On tente de récupérer le nom localisé
$nomIntl = Locale::getDisplayRegion('en-'.$code, $this->_locale);
// Si pas trouvé, on garde l'anglais
if ($nomIntl == $code)
{
$nomIntl = $nom;
}
// Ajout au listing
$actif = ($current == $iso) ? ' selected="selected"' : '';
$retour[] = '<option value="'.$iso.'"'.$actif.'>'.htmlspecialchars($nomIntl).'</option>';
}
// Finalisation
$retour[] = '</select>';
return implode("\n", $retour);
}
Reste juste un dernier détail : notre liste de pays initiale est triée par ordre alphabétique anglais, mais une fois traduite, ce tri ne sera plus cohérent : on se retrouve rapidement avec des z au milieu des b… Et c’est encore pire avec les caractères non latins.
Pour corriger ça, nous allons utiliser une consœur de la classe Locale : Collator. Cette classe permet de trier des listes en tenant compte des spécificités de chaque langue : ordre des lettres, précédence des caractères spéciaux, etc… Je vous laisse vous référer à la documentation pour plus de détails.
Dans notre cas, nous allons scinder notre boucle foreach en deux, pour permettre de tri les noms de pays après les avoir traduits, mais avant de composer la liste déroulante :
/**
* Construction du listing
* @param string $name le nom à donner au champ SELECT
* @param string $current la valeur actuellement sélectionnée
* @return string le code prêt pour affichage
*/
public function build($name = 'pays', $current = '')
{
// Récupération de la liste des pays
$list = self::_getList();
// Liste des noms traduits
$listIntl = array();
// Parcours
foreach ($list as $iso => $nom)
{
// Capitalisation
$code = strtoupper($iso);
// On tente de récupérer le nom localisé
$nomIntl = Locale::getDisplayRegion('en-'.$code, $this->_locale);
// Si pas trouvé, on garde l'anglais
if ($nomIntl == $code)
{
$nomIntl = $nom;
}
// Ajout
$listIntl[$iso] = $nomIntl;
}
// Tri alphabétique
$collator = new Collator($this->_locale);
$collator->asort($listIntl);
// Préparation du listing
$retour = array();
$retour[] = '<select name="'.$name.'" id="'.$name.'">';
// Parcours
foreach ($listIntl as $iso => $nom)
{
// Ajout au listing
$actif = ($current == $iso) ? ' selected="selected"' : '';
$retour[] = '<option value="'.$iso.'"'.$actif.'>'.htmlspecialchars($nom).'</option>';
}
// Finalisation
$retour[] = '</select>';
return implode("\n", $retour);
}
Résultat final
Et voilà ! Vous pouvez maintenant construire une liste de pays dans n’importe quelle langue, sans vous soucier d’embarquer une lourde base. La construction d’une liste se fait simplement de la manière suivante, après inclusion de la classe :
<?php $listing = new ListingPays(); echo $listing->build(); ?>
Ce qui nous donne :
Ou pour un exemple plus parlant, dans une locale bien distincte :
<?php
$listing = new ListingPays('zh');
echo $listing->build();
?>
Qui nous donne :
Voici la classe complète :
<?php
/**
* Classe de gestion des noms de pays dans diverses langues
* @version 1.0
*/
class ListingPays {
/**
* La locale dans laquelle construire le listing
* @var string
*/
protected $_locale;
/**
* Cache de la liste des noms de pays
* @var array
*/
protected static $_list;
/**
* Constructeur de la classe
* @param string la locale à utiliser, ou NULL pour utiliser la locale de l'utilisateur (par défaut)
*/
public function __construct($locale = NULL)
{
// Si la locale n'est pas fournie
if (is_null($locale))
{
// On tente de la récupérer à partir du useragent de l'utilisateur, sinon on prend la locale par défaut
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) and strlen($_SERVER['HTTP_ACCEPT_LANGUAGE']) > 0)
{
$locale = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
else
{
$locale = Locale::getDefault();
}
}
// Mémorisation
$this->_locale = $locale;
}
/**
* Construction du listing
* @param string $name le nom à donner au champ SELECT
* @param string $current la valeur actuellement sélectionnée
* @return string le code prêt pour affichage
*/
public function build($name = 'pays', $current = '')
{
// Récupération de la liste des pays
$list = self::_getList();
// Liste des noms traduits
$listIntl = array();
// Parcours
foreach ($list as $iso => $nom)
{
// Capitalisation
$code = strtoupper($iso);
// On tente de récupérer le nom localisé
$nomIntl = Locale::getDisplayRegion('en-'.$code, $this->_locale);
// Si pas trouvé, on garde l'anglais
if ($nomIntl == $code)
{
$nomIntl = $nom;
}
// Ajout
$listIntl[$iso] = $nomIntl;
}
// Tri alphabétique
$collator = new Collator($this->_locale);
$collator->asort($listIntl);
// Préparation du listing
$retour = array();
$retour[] = '<select name="'.$name.'" id="'.$name.'">';
// Parcours
foreach ($listIntl as $iso => $nom)
{
// Ajout au listing
$actif = ($current == $iso) ? ' selected="selected"' : '';
$retour[] = '<option value="'.$iso.'"'.$actif.'>'.htmlspecialchars($nom).'</option>';
}
// Finalisation
$retour[] = '</select>';
return implode("\n", $retour);
}
/**
* Obtention de la liste des pays
* @return array le listing des noms de pays, indexés par leur code ISO-2
*/
protected static function _getList()
{
// Si pas encore chargé
if (!isset(self::$_list))
{
// Inclusion et stockage
include('pays.php');
self::$_list = $pays;
}
// Renvoi
return self::$_list;
}
}




Un grand merci. Et tu nous fais découvrir les classes Locale et Collator ! PHP 5.3 est vraiment une grande avancée pour le support de l’internationalisation.