En l'occurrence nous avons deux partenaires: le client et le serveur.
Pour la notion de client-serveur on se référera à l'article de wikipédia sur le sujet.
De plus un serveur est souvent composé de plusieurs applications pouvant agir dans le processus, il faudrait donc le considérer comme un groupe d'entités plutôt qu'une seule. Cependant le principe reste strictement identique, c'est pourquoi, dans un soucis de simplicité, il sera considéré ici comme un tout.
Quand à notre contenu il peut s'agir de tout ce qui se trouve derrière une URL: image, code HTML, fichier JavaScript, etc. On désigne cela par le terme générique «ressource», lui-même défini comme une «entité conceptuelle».
L'utilité, elle, repose sur une hypothèse: une même ressource peut être disponible sous différentes formes (les formes variant en fonction de leur: encodage, langue, type de media, jeu de caractère, en HTTP 1.1). Si c'est le cas le serveur peut tenter, grâce à ce mécanisme, de renvoyer celle qui est la plus adaptée au client.
Comment ça marche ?
Schématiquement voici le déroulement d'un tel échange :
- Le client envoie une requête au serveur demandant une ressource, dans cette requête figurent certains entêtes spécifiques qui servent à décrire le contenu le plus adapté (souvent plusieurs alternatives accompagnées de leurs préférence respective).
- Le serveur choisi la forme de la ressource la plus proche de ce que le client demande et la renvoie.
- Le client reçoit normalement le meilleur contenu par rapport à ses possibilités.
Note : De par la définition de ressource, on pourra considérer qu'un même document dans deux langues est un seule entité ou au contraire deux distinctes. Dans ce tutoriel on adoptera le premier point de vue, dans le cas contraire il n'y aurait pas lieu de traiter la langue puisque celle-ci est unique (on peut cependant imaginer une page qui ne fait que rediriger l'utilisateur vers la bonne URL en utilisant le même mécanisme).
Exemple pratique: détecter la langue avec PHP
Introduction
L'exemple proposé ici consiste à créer une fonction PHP qui retourne la meilleur langue parmi une sélection en se servant de la négociation de contenu. Notre fonction finale se présentera ainsi :
function choix_langue($defaut, $dispo) {
// code ...
}
Où $defaut est une chaîne indiquant la langue à choisir si aucune ne convient et $dispo est un tableau contenant toutes les langues disponibles (y compris celle par défaut), le tout en minuscule. La fonction retourne l'une des langues présentes dans $dispo.
Récupération de l'entête client
Comme on l'a vu précédemment , les informations permettant de choisir la meilleure forme sont envoyées dans des entêtes. Celui qui nous intéresse se trouve, en PHP, dans la variable $_SERVER['HTTP_ACCEPT_LANGUAGE']
(les autres entêtes de négociation ont également leurs variables respectives, voir fr.php.net) . La première étape est logiquement de récupérer sont contenu et, dans le cas ou il n'existerait pas, de retourner la langue par défaut:
function choix_langue($defaut, $dispo) {
// retourner la langue par défaut si l'entête n'existe pas ou est vide
if(!isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) || empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
return $defaut;
// lire le contenu de l'entête
else
$entete = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
}
Analyser l'entête
Nous avons maintenant le contenu de l'entête dans une variable, il reste le gros morceau: l'analyser. En effet tel-quel il se présente sous la forme d'une chaîne de caractère adoptant la structure suivante:
1.CodeA1[-CodeA2][-CodeA3][...][;q=0-1](,CodeB1[-CodeB2'][-CodeB3''][...][;q=0-1])(...)
- D'aspect un peu barbare cette formulation est relativement facile à lire:
- Les crochets ou parenthèses indiquent des éléments facultatifs.
- Des points de suspension indique que l'on peut répéter la structure.
- CodeA1 est le premier code principal (par exemple "fr"), CodeB1 est le second, etc.
- CodeA2 est un code secondaire complétant le code principal (par exemple "ch"), il doit toujours être précédé d'un tiret. Il peut y avoir plusieurs codes secondaires.
- q=0-1 indique la préférence associée au code précédent cela peut varier entre 0 (pire) et 1(mieux), la séparation est faite par un point-virgule. On peut omettre la préférence, auquel cas elle vaut 1.
Un exemple d'entête possible:
1.fr-ch, es;q=0.9, fr;q=0.7, en;q=0.7, en-us;q=0.6
Une méthode rencontrée assez fréquemment sur internet est d'extraire les deux premiers caractères et les utiliser comme langue préférée par l'utilisateur. Si ça ne marche pas trop mal dans certains cas, cela reste très basique: avec notre entête exemple l'utilisateur aurait reçut un document en français alors qu'il aurait préféré de l'espagnol.
Dans notre fonction nous allons réellement analyser la totalité de l'entête, pour cela on commence par le nettoyer en retirant les espaces (inutiles), puis on le transforme en minuscules (les codes doivent êtres insensibles à la casse) et enfin on en fait un tableau en séparant la chaîne là ou se trouvent les virgules.
function choix_langue($defaut, $dispo) {
// retourner la langue par défaut si l'entête n'existe pas ou est vide
if(!isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) || empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
return $defaut;
// supprimer les espaces, tout convertir en minuscules et séparer les langues
else
$langue = explode(',', strtolower(str_replace(' ', '', $_SERVER['HTTP_ACCEPT_LANGUAGE'])));
}
La variable $langue
contient maintenant une série de codes associés à leurs éventuelles préférences.
La solution retenue pour la suite est la suivante: on enregistre la langue par défaut avec une préférence de 0 dans une variable nommée $choix
. Ensuite, on parcours le tableau issu de l'entête. Pour chaque langue disponible on compare la préférence courante et celle enregistrée, si cette dernière s'avère être inférieure alors on remplace $choix
(si la préférence courante est de 1, alors on peut directement renvoyer le code trouvé). Au final $choix
contiendra la meilleure langue pour le client parmi celles disponibles sur le serveur.
function choix_langue($defaut, $dispo) {
// (...)
// choix de base, la langue par défaut avec une préférence de 0
$choix = array($defaut, 0.0);
// parcourir les langues de l'entête
foreach($langue as $l) {
// séparer langue et préférence associée (qui vaut 1 si non définie)
$l = explode(';', $l);
$l[1] = (count($l) === 1) ? 1.0 : (float) str_replace('q=', '', $l[1]);
// vérifier si la langue est disponible
if(in_array($l[0], $dispo)) {
// si la préférence est de 1 alors on retourne la langue directement
if($l[1] === 1.0)
return $l[0];
// si la préférence est meilleure que celle du choix actuel on le remplace
elseif($l[1] > $choix[1])
$choix = $l;
}
}
// on retourne la meilleure langue trouvée
return $choix[1];
}
Raffiner l'analyse
Attention: ceci n'est pas totalement fidèle à la RFC définissant l'entête., à vous de choisir si vous souhaitez l'implémenter ou non.
Notre fonction dans son état actuel analyse parfaitement le contenu de l'entête. Il est cependant possible de prendre en compte les codes comportant une partie secondaire deux fois. Une fois dans leur entier et une fois en ne considérant que le code principal, ceci afin d'éviter, par exemple, qu'un utilisateur ayant spécifié uniquement "fr-ch" se retrouve avec la langue par défaut "es" alors qu'il aurait préféré le français "fr" qui est aussi disponible.
On modifie d'abord légèrement le code, il faut ajouter un tableau des langues traitées et faire en sorte que les modifications effectuées sur le tableau $langue
soient conservées, pour ce dernier point on précise à foreach() d'utiliser une référence.
function choix_langue($defaut, $dispo) {
// (...)
// langues vues
$langueVu = array();
// parcourir les langues de l'entête
foreach($langue as &$l) {
// (...)
$langueVu[] = $l[0];
}
// (...)
}
Nous pouvons à présent parcourir $langue
une seconde fois et traiter uniquement les codes avec une partie secondaire. Le principe de traitement reste identique à deux petits détails près: on ignore tout code principal équivalent à une langue déjà traitée auparavant et on ajoute la possibilité d'appliquer une pénalité à la préférence.
function choix_langue($defaut, $dispo) {
// (...)
// on recommence en ne traitant que les codes principaux
foreach($langue as $l) {
// recherche d'un séparateur (si pas trouvé on saute)
$pos = strpos('-', $l[0]);
if($pos === false) continue;
// extraire le code principal et l'utiliser comme langue sauf si déjà traitée
$l[0] = substr($l[0], 0, $pos);
if(in_array($l[0], $langueVu)) continue;
// vérifier si la langue est disponible
if(in_array($l[0], $dispo)) {
// appliquer une pénalité (décommenter si voulue)
// $l[1] -= 0.1;
// si la préférence est de 1 alors on retourne la langue directement
if($l[1] === 1.0)
return $l[0];
// si la préférence est meilleure que celle du choix actuel on le remplace
elseif($l[1] > $choix[1])
$choix = $l;
}
}
// on retourne la meilleure langue trouvée
return $choix[0];
}
Au final
Vous avez maintenant une fonction vous permettant de déterminer la langue préférée de votre visiteur. À vous de vous en servir pour améliorer la qualité de sa visite, par exemple en l'utilisant en combinaison avec les fonctions gettext.
Pour conclure sur la négociation de contenu
Autres solutions pratiques
Nous avons créé ici un code permettant de récupérer la langue en PHP, gardez à l'esprit que ce n'est qu'un exemple parmi un vaste choix de possibilités; il existe d'autres langages de programmation, d'autres intervenants côté serveur et d'autres formes négociables. Apache par exemple possède, à lui seul, deux façons de prendre en charge la négociation de contenu.
Utilisations
Dans la pratique, la négociation de contenu vise à rendre la visite du client sur votre site la plus agréable possible en lui délivrant un contenu adapté à ses capacités.
Il est vrai que les formes concernées sont relativement limitées, cependant on peut raisonnablement prédire un bel avenir à un tel principe avec la diversification des supports permettant un accès à internet (les capacités d'un écran 24' et d'un téléphone portable n'ont pas grand chose en commun). Il probable que le mécanisme lui-même changera ou évoluera, mais le concept reste identique.
Limitations
Si un tel système est extrêmement intéressant et offre des possibilités formidables d'adaptation au client il n'est pas non plus la panacée.
Tout d'abord parce que pour pouvoir servir différentes variantes d'une ressource il faut que celles-ci existent ce qui impose un travail parfois conséquent (traduction, images alternatives, etc). Ensuite parce que ces informations provenant du client, elle ne sont pas fiables (les raisons pouvant conduire à une mauvaise configuration sont nombreuses).
En conséquence, il faut toujours proposer un moyen de forcer le choix d'une forme précise, sans quoi on peut se retrouver à compliquer la vie du visiteur, l'exact opposé du but recherché.