[UNICODE] Base64 pour UTF-8 et Unicode
btoa('héllo') échoue. btoa('日本語') échoue. Ce n'est pas un bug — c'est une API des années 1990 qui se heurte au texte des années 2020. Voici la bonne façon d'encoder en Base64 de l'Unicode, octet par octet.
// LE PIÈGE
btoa() dans le navigateur semble innocent. Vous lui passez une chaîne, vous obtenez du Base64. Puis un jour un utilisateur soumet son nom — Renée — et votre code plante avec InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range.
Ce n'est pas une bizarrerie, et ce n'est pas la faute de JavaScript en soi. btoa() a été spécifié à une époque où « chaîne » et « octets » étaient considérés comme la même chose, tant que vous restiez en dessous de U+00FF. Unicode a conquis le monde, mais le contrat de btoa() est resté figé à Latin-1. Pour encoder en Base64 tout ce qui dépasse les caractères d'Europe occidentale, vous devez d'abord faire un détour par UTF-8.
// LE CORRECTIF EN 30 SECONDES
// Browser — encode any Unicode string to Base64
function encodeUtf8Base64(str) {
const bytes = new TextEncoder().encode(str); // UTF-8 bytes
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b); // Latin-1 wrapper
return btoa(bin); // now btoa can handle it
}
// Browser — decode Base64 back to a Unicode string
function decodeUtf8Base64(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes); // UTF-8 → string
}
// Usage
encodeUtf8Base64('Renée'); // 'UmVuw6ll'
encodeUtf8Base64('日本語'); // '5pel5pys6Kqe'
encodeUtf8Base64('👋 hello'); // '8J+RiyBoZWxsbw=='
decodeUtf8Base64('5pel5pys6Kqe'); // '日本語'
// POURQUOI ÇA MARCHE — UN BREF DÉTOUR PAR UTF-8
Les chaînes JavaScript sont des séquences d'unités de code UTF-16. Le caractère 日 est U+65E5, soit une seule unité de code 16 bits. L'emoji 👋 (U+1F44B) se situe hors du Plan Multilingue de Base et est représenté par une paire de substituts de deux unités de code 16 bits.
btoa() a besoin d'octets, pas d'unités de code. Et pas n'importe quels octets — il veut des octets Latin-1 dans la plage 0–255. Les unités de code UTF-16 dépassent régulièrement 255 (par exemple, 日 = 0x65E5 = 26085), donc btoa() lève une exception.
UTF-8 est un encodage 8 bits propre du jeu de caractères Unicode complet. Chaque caractère Unicode devient 1 à 4 octets, et chaque octet est garanti dans la plage 0–255. Si nous convertissons d'abord notre chaîne en octets UTF-8, puis que nous trompons btoa() en lui faisant voir ces octets comme une chaîne Latin-1 (un caractère par octet), tout s'aligne : btoa() voit des octets qu'il peut gérer, et le destinataire décode les mêmes octets en UTF-8.
// CE QUE FAIT TextEncoder SOUS LE CAPOT
TextEncoder est la manière normalisée de transformer une chaîne JavaScript en octets UTF-8. Il est dans tous les navigateurs depuis ~2017 et est une globale dans Node.js 11+.
Un aperçu rapide des séquences d'octets qu'il produit :
new TextEncoder().encode('A');
// Uint8Array [ 0x41 ] (1 byte)
new TextEncoder().encode('é');
// Uint8Array [ 0xC3, 0xA9 ] (2 bytes)
new TextEncoder().encode('日');
// Uint8Array [ 0xE6, 0x97, 0xA5 ] (3 bytes)
new TextEncoder().encode('👋');
// Uint8Array [ 0xF0, 0x9F, 0x91, 0x8B ] (4 bytes)
new TextEncoder().encode('Renée');
// Uint8Array [ 0x52, 0x65, 0x6E, 0xC3, 0xA9, 0x65 ] (6 bytes for 5 chars)
// NODE.JS : Buffer GÈRE L'UTF-8 PAR DÉFAUT
La classe Buffer de Node effectue la conversion UTF-8 en interne. Vous n'avez jamais besoin de TextEncoder ; passer une chaîne à Buffer.from() l'encode en UTF-8 sauf si vous choisissez explicitement un autre encodage. C'est pourquoi le Base64 côté serveur dans Node est une ligne :
// Encode
Buffer.from('日本語').toString('base64');
// '5pel5pys6Kqe'
// Decode
Buffer.from('5pel5pys6Kqe', 'base64').toString();
// '日本語'
// Explicit UTF-8 (the default, but clearer)
Buffer.from('Renée', 'utf8').toString('base64');
// 'UmVuw6ll'
// BASE64 UNICODE DANS CHAQUE LANGAGE
// Python
import base64
base64.b64encode('日本語'.encode('utf-8'))
# b'5pel5pys6Kqe'
base64.b64decode('5pel5pys6Kqe').decode('utf-8')
# '日本語'
// PHP
base64_encode('日本語'); // '5pel5pys6Kqe'
base64_decode('5pel5pys6Kqe'); // '日本語'
// (PHP strings are byte strings, so UTF-8 is handled implicitly
// if your source file is saved as UTF-8.)
// Java
import java.util.Base64;
import java.nio.charset.StandardCharsets;
Base64.getEncoder().encodeToString("日本語".getBytes(StandardCharsets.UTF_8));
// '5pel5pys6Kqe'
new String(Base64.getDecoder().decode("5pel5pys6Kqe"), StandardCharsets.UTF_8);
// '日本語'
// Go
import "encoding/base64"
base64.StdEncoding.EncodeToString([]byte("日本語"))
// '5pel5pys6Kqe'
b, _ := base64.StdEncoding.DecodeString("5pel5pys6Kqe")
string(b)
// '日本語'
// Ruby (source file must be UTF-8)
require 'base64'
Base64.strict_encode64('日本語') # '5pel5pys6Kqe'
Base64.decode64('5pel5pys6Kqe') # '日本語'
// Rust
use base64::{Engine, engine::general_purpose::STANDARD};
STANDARD.encode("日本語"); // "5pel5pys6Kqe"
STANDARD.decode("5pel5pys6Kqe")
.map(|b| String::from_utf8(b).unwrap())?; // "日本語"
// Shell (bash/zsh with GNU coreutils)
echo -n '日本語' | base64 # '5pel5pys6Kqe'
echo -n '5pel5pys6Kqe' | base64 -d # '日本語'
// ÉCHECS UNICODE COURANTS
-
>
Mojibake au décodage — vous avez décodé directement avec
atob()et le destinataire voit du charabia. Correctif : enveloppez avecTextDecoder. -
>
Erreurs de paires de substituts — votre code découpe des chaînes par nombre de caractères et atterrit au milieu d'une paire de substituts 👋. Correctif : utilisez
TextEncodersur la chaîne entière, pas caractère par caractère. -
>
Problèmes de BOM — votre texte source commence par un BOM UTF-8 (
EF BB BF). Il est encodé avec tout le reste, et le destinataire voit un\uFEFFparasite. Supprimez les BOM avant l'encodage. -
>
Métadonnées de jeu de caractères erronées — vous avez mis du Base64 d'octets UTF-8 dans un champ étiqueté
charset=iso-8859-1. Le décodeur utilise l'étiquette pour interpréter les octets. Alignez toujours les métadonnées avec l'encodage réel. -
>
Confusion Base64 URL-safe — vous avez utilisé base64url pour le stockage mais oublié de restaurer l'alphabet standard avant le décodage. Correctif : normalisez avec
replace(/-/g, '+').replace(/_/g, '/')et complétez à un multiple de 4. -
>
Encodage dépendant de la locale — en Python 2,
'日本語'.encode()était par défaut en ASCII et plantait. Dans tout runtime moderne, spécifiez toujoursutf-8explicitement.
// TEST D'ALLER-RETOUR — LA PREUVE DÉFINITIVE
La meilleure façon de vérifier que votre pipeline Base64 gère correctement l'Unicode est un test d'aller-retour. Encodez une chaîne connue, décodez-la, et comparez à l'original octet par octet.
// Vitest / Jest / any test runner
const samples = [
'hello',
'héllo',
'日本語',
'한국어',
'👋 Renée — 日本語 — مرحبا',
'\n\t\0 edge cases',
'𐀀 ancient greek (U+10000)',
];
for (const s of samples) {
const encoded = encodeUtf8Base64(s);
const decoded = decodeUtf8Base64(encoded);
expect(decoded).toBe(s);
expect([...new TextEncoder().encode(s)])
.toEqual([...new TextEncoder().encode(decoded)]);
}
// BASE64 DE BINAIRE vs BASE64 DE TEXTE
Une distinction subtile mais importante. Lorsque vous encodez un fichier PNG en Base64, l'entrée est déjà constituée d'octets — il n'y a pas de question Unicode à trancher. Passez simplement ces octets directement à l'encodeur.
Lorsque vous encodez une chaîne en Base64, l'entrée est une séquence de points de code Unicode, pas des octets. Vous devez vous engager sur un encodage d'octets (effectivement toujours UTF-8 en 2026) avant que l'encodeur ne puisse voir quoi que ce soit. La chaîne Base64 que vous produisez n'est valide que si le destinataire inverse le même encodage d'octets.
C'est la source de la plupart des tickets « pourquoi mon Base64 est-il faux » : deux systèmes ont encodé en Base64 la même chaîne logique en utilisant des encodages d'octets sous-jacents différents (l'un UTF-8, l'autre UTF-16, ou un Latin-1), et le décodeur du destinataire reproduit joyeusement les mauvais octets.
// DEVRAIS-JE TOUJOURS UTILISER UTF-8 ?
Oui, sauf si vous travaillez dans un système hérité qui spécifie autre chose. UTF-8 est :
• L'encodage dominant sur le web (> 97 % des sites web en 2026)
• Le choix par défaut dans la bibliothèque standard de tout langage de programmation moderne
• Compatible ASCII pour les 128 premiers points de code, il ne casse donc jamais l'anglais
• Bien défini et aller-retour compatible pour chaque caractère Unicode
• Le jeu de caractères recommandé par l'IETF pour les nouveaux protocoles (RFC 2277)
Les anciennes alternatives (UTF-16, UTF-32, Latin-1, GBK, Shift-JIS) apparaissent encore dans les bases de données héritées, les API Windows et certains protocoles de télécommunications. Si vos données proviennent de l'un de ces systèmes, convertissez en UTF-8 à la frontière, encodez l'UTF-8 en Base64, et documentez la décision d'encodage dans le format de la charge utile.
// LE REPLI SANS TextEncoder (NAVIGATEURS HÉRITÉS)
Si vous devez prendre en charge un très vieux navigateur sans TextEncoder (IE11 et antérieur), l'astuce classique consiste à utiliser encodeURIComponent comme encodeur UTF-8, puis à revenir via unescape à une chaîne d'octets. Ce modèle apparaît dans toutes les réponses StackOverflow sur le Base64 et l'Unicode de 2008 à 2020.
// Legacy browser fallback (pre-2017)
function encodeUtf8Base64Legacy(str) {
return btoa(unescape(encodeURIComponent(str)));
}
function decodeUtf8Base64Legacy(b64) {
return decodeURIComponent(escape(atob(b64)));
}
encodeUtf8Base64Legacy('日本語'); // '5pel5pys6Kqe'
decodeUtf8Base64Legacy('5pel5pys6Kqe'); // '日本語'
// Note: unescape() and escape() are deprecated.
// Only use this pattern if TextEncoder is genuinely unavailable.
// It also mishandles some edge-case surrogates.
// À RETENIR
- > Le Base64 encode des octets, pas des caractères. Choisissez d'abord un encodage d'octets — presque toujours UTF-8.
-
>
Dans le navigateur :
TextEncoder→String.fromCharCode→btoa→ un sens. -
>
Dans le navigateur :
atob→Uint8Array.from→TextDecoder→ l'autre sens. -
>
Dans Node.js :
Buffer.from(str).toString('base64')etBuffer.from(b64, 'base64').toString()— tous deux sont en UTF-8 par défaut. -
>
Dans Python / Go / Java : passez toujours un
'utf-8'explicite à.encode()/.getBytes(). - > Testez toujours en aller-retour avec de l'Unicode réel (emoji, CJC, arabe, marques combinantes) avant la mise en production.
// CONNEXE
• Qu'est-ce que le Base64 et comment fonctionne-t-il ? — le regroupement sur 6 bits qui sous-tend tout ce qui est ici.
• Base64 en JavaScript (navigateur + Node) — utilitaire multi-runtime complet.
• URL-safe vs Base64 standard — un choix orthogonal que vous devrez peut-être aussi faire.
• Essayez l'encodeur Base64 en direct — le même pipeline Unicode-safe, dans votre navigateur.