Unicode et pourquoi le texte se casse
Le texte paraît simple jusqu’à ce qu’un nom client s’affiche « é », qu’un mot de passe avec emoji échoue la validation, ou qu’un compteur de tweet diverge de votre app. Ces bugs viennent rarement de « caractères bizarres » — ils viennent de mélanger trois idées : comment Unicode nomme les caractères, comment les encodages les stockent en octets, et ce que les humains perçoivent comme un caractère à l’écran. Un glyphe visible n’est pas un octet et pas toujours un index de chaîne JavaScript.
7 min de lecture · Mis à jour le 2026-10-04

Unicode assigne des numéros ; les encodages stockent des octets
Le standard Unicode assigne un point de code — un nombre, écrit comme U+0041 pour le A majuscule latin — à chaque caractère abstrait de son catalogue. Le catalogue n’est pas une police : les polices mappent points de code vers glyphes. UTF-8, UTF-16 et UTF-32 sont des schémas d’encodage qui transforment des séquences de points de code en octets pour fichiers, HTTP et bases. Les bugs apparaissent quand un logiciel traite des octets UTF-8 comme des caractères Latin-1, ou quand deux systèmes supposent des encodages différents sans les déclarer.
Même caractère, représentations différentes
| Vue | Exemple pour é (U+00E9) |
|---|---|
| Point de code | U+00E9 (caractère assigné unique) |
| Octets UTF-8 (hex) | C3 A9 (deux octets) |
| Unités de code UTF-16 (hex) | 00 E9 (une unité dans le BMP) |
| Longueur chaîne JavaScript | 1 (une unité UTF-16 pour é BMP) |
UTF-8 en pratique
UTF-8 est à largeur variable : lettres ASCII en un octet ; beaucoup de scripts européens en deux ; la plupart des emoji et scripts rares en quatre. C’est l’encodage dominant sur le web (HTTP, JSON, HTML). Quand un système dit « Unicode » en 2026, cela signifie en général « UTF-8 sur le fil sauf indication contraire ».
Character: é
UTF-8 bytes: 0xC3 0xA9
Mis-decoded as Latin-1: à © → often shown as "é" (mojibake)UTF-16 et chaînes JavaScript
Les chaînes JavaScript sont des séquences d’unités de code UTF-16, pas de points de code Unicode. Les caractères hors Plan multilingue de base (points au-dessus de U+FFFF) — beaucoup d’emoji, rares caractères han — occupent deux unités UTF-16 appelées paire surrogate. String.length compte donc des unités de code, pas des caractères visibles, et indexer avec str[i] peut couper une paire surrogate et produire des caractères invalides.
const smile = "😀";
console.log(smile.length); // 2 (surrogate pair)
console.log(smile[0] === smile[1]); // false
console.log([...smile].length); // 1 (iterates code points)
console.log(Array.from(smile).length); // 1Spread et Array.from itèrent par points de code Unicode. Pour des limites en production, préférez Intl.Segmenter ou une bibliothèque sensible aux graphemèmes pour compter ce que voient les utilisateurs.
Graphemèmes : ce que comptent les utilisateurs
Un caractère visible peut combiner plusieurs points de code. Une lettre de base plus un accent aigu combinant (e + ◌́) peut s’afficher comme é mais rester deux points de code. Les drapeaux emoji sont des paires de lettres indicateurs régionaux ; les modificateurs de teint de peau s’attachent aux emoji personnes ; les séquences ZWJ joignent des emoji famille. Unicode définit des frontières de graphemèmes étendus pour « ce qui ressemble à un caractère » en édition et déplacement du curseur.
Un glyphe, plusieurs points de code
| Visuel | Points de code (conceptuel) | Piège JS .length |
|---|---|---|
| é (précomposé) | U+00E9 | 1 unité de code |
| é (e + accent aigu combinant) | U+0065 U+0301 | 2 unités de code |
| 👨👩👧👦 (famille ZWJ) | Plusieurs points + ZWJ U+200D | .length ≫ 1 |
| 🇫🇷 (drapeau) | Deux indicateurs régionaux | .length === 4 |
| 1️⃣ (chiffre keycap) | U+0031 U+FE0F U+20E3 | .length === 3 |
| 👍🏽 (teint de peau) | U+1F44D U+1F3FD | .length === 4 |
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "e\u0301"; // e + combining acute
console.log(text.length); // 2
console.log([...segmenter.segment(text)].length); // 1 graphemeconst bio = "Hi 👨👩👧👦!"; // family ZWJ sequence
console.log(bio.length); // 11 UTF-16 code units
console.log([...bio].length); // 8 Unicode code points
console.log([...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(bio)].length); // 7 graphemes (letters, space, family emoji, !)
// A "max 10 characters" rule using .length rejects this bio even though it looks short.Mojibake et correction des erreurs d’encodage
Le mojibake (texte brouillé) signifie en général que des octets ont été interprétés avec le mauvais décodeur. Cas classique : octets UTF-8 lus comme Windows-1252 ou ISO-8859-1. Le correctif est de relire le flux d’octets d’origine avec l’encodage qui a servi à encoder, puis transcodifier en UTF-8 — pas de remplacer manuellement « é » par « é » dans une chaîne déjà corrompue.
Octets entrants
Upload fichier, corps HTTP, BLOB base
Détecter ou déclarer l’encodage
BOM, charset Content-Type, métadonnées
Décoder en Unicode
Points de code en mémoire dans votre langage
Traiter
Recherche, validation, affichage — sur texte Unicode
Encoder en sortie
Presque toujours UTF-8 pour API web
Convertir aux bords ; garder Unicode au milieu.
Emoji et limites de longueur
Les limites produit — « 140 caractères », segments SMS, VARCHAR base — choisissent une règle de comptage. Les comptes style Twitter utilisaient historiquement des unités de code pondérées ; le SMS utilise GSM-7 ou segments UCS-2 ; les bases peuvent compter des octets. Un message riche en emoji peut dépasser un budget d’octets tout en paraissant court visuellement. Documentez quelle métrique expose votre compteur : octets, unités UTF-16, points de code ou graphemèmes.
- Les limites de stockage (bytea PostgreSQL vs longueur char) se soucient souvent des octets en UTF-8.
- maxlength JavaScript sur les inputs compte souvent des unités UTF-16.
- Réglementations ou facturation peuvent compter graphemèmes ou pages de codes legacy.
- Base64 gonfle l’UTF-8 binaire — encoder du texte pour des URL n’est pas la même chose qu’un comptage de caractères.
Normalisation : quand l’égalité échoue
La même chaîne visuelle peut avoir plusieurs représentations Unicode. Les formes NFC et NFD choisissent des séquences précomposées vs décomposées canoniques. Mots de passe et noms d’utilisateur devraient être comparés après normalisation convenue — sinon vous bloquez des utilisateurs dont l’IME produit une séquence équivalente mais différente.
Case folding et locale
I pointillé et I sans point turcs, ß allemand et variantes de sigma grec montrent pourquoi toLowerCase() seul est une faible stratégie d’égalité. Case folding Unicode (case-fold) et collation sensible à la locale appartiennent aux index de recherche et politiques de noms d’utilisateur, pas aux bricolages ad hoc dans le code applicatif.
Surrogates et séquences invalides
UTF-8 n’autorise que des séquences d’octets bien formées ; les surrogates seuls sont illégaux en UTF-8 mais peuvent apparaître quand des données UTF-16 sont mal gérées. Les analyseurs qui substituent silencieusement le caractère de remplacement (U+FFFD) peuvent cacher une corruption — journaliser et rejeter l’entrée invalide aux frontières de confiance quand l’intégrité compte.
Checklist développeur
- Déclarer charset=utf-8 sur les réponses HTML et JSON.
- Ne jamais supposer un octet par caractère pour les tailles de buffer.
- Ne pas utiliser seule la longueur de chaîne pour des limites visibles utilisateur.
- Utiliser Intl.Segmenter ou ICU pour des limites d’édition sensibles aux graphemèmes.
- Tester avec marques combinant, drapeaux emoji et séquences ZWJ dans les jeux QA.
- Pour CSV legacy ou exports Windows, demander quel encodage a créé le fichier.
Les compteurs de caractères et mots SmartKitio doivent être lus avec leur mode de comptage documenté — comparez aux règles de votre plateforme avant d’imposer des limites en production.
Les données binaires ne sont pas du texte
Base64 et hex représentent des octets, pas des chaînes Unicode. Décoder du Base64 produit des octets qu’il faut encore décoder en UTF-8 (ou le charset correct) avant de traiter le résultat comme texte. Passer du binaire par des API string sans étape d’encodage explicite recrée du mojibake à une autre couche — courant dans pipelines d’upload et pièces jointes email.
À retenir
- Unicode nomme les caractères en points de code ; UTF-8 (et autres encodages) stockent ces points en octets.
- JavaScript .length compte des unités UTF-16 — paires surrogate et séquences combinant cassent les limites naïves.
- Les utilisateurs comptent des graphemèmes ; utilisez Intl.Segmenter ou ICU quand les règles produit disent « caractères ».
- Le mojibake est en général un mauvais décodage à une frontière — corrigez le pipeline, pas la chaîne au search-and-replace.
- Normalisez (souvent NFC) avant de comparer mots de passe et noms d’utilisateur venant d’IME différents.
Pour aller plus loin
Articles associés

Technologie
Pourquoi les logiciels utilisent différents formats de données
JSON, CSV, blobs binaires et images répondent à des contraintes différentes — l’échange n’est jamais gratuit.
7 min de lecture · 2026-10-04
Lire l’article
Programmation
Encodage et chiffrement : quelle différence ?
Encodage, chiffrement, hachage et compression résolvent des problèmes différents. Les confondre — surtout Base64 avec le secret — provoque de vraies failles de sécurité.
7 min de lecture · 2026-10-04
Lire l’article
Web
Traitement côté client et côté serveur
L’endroit où le calcul s’exécute façonne vie privée, latence, coût et confiance. Carte pratique du travail navigateur versus serveur — et ce que font vraiment les apps hybrides.
6 min de lecture · 2026-10-04
Lire l’article
Web
Les pourcentages dans la vie quotidienne
Pourboires, taxes, réductions, intérêts, stats et titres — formules, points de pourcentage vs variation relative, et exemples travaillés à vérifier.
8 min de lecture · 2026-10-04
Lire l’article
