Utiliser des caractères UNICODE dans une page en ISO latin

L’entête d’une page HTML permet de préciser son charset, par exemple :

<meta http-equiv="Content-Type" content="text/html;charset=ISO8859-1" >

permet de préciser que le contenu d’une page HTML, est codé en utilisant le répertoire de caractères ISO8859-1.

Ce répertoire, limité à 256 caractères, ne comporte pas plusieurs caractères qui font pourtant partie des caractères français :

Les « o e collés » majuscules "Œ" (code unicode U+0152) et minuscule "œ" (code unicode U+0153) et le ÿ majuscule (code unicode U+0178).

Le caractère euro € est maintenant rentré dans les mœurs et est même disponible directement sur un clavier windows français. Le « oe collés » est indispensable à l’écriture de mots comme : bœuf, chœur, cœur, manœuvre, mœurs, œil, œillet, œuf, œuvre, sœur, vœu,  cœlacanthe…

Quant au ÿ, il est plus rare mais présent par exemple dans des noms propres comme la Haÿ les roses.

N’hésitez pas à lire http://www.gutenberg.eu.org/pub/GUTenberg/publicationsPDF/25-andre.pdf pour plus de détails sur ce point.

Ces caractères n’étant pas supportés par ISO8859-1, soit on change le charset (pour par exemple passer en UTF-8 ou en ISO8859-15), soit on utilise un échappement. Cette dernière solution a le mérite d’être indépendante du charset utilisé pour la page.

Pour escaper les « o e collés » dans le texte HTML, la première solution est d’utiliser les entités nommés &oelig; et &OElig; ce qui donne par exemple (pour le titre d’une page HTML) :

<title>La vie trépidante du c&oelig;lacanthe</title>

On peut aussi utiliser les entités numériques décimale HTML ; ainsi :

<title>La vie trépidante du c&#339;lacanthe</title>

est aussi parfaitement valide (339 étant le code décimal correspond à 153 en hexadécimal), ou pour rester en hexadécimal :

<title>La vie trépidante du c&#x153;lacanthe</title>

Il se trouve que Windows autorise aussi (mais cette une mauvaise pratique puisqu’elle n’assure pas que le caractère sera correctement interprété sur toutes les plateformes) l’entité &#156; qui correspond au codage Windows-1252 et au raccourcis clavier Windows pour obtenir le caractère (Alt+0156).

En ce qui concerne le code javascript, les mêmes précautions sont aussi valables. Elles devraient même être étendues à tous les caractères non ASCII, parce qu’il suffit d’éditer un fichier javascript avec un éditeur non ASCII, ou supportant un autre charset que celui utilisé initialement pour avoir des problèmes.

Ainsi,

<SCRIPT>
var a="cœlacanthe" ;
alert("a : "+a) ;
</SCRIPT >

Ne fonctionnerait pas correctement avec une page dont le charset est en ISO8859-1. Il existe la possibilité de préciser un charset pour la balise SCRIPT, lorsqu’on utilise un fichier externe, ce qui donne par exemple :

<script type="text/javascript" src="monscript.js" charset="UTF-8"></script>

mais cela ne résout pas le problème des scripts inline et reste dangereux (le charset du fichier étant précisé dans la page qui l’utilise, pas par le fichier lui-même).

En fait, dans l’exemple précédent, la longueur même de la chaîne est fausse avec une page au charset en ISO8859-1 (elle fait 11 caractères dans ce cas, alors qu’elle ne devrait en comporter que 10).

Il faut donc utiliser la forme uxxxxxxxx est le code hexadécimal unicode du caractère. Cet échappement javascript fonctionne de la même façon qu’en java. Ainsi, la forme suivante fonctionne correctement, quel que soit le charset de la page :

<SCRIPT>
var a="cu0153lacanthe" ;
alert("a : "+a) ;
</SCRIPT >

Attention, la chaîne a comporte bien 10 caractères (l’échappement UNICODE correspond toujours à un seul caractère).

Notons que comme en java, le caractère produit par un échappement UNICODE ne participe pas à d'éventuels autres échappements. Par exemple,

u005cu005a donne les 6 caractères u 0 0 5 a même si l’échappement  u005C donne le caractère "".

Pour le caractère euro, le code UNICODE est U+20AC, en HTML, on peut donc utiliser les entités &euro; &#x20AC;ou &#8364; (là aussi, il est très déconseillé d’utiliser le codage Windows-1252 : &#128;).

En javascript, on utilisera donc u20AC.

Noter que même pour une page dont le charset est à ISO8859-1, il est possible, via javascript, d’insérer dynamiquement dans la page des caractères unicodes non couverts par ISO8859-1.

Le charset de la page permet d'indiquer au navigateur comment interpréter les octets du fichier qu'il doit afficher mais n'implique pas une limitation de ce qu'on peut rajouter dynamiquement via javascript.

Ainsi, le code suivant est parfaitement valide, quel que soit le charset de la page :

<DIV id="destination"></DIV>
<SCRIPT>
var a="cu0153lacanthe" ;
document.getElementById("destination").innerHTML=a ;
</SCRIPT>

On peut donc, via AJAX, insérer des chaînes comportant des caractères UNICODE dans une page quel que soit son charset (par contre, le navigateur peut ou pas comporter la fonte capable d’afficher ce caractère, il suffit d’essayer la page http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt pour voir qu’il reste des progrès à faire dans ce domaine).

En ce qui concerne l’utilisation de JSON, la norme (http://www.ietf.org/rfc/rfc4627.txt) précise bien que le support d’UNICODE est obligatoire. Javascript doit donc permettre de communiquer en JSON avec des caractères UNICODE :

JSON text SHALL be encoded in Unicode. The default encoding is UTF-8.

JSON peut donc aussi être représenté en utilisant de l’UTF-16 ou UTF-32 mais le codage par défaut est UTF-8.

Rappelons qu’UTF-8 est un codage à longueur variable (de 1 à 4 octets pour encoder 1 caractère), donc, toute erreur de charset (par exemple interpréter une chaine UTF-8 en ISO8859-1) se traduit par l’affichage de caractères excédentaires.

Par exemple, le caractère euro de code UNICODE U+20AC s’exprime en UTF-8 sous forme de 3 octets : E2 82 AC

En UTF-8, seuls les caractères ASCII (code inférieur à 127) s’expriment avec un seul octet, les caractères entre 128 et 255 s'expriment sur 2 octets (entre C2 80 et C3 BF). Du coup, lorsqu'il y a une erreur de charset, les caractères ISO8859-1 non ascii sont mal affichés. Par exemple, l'apparition de séquences de 2 caractères à la place d'un seul (par exemple pour le "é") est un signe indubitable d'une page contenant de l'UTF-8 affichée en ISO8859-1.

Retour d’expérience : stocker des chaînes UTF-8 dans une base de données ISO8859-1

Problématique

Les utilisateurs d'une application web doivent pouvoir saisir un texte libre (de taille inférieure à 255 caractères). Ces textes sont ensuite rendus persistants en base de données.
Les utilisateurs doivent pouvoir saisir le caractère ou des caractères non ISO8859-1 comme "œ" et les rechercher en base de données.

La base de données est configurée en ISO8859-1 et la persistance des caractères comme déclenche des erreurs.

Remarque

Sur un clavier AZERTY récent, le caractère euro peut s'obtenir directement au clavier.
Quant aux autres caractères non ISO8859-1, ils sont automatiquement insérés dans word par le correcteur orthographique. Autrement dit, un utilisateur qui saisit son commentaire sous Word et fait un copier/coller dans le TextArea de l'application web insére automatiquement ce type de caractère.

Noter que le œ est bien un caractère spécifique ; on ne peut remplacer toute séquence oe par "œ". L'e dans l'o est en effet une ligature linguistique et pas esthétique (elle a un impact sur la prononciation). Les mots coefficients, coexistance, moelle et foehn par exemple ne peuvent pas être écrits avec le caractère "œ".

Contexte technique

Une application web
Une base de données Sybase configurée en ISO8859-1 (et dont la configuration est non modifiable)
Le framework javascript DOJO
TOMCAT
JRE 1.6


Rappels : caractères supportés par les normes

Les normes ISO codent leur caractères sur 8 bits (et les 127 premières valeurs sont celles de l'ASCII).

La norme ISO8859-1 qui recouvre les caractères utilisés par une bonne partie des pays européens couvre pratiquement l'ensemble des caractères français sauf les caractères œ et Œ et Ÿ (et pas le caractère euro).

La norme ISO8859-15 intègre les caractères œ et Œ et Ÿ ainsi que l'euro "€", elle couvre l'ensemble des caractères utilisés en Français. Malheureusement, cela ne résout pas le problème des caractères UNICODE non ISO. De plus, l'intégration de ces nouveaux caractères dans ISO8859-15 s'est fait en supprimant d'autres caractères comme "½" et "¼" (présents dans ISO8859-1) qui sont toujours générés automatiquement par word.
Autrement dit, même en ISO8859-15, un copier/coller peut insérer des caractères non couverts.

UNICODE est une norme destinée à représenter tous les caractères. UNICODE utiliser un stockage sur 32 bits (de U+0000 à U+FFFF).

UTF-8 est une des normes permettant les stockage des caractères UNICODE : UTF-8 utilise un nombre d'octets variables pour stocker chaque caractère d'une chaîne.

Solution 1 : utiliser UTF8 au niveau de l'application

Il faut rajouter un charset=UTF8 sur tous les appels AJAX (par exemple dans DOJO).
Il faut rajouter un request.setCharacterEncoding("UTF-8") dans chaque servlet de l'application (voire dans un filtre générique sur les .do struts, les jsp, les servlets).

Même si le charset par défaut des servlets engine est ISO8859-1, il n'est pas nécessaire de paramétrer TOMCAT pour qu'il utilise de façon générale UTF-8 (si on souhaite néanmoins le faire, il faut modifier la balise CONNECTOR dans le fichier de configuration pour lui positionner le charset).

Rappels : stockage des caractères selon les normes

UTF8 code les caractères sur un nombre d'octets variables.
Les caractères ASCII (7 bits) sont stockés sur un seul octet (pour les caractères ASCII, il n'y a pas de différence entre un codage ISO et un codage UTF-8).
Les caractères ISO non ascii (les codes entre 128 et 255) sont stockés sur 2 octets en UTF8.
Les caractères de code supérieurs à 255 peuvent réclamer jusqu'à 4 octets pour leur codage (dans l'ancienne version UNICODE, ils pouvaient réclamer jusqu'à 6 octets).

Si l'on précise le charset UTF-8 dans l'url du driver JDBC Sybase utilisé, la base stocke des caractères UTF-8.
Attention ! la base continue à être configurée en ISO8859-1, mais on l'attaque avec un driver JDBC UTF-8.

Ainsi, si on insère la chaîne "sœur" la colonne contient 5 caractères :
"s", "
Å","<control> ","u","r" les deux caractères insérés à la place de œ stocke ont pour code UNICODE hexadécimal U+00C5 et U+OO93 (ce dernier étant un caractère de contrôle non imprimable).
Autrement dit, la base stocke bien les caractères de l'UTF-8. Si on fait un SELECT on récupère bien la chaîne correcte en JAVA.
Cependant, si on teste la taille de la colonne en base de données (via datalength ou char_length sous Sybase), on obtient 5 caractères (et pas 4).
Cela a deux conséquences :
si d'autres applications accèdent aux mêmes données, il est nécessaire de les modifier pour qu'elles gèrent l'UTF-8,
la taille est différente en nombre de caractères (en java ou en javascript) et en bases de données.
C'est à dire que si on accepte des saisies de 255 caractères, on doit utiliser une déclaration plus longue en base de données (et pas un VARCHAR 255).

[remarque : Java gère nativement UTF-8
java manipule directement un codage interne des caractères en UTF-8, autrement dit, pas de différence sur les String Java. la seule conséquence est la méthode getBytes qui rend un nombre d'octets différents selon l'encoding.
]

Bilan :

Le problème principal est qu'en utilisant UTF-8, le stockage de tous les caractères ISO (non ascii) est modifié également : et en français, on les utilise beaucoup !
En effet, les codes compris entre U+0080 (128) et U+00FF (255), mêmes s'ils correspondent aux caractères ISO8859-1, sont codés sur 2 octets (entre C2 80 et C3 BF).
Du coup, toutes les chaînes comportant des caractères ISO français (hors ascii), comme les accents ont une longueur supérieure au nombre de caractères.

Une recherche SQL via un Select LIKE ne fonctionnant plus pour tous les caractères codés sur 2 octets, la requête doit elle aussi être transformée : tout caractère non ascii doit être transformé en son équivalent UTF-8. Ainsi une recherche sur la chaîne "sœur" doit être d'abord traduite en UTF-8. [et il peut y avoir des problèmes au niveau du moteur de recherche de la base de données]

Solution 2 : escaper les caractères supérieurs à 255 et conserver une base en ISO8859-1

L'idée est de communiquer avec la base via ISO8859-1 mais d'escaper tous les caractères de code supérieurs à 255.
Du coup, la plupart des caractères français (sauf les oe , ÿ et le caractère €) sont stockés tels quels dans la base.
Seule la présence de caractères non ISO8859-1 dans une chaîne modifie la longueur de cette chaîne en base de données.

On peut choisir plusieurs escape possibles :
à la URL encoded (avec le caractère %).
Ce qui donnerait pour la chaîne "sœur" la chaîne "s%C5%93ur" Noter qu'il ne s'agit pas d'une véritable URL encoded parce que celle-ci escape tous les caractères non ascii.

avec une syntaxe à la java "suC593ur"

avec des entités nommées ou numériques HTML : "s&oelig;ur"

Conséquence :

Il faut en parallèle augmenter la taille de la colonne "commentaire", pour qu'elle fasse plus de 255 caractères (même si les contrôles javascript et java limitent les saisies à 255 caractères effectifs).
Les autres application utilisant la base doivent potentiellement être adaptées si elles utilisent le champ commentaire (par exemple par transliteration cf. solution 3 décrite plus bas).

Conclusion :

La seule vraie solution étant la configuration de la base de données en UTF-8, on n'a ensuite que des solutions qui sont des pis-aller.
La solution 2, même si elle est hybride, a l'avantage de diminuer le surcoût en terme de stockage dans la base de données mais elle nécessite de gérer par programme l'espace/unescape des chaînes traitées.

La solution 1, marche automatiquement, reste à savoir si la démarche consistant à attaquer une base configurée en ISO8859-1 avec un driver UTF-8 est généralisable à toutes les bases de données.

Dans tous les cas de figure, les requêtes de sélection LIKE (ou REGEXP mySQL) doivent être modifiées pour tenir compte de l'escape choisi

Solution 3 : escape "translitération"

Citons également une troisième solution mais qui contrairement aux autres, déclenche une perte d'information. Il s'agit de faire des translitérations des caractères fautifs.
Par exemple, de remplacer les occurences de ÿ par y, de œ par oe...
De la même façon, on peut translitérer le caractère en une chaîne "euro".

Cependant, ça ne résout pas le problème pour les caractères UTF8. ça peut cependant être un traitement utilisé pour les autres applications partageant les mêmes données en base.

Remarque :
la recherche via LIKE doit être adaptée : on peut par exemple accepter les recherches sur les orthographes soeur et sœur, utiliser un soundex ou une expression régulière appropriée.