samedi 29 septembre 2007

Internationalisation, i18n, unicode et utf8

Aujourd'hui c'est le week end et j'oublie un peu le code. J'ai donc choisi de vous parler de l'internationalisation et de la saine gestion des caractères exotiques. Ceci afin de pouvoir créer des sites qui acceptent et qui affichent n'importe quel caractère. Vous permettrez ainsi à vos visiteurs suédois, polonais, irakiens, tchèques ou japonais de sentir que votre site à été conçu aussi pour eux. Pour vos tests, vous pouvez vous référer à cette page. Pour clarifier les choses, commençons par quelques définitions.

Unicode définit de manière globale ce qu'est un caractère. Unicode définit un numéro, appellé codepoint pour chaque caractère possible sur la planète terre. Vous pouvez voir la liste de ces caractères ici. Par exemple le caractère 'k' est au codepoint 'U+006B' . Le caractère khmer 'dap roc' est situé au codepoint 'U+19FA' . Unicode est donc une manière globale de traiter les caractères de manière abstraite. Depuis perl 5.8, perl est compatible avec unicode. Ainsi pour faire faire une chaine composée des deux caractères cités en exemple, on peut utiliser la notation suivante:

  1. my $kkhmer = "\x{006B}\x{19FA}"
C'est d'ailleurs un moyen pour écrire du code "obsfuscated". Cette chaine kkhmer est donc de longueur 2 en perl. Il existe aussi un moyen d'écrire des caractères unicode sous forme d'entités html: &006B;&19FA; . Le module HTML::Entities vous permet d'automatiser cette transformation, et mason fournit un mécanisme pour la faire automatiquement.

UTF-8 est un encodage. C'est donc un moyen de représenter un caractère sous une forme binaire pour le stockage dans un fichier, la transmission à travers un réseau ou le stockage dans une base de données. Contrairement à l'encodage ISO-8859-1 (plus connu sous le nom latin 1), UTF-8 est capable d'encoder tout les caractères unicodes. Sans rentrer dans les détails, utf-8 encode chaque caractère sur 1 ou plusieurs octets. Beaucoup de problèmes d'encodage proviennent de la confusion entre la notion de chaine unicode et de chaine binaire d'octets encodant ces chaines unicodes. Pour reprendre notre exemple, le caractère 'k' est représenté en UTF-8 par l'octet '6B' et le caractère khmer est représenté par les 3 octets 'xE1xA7xBA' .

Beaucoup de confusions proviennent aussi du fait que pour les caractères purement ascii (comme dans le bon vieux temps), le codepoint unicode, l'encodage UTF-8 et l'encodage latin1 sont identiques. C'est pourquoi on ne se rend compte de ce genre de problème que lorsqu'il commence à y avoir des accents dans le système.

Cas pratique.

Après ces quelques clarifications, construire un système compatible avec les caractères internationaux est beaucoup plus facile. Il suffit de cloisonner les responsabilités et d'assurer que la communication entre les composants du système se fait dans le bon encodage. Pour un exemple concret, nous allons prendre 3 composants: le navigateur web, perl (sous apache) et mysql. Pour chacun de ces composants, on va définir (et assurer) trois choses: l'encodage de sortie, l'encodage d'entrée et la représentation interne.

  • Le navigateur:
    • Encodage d'entrée: Défini à la fois par l'entête HTTP 'Content-Type: text/html; charset=UTF-8' et éventuellement par la balise méta HTML : <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> . Chaque navigateur à un comportement spécifique lorsque ces deux informations ne sont pas identiques. Assurez vous donc qu'elle le sont. Ajoutez la directive AddDefaultCharset UTF-8 Dans votre config d'apache.
    • Encodage de sortie (POST de formulaire ou GET vers le serveur): par défaut c'est le même que l'encodage d'entrée. Il est cependant bon de le spécifier dans chaque formulaire, au cas ou des utilisateurs changerais l'encodage de la page avant d'envoyer leurs informations. Dans chaque formulaire, ajoutez simplement l'attribut accept-charset="UTF-8" .
    • Encodage interne: Normalement, on n'a pas à connaître cette information. Chaque système à fait un choix. L'important c'est que le système puisse traiter des chaines unicode. C'est le cas des navigateurs et de javascript.
  • Perl:
    • Encodage d'entrée (web): Par défaut rien n'est défini. C'est donc des chaines binaires que vous récupérez. Si vous faite du cgi, spécifiez l'encodage au niveau de l'objet $c avec $c->charset('utf-8'). Les appels à $c->param() vous donnerons des chaines perl correctes (unicode). En mason, le problème est identique. J'utilise personnellement la fonction Encode::decode_utf8 sur chaque paramêtre que je sais pouvoir être du texte libre.
    • Encodage de sortie: Par défaut perl encode les chaines en latin1 en sortie. On peur modifier ce comportement avec 'binmode STDOUT , ':utf8' en CGI. En mason, on peut ajouter un filtre à la sortie en ajoutant la directive suivante dans apache: PerlAddVar MasonOutMethod "sub{ binmode STDOUT , ':utf8' ; print @_ ;}"
    • Encodage interne: Comme je l'ai dit précédemment, on ne devrait pas connaitre cette information. Cependant en perl, les chaines unicode internes sont encodées en UTF-8. C'est aussi une source de confusion, puisque ça amène a penser qu'on peut outputer directement une chaine interne. Ce n'est pas le cas (voir point précédent).
  • MySQL:
    • Encodage d'entrée: Aucun. En utilisation avec perl, ce n'est pas génant, étant donné que les chaines internes sont représentées en UTF-8. Lorsqu'on fait un insert ou un update avec le driver MySQL, c'est donc des octets UTF-8 qui sont envoyés et stockés par MySQL.
    • Encodage de sortie: Aucun. C'est donc des octets qu'on obtient par défault lorsqu'on fait un select avec le driver standard. Pour eviter de les décoder à chaque fois et pour obtenir des vraies chaines perl UTF-8, j'ai écrit un patch pour le driver disponible ici. Ce patch a été inclu dans la version officielle du driver à titre expérimental. Activez le décodage UTF-8 automatique comme ceci: $dbh->{'mysql_enable_utf8} = 1
Conclusion:

Grace à ces quelques techniques, vous êtes maintenant en mesure de construire un site compatible avec tout les caractères de la planète. Dans notre cas pratique, on voit que les choses sont nettes concernant le navigateur et perl. MySQL doit certainement être encore amélioré sur ce point. Pour d'autres bases de données, les choses sont plus claires. Par exemple en Postgresql, si on déclare une colonne comme UTF-8, la communication avec perl à travers le driver se fait de manière transparente.

Aucun commentaire: