SmartKitio

Web

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

Gros plan sur des lettres imprimées et la typographie évoquant l’encodage du texte
Photo via Unsplash · Licence Unsplash

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

VueExemple pour é (U+00E9)
Point de codeU+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 JavaScript1 (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 ».

text
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.

javascript
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); // 1

Spread 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

VisuelPoints de code (conceptuel)Piège JS .length
é (précomposé)U+00E91 unité de code
é (e + accent aigu combinant)U+0065 U+03012 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
javascript
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 grapheme
javascript
const 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.

Pipeline texte sain
  1. Octets entrants

    Upload fichier, corps HTTP, BLOB base

  2. Détecter ou déclarer l’encodage

    BOM, charset Content-Type, métadonnées

  3. Décoder en Unicode

    Points de code en mémoire dans votre langage

  4. Traiter

    Recherche, validation, affichage — sur texte Unicode

  5. 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.

Règle empirique : normaliser en NFC pour stockage et comparaison sauf spec contraire (certains contextes crypto interdisent la normalisation).

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

  1. Déclarer charset=utf-8 sur les réponses HTML et JSON.
  2. Ne jamais supposer un octet par caractère pour les tailles de buffer.
  3. Ne pas utiliser seule la longueur de chaîne pour des limites visibles utilisateur.
  4. Utiliser Intl.Segmenter ou ICU pour des limites d’édition sensibles aux graphemèmes.
  5. Tester avec marques combinant, drapeaux emoji et séquences ZWJ dans les jeux QA.
  6. 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

  1. Unicode Standard — Fundamental concepts
  2. Unicode — UTF-8 FAQ
  3. MDN — UTF-16 characters, Unicode code points, and grapheme clusters
  4. MDN — Intl.Segmenter

Articles associés