Cet article est la traduction d’un article de 2003 en anglais de Joel Spolsky, développeur à New York.
Avez-vous déjà entendu parler de cette mystérieuse balise Content-Type ? Celle que vous êtes censé mettre dans HTML sans jamais vraiment savoir ce qu’elle doit être ?
Avez-vous déjà reçu un e-mail de vos amis en Bulgarie avec une ligne d’objet comme « ???? ?????? ??? ???? » ?
Je suis consterné de découvrir combien de développeurs de logiciels ne sont pas vraiment au courant du monde mystérieux des jeux de caractères, des encodages, d’Unicode, etc. Il y a quelques années, un testeur bêta pour FogBUGZ se demandait si le programme pouvait gérer les e-mails entrants en japonais. Japonais ? Ils ont des e-mails en japonais ? Je n’en avais aucune idée. En examinant de près le contrôle ActiveX commercial que nous utilisions pour analyser les messages MIME, nous avons découvert qu’il faisait exactement le contraire avec les jeux de caractères, nous avons donc dû écrire un code héroïque pour annuler la mauvaise conversion et la refaire correctement. Lorsque j’ai examiné une autre bibliothèque commerciale, elle avait également une implémentation des codes de caractères complètement cassée. J’ai correspondu avec le développeur de ce package et il pensait qu’ils ne « pouvaient rien y faire ». Comme beaucoup de programmeurs, il espérait simplement que tout cela se résorberait d’une manière ou d’une autre.
Mais ça ne va pas. Quand j’ai découvert que l’outil de développement web populaire PHP ignorait presque totalement les problèmes d’encodage des caractères, utilisant joyeusement 8 bits pour les caractères, rendant presque impossible le développement de bonnes applications web internationales, j’ai pensé que c’était assez.
Donc, j’ai une annonce à faire : si vous êtes un programmeur travaillant en 2003 et que vous ne connaissez pas les bases des caractères, des jeux de caractères, des encodages et d’Unicode, et que je vous attrape, je vais vous punir en vous faisant éplucher des oignons pendant 6 mois dans un sous-marin. Je le jure.
Et une chose de plus :
CE N’EST PAS SI DIFFICILE.
Dans cet article, je vais vous expliquer exactement ce que chaque programmeur en activité devrait savoir. Toutes ces choses sur « texte brut = ASCII = caractères de 8 bits » sont non seulement fausses, mais désespérément fausses, et si vous programmez encore de cette manière, vous n’êtes guère mieux qu’un médecin qui ne croit pas aux microbes. Veuillez ne pas écrire une autre ligne de code avant d’avoir fini de lire cet article.
Avant de commencer, je dois vous avertir que si vous faites partie de ces rares personnes qui connaissent l’internationalisation, vous allez trouver toute ma discussion un peu simplifiée. J’essaie vraiment de fixer une barre minimale ici afin que tout le monde puisse comprendre ce qui se passe et puisse écrire du code qui a une chance de fonctionner avec du texte dans n’importe quelle langue autre que le sous-ensemble de l’anglais qui n’inclut pas les mots avec des accents. Et je dois vous avertir que la gestion des caractères n’est qu’une petite partie de ce qu’il faut pour créer des logiciels qui fonctionnent à l’international, mais je ne peux écrire qu’à propos d’une chose à la fois, donc aujourd’hui ce sont les jeux de caractères.
Une perspective historique
Le moyen le plus simple de comprendre cela est d’aller chronologiquement.
Vous pensez probablement que je vais parler de jeux de caractères très anciens comme EBCDIC ici. Eh bien, je ne vais pas. EBCDIC n’est pas pertinent pour votre vie. Nous n’avons pas besoin de remonter aussi loin dans le temps.
Dans les temps semi-anciens, lorsque Unix était en train d’être inventé et que K&R écrivaient « The C Programming Language », tout était très simple. EBCDIC était en voie de disparition. Les seuls caractères qui comptaient étaient les bonnes vieilles lettres anglaises sans accent, et nous avions un code pour elles appelé ASCII qui pouvait représenter chaque caractère avec un nombre entre 32 et 127. L’espace était 32, la lettre « A » était 65, etc. Cela pouvait être stocké commodément en 7 bits. La plupart des ordinateurs de cette époque utilisaient des octets de 8 bits, donc non seulement vous pouviez stocker chaque caractère ASCII possible, mais vous aviez tout un bit de trop, que, si vous étiez méchant, vous pouviez utiliser pour vos propres objectifs sournois : les idiots de WordStar allumaient effectivement le bit élevé pour indiquer la dernière lettre d’un mot, condamnant WordStar au texte anglais uniquement. Les codes inférieurs à 32 étaient appelés non imprimables et étaient utilisés pour des caractères de contrôle, comme 7 qui faisait biper votre ordinateur et 12 qui faisait voler la page actuelle de papier hors de l’imprimante et en chargeait une nouvelle.
Et tout allait bien, en supposant que vous parliez anglais.
Puisque les octets ont de la place pour jusqu’à huit bits, beaucoup de gens ont commencé à penser : « bon sang, nous pouvons utiliser les codes 128-255 pour nos propres objectifs. » Le problème était que beaucoup de gens avaient cette idée en même temps, et ils avaient leurs propres idées sur ce qui devait aller où dans l’espace de 128 à 255.
L’IBM-PC avait quelque chose qui est devenu connu sous le nom de jeu de caractères OEM qui fournissait quelques caractères accentués pour les langues européennes et un tas de caractères de dessin de lignes… des barres horizontales, des barres verticales, des barres horizontales avec de petites franges pendantes sur le côté droit, etc., et vous pouviez utiliser ces caractères de dessin de lignes pour faire des boîtes et des lignes élégantes sur l’écran, que vous pouvez encore voir fonctionner sur l’ordinateur 8088 de votre pressing. En fait, dès que les gens ont commencé à acheter des PC en dehors de l’Amérique, toutes sortes de jeux de caractères OEM différents ont été imaginés, qui utilisaient tous les 128 caractères du haut pour leurs propres objectifs. Par exemple, sur certains PC, le code de caractère 130 s’affichait comme é, mais sur les ordinateurs vendus en Israël, c’était la lettre hébraïque Gimel (ג), donc lorsque les Américains envoyaient leurs CV en Israël, ils arrivaient comme des rsums. Dans de nombreux cas, comme le russe, il y avait beaucoup d’idées différentes sur ce qu’il fallait faire avec les caractères des 128 du haut, donc vous ne pouviez même pas échanger des documents russes de manière fiable.
Finalement, ce laisser-faire OEM a été codifié dans la norme ANSI. Dans la norme ANSI, tout le monde s’accordait sur ce qu’il fallait faire en dessous de 128, ce qui était à peu près la même chose qu’ASCII, mais il y avait beaucoup de façons différentes de gérer les caractères à partir de 128 et au-delà, en fonction de l’endroit où vous viviez. Ces différents systèmes étaient appelés pages de code. Par exemple, en Israël, DOS utilisait une page de code appelée 862, tandis que les utilisateurs grecs utilisaient 737. Ils étaient les mêmes en dessous de 128 mais différents à partir de 128, où résidaient toutes les lettres étranges. Les versions nationales de MS-DOS avaient des dizaines de ces pages de code, gérant tout, de l’anglais à l’islandais, et ils avaient même quelques pages de code « multilingues » qui pouvaient faire de l’espéranto et du galicien sur le même ordinateur ! Wow ! Mais obtenir, disons, l’hébreu et le grec sur le même ordinateur était une impossibilité complète à moins que vous n’écriviez votre propre programme personnalisé qui affichait tout en utilisant des graphiques bitmap, car l’hébreu et le grec nécessitaient des pages de code différentes avec des interprétations différentes des nombres élevés.
Pendant ce temps, en Asie, des choses encore plus folles se produisaient pour tenir compte du fait que les alphabets asiatiques ont des milliers de lettres, qui n’allaient jamais tenir dans 8 bits. Cela était généralement résolu par le système désordonné appelé DBCS, le « jeu de caractères double octet » dans lequel certaines lettres étaient stockées dans un octet et d’autres en prenaient deux. Il était facile d’avancer dans une chaîne, mais presque impossible de reculer. Les programmeurs étaient encouragés à ne pas utiliser s++ et s– pour avancer et reculer, mais plutôt à appeler des fonctions telles que AnsiNext et AnsiPrev de Windows qui savaient comment gérer tout le bazar.
Mais encore, la plupart des gens faisaient comme si un octet était un caractère et un caractère était de 8 bits et tant que vous ne déplaciez jamais une chaîne d’un ordinateur à un autre, ou ne parliez pas plus d’une langue, cela fonctionnerait toujours plus ou moins. Mais bien sûr, dès que l’Internet est apparu, il est devenu assez courant de déplacer des chaînes d’un ordinateur à un autre, et tout le désordre s’est effondré. Heureusement, Unicode avait été inventé.
Unicode
Unicode était un effort courageux pour créer un jeu de caractères unique qui incluait tous les systèmes d’écriture raisonnables de la planète et certains imaginaires comme le klingon aussi. Certaines personnes ont la fausse impression qu’Unicode est simplement un code de 16 bits où chaque caractère prend 16 bits et donc il y a 65 536 caractères possibles. Ce n’est pas, en fait, correct. C’est le mythe le plus courant à propos d’Unicode, donc si vous le pensiez, ne vous sentez pas mal.
En fait, Unicode a une manière différente de penser les caractères, et vous devez comprendre la manière Unicode de penser les choses sinon rien n’aura de sens.
Jusqu’à présent, nous avons supposé qu’une lettre correspond à quelques bits que vous pouvez stocker sur disque ou en mémoire :
A -> 0100 0001
Dans Unicode, une lettre correspond à quelque chose appelé un point de code qui est toujours juste un concept théorique. La manière dont ce point de code est représenté en mémoire ou sur disque est une toute autre histoire.
Dans Unicode, la lettre A est une idée platonicienne. Elle flotte juste dans le ciel :
A
Cette A platonicienne est différente de B, et différente de a, mais identique à A et A et A. L’idée que A dans une police Times New Roman est le même caractère que le A dans une police Helvetica, mais différent de « a » en minuscule, ne semble pas très controversée, mais dans certaines langues, il peut être controversé de déterminer ce qu’est une lettre. La lettre allemande ß est-elle une vraie lettre ou juste une manière élégante d’écrire ss ? Si la forme d’une lettre change à la fin du mot, est-ce une lettre différente ? L’hébreu dit oui, l’arabe dit non. Quoi qu’il en soit, les personnes intelligentes du consortium Unicode ont résolu cela pendant la dernière décennie ou plus, accompagnées de beaucoup de débats hautement politiques, et vous n’avez pas à vous en soucier. Ils ont déjà tout compris.
Chaque lettre platonicienne dans chaque alphabet se voit attribuer un numéro magique par le consortium Unicode qui est écrit ainsi : U+0639. Ce numéro magique est appelé un point de code. Le U+ signifie « Unicode » et les chiffres sont en hexadécimal. U+0639 est la lettre arabe Ain. La lettre anglaise A serait U+0041. Vous pouvez les trouver tous en utilisant l’utilitaire charmap sur Windows 2000/XP ou en visitant le site Web Unicode.
Il n’y a pas de véritable limite au nombre de lettres que Unicode peut définir et en fait, ils ont dépassé les 65 536, donc chaque lettre Unicode ne peut pas vraiment être compressée en deux octets, mais c’était un mythe de toute façon.
OK, disons que nous avons une chaîne :
Hello
qui, en Unicode, correspond à ces cinq points de code :
U+0048 U+0065 U+006C U+006C U+006F.
Juste un tas de points de code. Des chiffres, vraiment. Nous n’avons pas encore dit quoi que ce soit sur la façon de stocker cela en mémoire ou de le représenter dans un message électronique.
Encodages
C’est là que les encodages entrent en jeu.
La première idée pour l’encodage Unicode, qui a conduit au mythe des deux octets, était, hé, stockons simplement ces chiffres en deux octets chacun. Donc Hello devient
00 48 00 65 00 6C 00 6C 00 6F
Non ? Pas si vite ! Cela ne pourrait-il pas aussi être :
48 00 65 00 6C 00 6C 00 6F 00 ?
Eh bien, techniquement, oui, je crois que cela pourrait, et, en fait, les premiers implémenteurs voulaient pouvoir stocker leurs points de code Unicode en mode haut-de-gamme ou bas-de-gamme, selon ce qui était le plus rapide pour leur CPU particulier, et voilà, il y avait déjà deux façons de stocker Unicode. Donc, les gens ont été obligés de proposer la convention bizarre de stocker un FE FF au début de chaque chaîne Unicode ; cela s’appelle une marque d’ordre des octets Unicode et si vous échangez vos octets hauts et bas, cela ressemblera à un FF FE et la personne qui lit votre chaîne saura qu’elle doit échanger chaque autre octet. Ouf. Toutes les chaînes Unicode dans la nature n’ont pas une marque d’ordre des octets au début.
Pendant un temps, il semblait que cela pourrait être suffisant, mais les programmeurs se plaignaient. « Regardez tous ces zéros ! » disaient-ils, car ils étaient Américains et regardaient du texte en anglais qui utilisait rarement des points de code au-dessus de U+00FF. De plus, ils étaient des hippies libéraux en Californie qui voulaient conserver (ricanez). S’ils avaient été des Texans, ils n’auraient pas dérangé de doubler la quantité de stockage nécessaire pour les chaînes. Mais ces faibles Californiens ne pouvaient pas supporter l’idée de doubler la quantité de stockage nécessaire pour les chaînes, et de toute façon, il y avait déjà tous ces documents là dehors utilisant divers jeux de caractères ANSI et DBCS et qui va tous les convertir ? Moi ? Pour cette raison seule, la plupart des gens ont décidé d’ignorer Unicode pendant plusieurs années et entre-temps, les choses ont empiré.
Ainsi fut inventé le concept brillant de UTF-8. UTF-8 était un autre système pour stocker votre chaîne de points de code Unicode, ces nombres magiques U+, en mémoire en utilisant des octets de 8 bits. Dans UTF-8, chaque point de code de 0 à 127 est stocké dans un seul octet. Seuls les points de code 128 et au-dessus sont stockés en utilisant 2, 3, en fait, jusqu’à 6 octets.
Comment UTF-8 fonctionne
Cela a l’effet secondaire intéressant que le texte anglais ressemble exactement à ce qu’il était en UTF-8 qu’en ASCII, donc les Américains ne remarquent même rien de mal. Seul le reste du monde doit sauter à travers des cerceaux. Spécifiquement, Hello, qui était U+0048 U+0065 U+006C U+006C U+006F, sera stocké comme 48 65 6C 6C 6F, ce qui, regardez ! est le même qu’il était stocké en ASCII, et ANSI, et chaque jeu de caractères OEM sur la planète. Maintenant, si vous êtes assez audacieux pour utiliser des lettres accentuées ou des lettres grecques ou des lettres klingonnes, vous devrez utiliser plusieurs octets pour stocker un seul point de code, mais les Américains ne le remarqueront jamais. (UTF-8 a également la propriété intéressante que le vieux code de traitement de chaîne ignorant qui veut utiliser un seul octet 0 comme terminateur nul ne tronquera pas les chaînes).
Jusqu’à présent, je vous ai expliqué trois façons d’encoder Unicode. Les méthodes traditionnelles de le stocker en deux octets sont appelées UCS-2 (parce qu’il a deux octets) ou UTF-16 (parce qu’il a 16 bits), et vous devez toujours comprendre si c’est UCS-2 de haute-gamme ou UCS-2 de basse-gamme. Et il y a la nouvelle norme UTF-8 populaire qui a la belle propriété de fonctionner également respectablement si vous avez la heureuse coïncidence de texte anglais et de programmes débiles qui sont complètement inconscients qu’il existe autre chose qu’ASCII.
En fait, il y a en fait beaucoup d’autres façons d’encoder Unicode. Il y a quelque chose appelé UTF-7, qui est beaucoup comme UTF-8 mais garantit que le bit haut sera toujours zéro, donc si vous devez passer Unicode à travers un système de messagerie d’État policier draconien qui pense que 7 bits sont suffisants, merci, il peut encore se faufiler sans être touché. Il y a UCS-4, qui stocke chaque point de code en 4 octets, ce qui a la belle propriété que chaque point de code peut être stocké dans le même nombre d’octets, mais, bon sang, même les Texans ne seraient pas si audacieux pour gaspiller autant de mémoire.
Et en fait, maintenant que vous pensez aux choses en termes de lettres idéales platoniciennes qui sont représentées par des points de code Unicode, ces points de code Unicode peuvent être encodés dans n’importe quel ancien schéma d’encodage aussi ! Par exemple, vous pourriez encoder la chaîne Unicode pour Hello (U+0048 U+0065 U+006C U+006C U+006F) en ASCII, ou l’ancien encodage OEM grec, ou l’encodage ANSI hébreu, ou l’un des centaines d’encodages qui ont été inventés jusqu’à présent, avec une condition : certaines des lettres peuvent ne pas apparaître ! S’il n’y a pas d’équivalent pour le point de code Unicode que vous essayez de représenter dans l’encodage que vous essayez de représenter, vous obtenez généralement un petit point d’interrogation : ? ou, si vous êtes vraiment bon, une boîte. Qu’avez-vous obtenu ? -> �
Il y a des centaines d’encodages traditionnels qui peuvent seulement stocker certains points de code correctement et changer tous les autres points de code en points d’interrogation. Certains encodages populaires du texte anglais sont Windows-1252 (la norme Windows 9x pour les langues d’Europe occidentale) et ISO-8859-1, alias Latin-1 (également utile pour toute langue d’Europe occidentale). Mais essayez de stocker des lettres russes ou hébraïques dans ces encodages et vous obtenez un tas de points d’interrogation. UTF 7, 8, 16 et 32 ont tous la belle propriété de pouvoir stocker n’importe quel point de code correctement.
Le fait le plus important sur les encodages
Si vous oubliez complètement tout ce que je viens d’expliquer, veuillez vous souvenir d’un fait extrêmement important. Il n’a pas de sens d’avoir une chaîne sans savoir quel encodage elle utilise. Vous ne pouvez plus mettre la tête dans le sable et prétendre que le « texte brut » est ASCII.
Il n’y a pas de texte brut.
Si vous avez une chaîne, en mémoire, dans un fichier, ou dans un message électronique, vous devez savoir quel encodage elle utilise ou vous ne pouvez pas l’interpréter ou l’afficher correctement pour les utilisateurs.
Presque chaque problème stupide de « mon site Web ressemble à du charabia » ou « elle ne peut pas lire mes e-mails lorsque j’utilise des accents » se résume à un programmeur naïf qui ne comprenait pas le simple fait que si vous ne me dites pas si une chaîne particulière est encodée en UTF-8 ou ASCII ou ISO 8859-1 (Latin 1) ou Windows 1252 (Europe occidentale), vous ne pouvez tout simplement pas l’afficher correctement ou même savoir où elle se termine. Il y a plus d’une centaine d’encodages et au-dessus du point de code 127, toutes les paris sont ouverts.
Comment préservons-nous cette information sur l’encodage d’une chaîne ? Eh bien, il y a des façons standard de le faire. Pour un message électronique, vous devez avoir une chaîne dans l’en-tête sous la forme
Content-Type: text/plain; charset= »UTF-8″
Pour une page Web, l’idée originale était que le serveur Web renverrait un en-tête http Content-Type similaire avec la page Web elle-même – pas dans le HTML lui-même, mais comme un des en-têtes de réponse qui sont envoyés avant la page HTML.
Cela cause des problèmes. Supposons que vous ayez un grand serveur Web avec beaucoup de sites et des centaines de pages contribuées par beaucoup de gens dans beaucoup de langues différentes et toutes utilisant l’encodage que leur copie de Microsoft FrontPage jugeait bon de générer. Le serveur Web lui-même ne saurait pas vraiment quel encodage chaque fichier était écrit, donc il ne pouvait pas envoyer l’en-tête Content-Type.
Il serait pratique de pouvoir mettre le type de contenu du fichier HTML directement dans le fichier HTML lui-même, en utilisant une sorte de balise spéciale. Bien sûr, cela rendrait les puristes fous… comment pouvez-vous lire le fichier HTML jusqu’à ce que vous sachiez quel encodage il utilise ?! Heureusement, presque tous les encodages couramment utilisés font la même chose avec les caractères entre 32 et 127, donc vous pouvez toujours aller aussi loin dans la page HTML sans commencer à utiliser des lettres étranges :
<html>
<head>
<meta http-equiv= »Content-Type » content= »text/html; charset=utf-8″>
Mais cette balise meta doit vraiment être la toute première chose dans la section <head> car dès que le navigateur Web voit cette balise, il va arrêter de parser la page et recommencer après avoir réinterprété toute la page en utilisant l’encodage que vous avez spécifié.
Que font les navigateurs Web s’ils ne trouvent aucun Content-Type, que ce soit dans les en-têtes http ou la balise meta ? Internet Explorer fait en fait quelque chose de très intéressant : il essaie de deviner, en fonction de la fréquence à laquelle divers octets apparaissent dans le texte typique dans les encodages typiques de diverses langues, quelle langue et quel encodage ont été utilisés. Parce que les différentes anciennes pages de code de 8 bits avaient tendance à placer leurs lettres nationales dans différentes plages entre 128 et 255, et parce que chaque langue humaine a une distribution histogramme caractéristique de l’utilisation des lettres, cela a en fait une chance de fonctionner. C’est vraiment bizarre, mais cela semble fonctionner assez souvent pour que les rédacteurs naïfs de pages Web qui n’ont jamais su qu’ils avaient besoin d’un en-tête Content-Type regardent leur page dans un navigateur Web et cela semble correct, jusqu’au jour où ils écrivent quelque chose qui ne correspond pas exactement à la distribution de fréquence des lettres de leur langue maternelle, et Internet Explorer décide que c’est du coréen et l’affiche ainsi, prouvant, je pense, que la loi de Postel sur le fait d’être « conservateur dans ce que vous émettez et libéral dans ce que vous acceptez » n’est franchement pas un bon principe d’ingénierie. Quoi qu’il en soit, que fait le pauvre lecteur de ce site Web, écrit en bulgare mais qui semble être coréen (et même pas cohérent coréen) ? Il utilise le menu Affichage | Encodage et essaie plusieurs encodages différents (il y en a au moins une douzaine pour les langues d’Europe de l’Est) jusqu’à ce que l’image devienne plus claire. S’il savait faire cela, ce que la plupart des gens ne savent pas faire.
Pour la dernière version de CityDesk, le logiciel de gestion de site Web publié par ma société, nous avons décidé de tout faire en interne en UCS-2 (deux octets) Unicode, qui est ce que Visual Basic, COM et Windows NT/2000/XP utilisent comme type de chaîne natif. Dans le code C++, nous déclarons simplement les chaînes comme wchar_t (« large char ») au lieu de char et utilisons les fonctions wcs au lieu des fonctions str (par exemple wcscat et wcslen au lieu de strcat et strlen). Pour créer une chaîne littérale UCS-2 en code C, il suffit de mettre un L devant elle comme ceci : L »Hello ».
Lorsque CityDesk publie la page Web, il la convertit en encodage UTF-8, qui est bien pris en charge par les navigateurs Web depuis de nombreuses années. C’est ainsi que toutes les versions en 29 langues de Joel on Software sont encodées et je n’ai encore entendu parler d’aucune personne ayant eu des problèmes pour les visualiser.
Cet article devient assez long, et je ne peux pas couvrir tout ce qu’il y a à savoir sur les encodages de caractères et Unicode, mais j’espère que si vous avez lu jusqu’ici, vous en savez assez pour retourner programmer, en utilisant des antibiotiques au lieu de sangsues et de sorts, une tâche à laquelle je vais vous laisser maintenant.