UTF-8 vs UTF-16 vs Unicode — Guida completa alla codifica
Risposta breve a ciò che la maggior parte delle ricerche su utf-8 unicode encoding sta davvero chiedendo: Unicode e UTF-8 non sono la stessa cosa. Unicode è una tabella numerata che assegna un code point (un numero come U+1F600) a ogni carattere. UTF-8, UTF-16 e UTF-32 sono codifiche, tre modi diversi di trasformare quei code point in byte. UTF-8 va bene quasi sempre: a livello di byte coincide con ASCII per il testo inglese, scala fino a quattro byte per ogni emoji ed è imposta da JSON, HTML5 e dalla maggior parte dei protocolli moderni.
Questa guida è per lo sviluppatore che ci è già cascato: l’errore Incorrect string value di MySQL su un 😀, la sorpresa di "😀".length === 2 in JavaScript, il CSV che si apre bene in cat ma è illeggibile in Excel. Si parte dai code point e si passa per la meccanica dei byte di UTF-8, le surrogate pair, i BOM, il comportamento di default di nove linguaggi e otto trappole in produzione, fino alla matrice decisionale e alla FAQ.
Per verificare una sequenza di byte mentre leggi, incolla una stringa nel Codificatore e decodificatore Base64: il payload decodificato è lo stream di byte UTF-8 che questo articolo spiega.
Perché la codifica continua a darti grattacapi nel 2026
Tre scenari da bug tracker reali degli ultimi dodici mesi:
- MySQL rifiuta un emoji. Un utente invia
Hello 😀e il server restituisceIncorrect string value: '\xF0\x9F\x98\x80'. La tabella èutf8, lo sviluppatore pensa “ma è UTF-8, cosa c’è che non va?”, e la risposta è sepolta nella storia di MySQL (trattata nella sezione 7). - Un contatore di caratteri parte rotto. Un validatore per tweet da 280 caratteri usa
text.length, accetta un messaggio pieno di emoji e l’API lo rifiuta. Succede anche il contrario: un post valido viene rifiutato dal front end. Sintomo diagnosticato nella sezione 4. - Un HTML locale diventa “䏿–‡”. Uno sviluppatore salva un file in Windows-1252, lo apre in un browser che tira a indovinare UTF-8 e guarda il Mojibake fiorire. Questa è la storia del BOM e della dichiarazione di charset nella sezione 5, con paralleli alla Codifica e decodifica URL: guida online alla codifica percentuale dove lo stesso disallineamento byte-vs-carattere distrugge le query string.
Dopo questa guida saprai (a) distinguere Unicode da UTF-8 in una frase, (b) scegliere tra UTF-8, UTF-16 e UTF-32 per qualsiasi nuovo progetto, (c) scrivere codice che conta correttamente gli emoji in ogni linguaggio principale e (d) fare il debug di qualsiasi bug di charset partendo dal solo stream di byte. La codifica dei caratteri ha molti dettagli, ma la superficie pratica è piccola.
Cos’è Unicode? Code point vs caratteri vs glifi
Unicode è una tabella di caratteri che assegna un numero univoco, un code point come U+1F600, a ogni carattere. UTF-8, UTF-16 e UTF-32 sono codifiche che traducono quei code point in byte. Unicode di per sé non memorizza alcun byte; definisce soltanto la mappatura dal carattere astratto all’intero.
Altri tre termini confondono la conversazione, perché spesso si riferiscono allo stesso segno visibile:
Tre livelli da tenere separati
- Code point (
U+0041,U+1F600): l’intero che Unicode assegna. Lo spazio va daU+0000aU+10FFFF, circa 1,1 milioni di slot, di cui circa 150.000 attualmente assegnati. - Carattere (o carattere astratto): l’identità semantica, lettera latina maiuscola A, emoji faccina sorridente.
- Glifo: la forma visiva che un font rende. Un carattere ha molti glifi: una A serif, una A corsiva, una A scritta a mano. A Unicode i glifi non interessano.
- Grapheme cluster: ciò che un utente percepisce come un singolo “carattere”. Spesso un solo code point, a volte diversi. La lettera á può essere il code point
U+00E1oppure due code pointa + U+0301(accento acuto combinante); la guida ai limiti di caratteri e parole per piattaforma esplora come Twitter, SMS e SEO traccino questa linea in modo diverso.
In sintesi: code point → codifica → byte → rendering. Ogni freccia può rompersi in modo indipendente.
Notazione dei code point — U+XXXX e \uXXXX
Vedrai i code point scritti in varie forme. U+0041 è la notazione canonica Unicode: da quattro a sei cifre esadecimali, prefisso U+. Nel codice sorgente:
- JavaScript / JSON:
"A"(quattro cifre hex, solo BMP) e"\u{1F600}"(parentesi graffe ES6, qualsiasi code point). - Python:
"A"(4 cifre),"\U00000041"(8 cifre, U maiuscola),"\N{LATIN CAPITAL LETTER A}"(per nome). - Output di shell / git log / sed: vedi spesso byte UTF-8 grezzi come
\xc3\xa9peré. Quello non è un code point, è la forma codificata, ed è il tema della sezione 3.
I 17 piani — BMP e oltre
Unicode partiziona il proprio spazio di code point in 17 piani da 65.536 code point ciascuno: 17 × 2^16 = 1.114.112.
- Piano 0, il Basic Multilingual Plane (BMP):
U+0000–U+FFFF. Latino, ideogrammi CJK, cirillico, arabo, greco: quasi ogni sistema di scrittura che incontri nel testo legacy vive qui. - Piani 1-16, i piani supplementari:
U+10000–U+10FFFF. La maggior parte degli emoji (U+1F600e simili), caratteri CJK rari, sistemi di scrittura storici (geroglifici egizi, cuneiforme), notazione musicale.
Il confine BMP / piani supplementari a U+FFFF è il numero più importante di questo articolo. È dove UTF-16 smette di essere una code unit per carattere, dove UTF-8 passa da tre a quattro byte e dove la collation utf8 mal denominata di MySQL si arrende.
Verifica veloce con gli emoji
"a" → 1 codepoint U+0061 → 1 grapheme
"é" (NFC) → 1 codepoint U+00E9 → 1 grapheme
"é" (NFD) → 2 codepoints U+0065 U+0301 → 1 grapheme
"😀" → 1 codepoint U+1F600 (Plane 1) → 1 grapheme
"👨👩👧" → 5 codepoints (3 people + 2 ZWJ U+200D) → 1 grapheme
Guarda l’ultima riga. L’emoji famiglia è un carattere percepito dall’utente come uno solo, ma sono cinque code point uniti da Zero-Width Joiner. Ogni strato dello stack può contarlo in modo diverso, e la trappola 6 della sezione 7 è il bug report che questo disaccordo finisce per generare.
Meccanica della codifica UTF-8 — come funzionano 1-4 byte
UTF-8 codifica i code point Unicode in 1-4 byte. ASCII (U+0000–U+007F) usa 1 byte ed è identico a livello di byte ad ASCII. I code point più alti usano sequenze multi-byte in cui il primo byte segnala la lunghezza totale e ogni byte di continuazione inizia con il pattern di bit 10xxxxxx. Questo layout auto-descrittivo è il motivo per cui UTF-8 si è imposta sulle altre codifiche.
La tabella dei pattern di byte — UTF-8 in un solo diagramma
| Intervallo code point | Byte UTF-8 | Pattern di byte |
|---|---|---|
U+0000 – U+007F | 1 byte | 0xxxxxxx |
U+0080 – U+07FF | 2 byte | 110xxxxx 10xxxxxx |
U+0800 – U+FFFF | 3 byte | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000 – U+10FFFF | 4 byte | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Ogni x è un bit di dati preso dalla rappresentazione binaria del code point. Lo 0 / 110 / 1110 / 11110 iniziale dice al decoder quanti byte totali aspettarsi; il 10 iniziale marca ogni byte di continuazione. Questa ridondanza rende UTF-8 auto-sincronizzante: perdi un byte e puoi riprendere dal byte di inizio successivo invece di corrompere tutto a valle.
Esempio svolto — codifica di 中 (U+4E2D)
Il code point 0x4E2D ricade in U+0800–U+FFFF, quindi usiamo il template a 3 byte.
- Binario:
0x4E2D=0100 1110 0010 1101(16 bit). - Spezza in 4-6-6 per riempire gli slot
x:0100 / 111000 / 101101. - Sostituisci dentro
1110xxxx 10xxxxxx 10xxxxxx:11100100 10111000 10101101. - Hex:
0xE4 0xB8 0xAD.
Per questo 中 diventa %E4%B8%AD dopo la codifica URL: la percent-encoding avvolge ogni byte UTF-8 in %XX, non codifica direttamente il code point. La trappola 3 della sezione 7 ne descrive la catena.
Esempio svolto — codifica di 😀 (U+1F600)
Il code point 0x1F600 eccede il BMP, quindi usiamo il template a 4 byte.
- Binario:
0x1F600=0 0001 1111 0110 0000 0000(21 bit, con padding). - Spezza in 3-6-6-6:
000 / 011111 / 011000 / 000000. - Sostituisci dentro
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:11110000 10011111 10011000 10000000. - Hex:
0xF0 0x9F 0x98 0x80.
Questi quattro byte sono ciò su cui la collation utf8 di MySQL si strozza: alloca al massimo tre byte per carattere. La trappola 1 della sezione 7 ha la soluzione.
Perché UTF-8 si è imposta
- Compatibilità ASCII. Un file di puro testo ASCII è identico a livello di byte alla sua codifica UTF-8. Decenni di strumenti che precedono Unicode (
grep,awk, le classiche pipe della shell) continuano a funzionare per quel sottoinsieme. - Auto-sincronizzante. I byte di continuazione iniziano sempre con
10, che non collide mai con alcun byte di inizio. Perdi un byte in un trasferimento di rete e ti risincronizzi al successivo confine di carattere invece di propagare spazzatura a cascata. - Nessun byte order. UTF-8 è uno stream di byte, non di unità a 16 o 32 bit, quindi l’endianness è irrilevante. UTF-16 e UTF-32 hanno bisogno di un Byte Order Mark per dichiarare quale estremità viene prima; UTF-8 no (e di solito non dovrebbe; vedi sezione 5).
UTF-8 non valido — cosa proibisce la specifica
Un decoder rigoroso rifiuta queste sequenze di byte:
- Sequenze a 5 o 6 byte. Le prime RFC le ammettevano; la RFC 3629 (2003) ha limitato UTF-8 a 4 byte per allinearsi allo spazio Unicode a 21 bit.
- Codifiche overlong. Codificare
/come tre byte0xE0 0x80 0xAFinvece di un singolo byte0x2F. Un tempo fonte fertile di exploit di directory traversal nei validatori di path che decodificavano dopo la sanitizzazione. - Code point surrogati isolati (
U+D800–U+DFFF). Sono riservati a UTF-16 e non dovrebbero mai comparire in UTF-8. - Sequenze troncate. Un byte di inizio a 3 byte seguito da un solo byte di continuazione, comune quando l’input utente viene tagliato su un confine di byte nel mezzo di un carattere multi-byte.
Per vederlo concretamente, butta una stringa nel Codificatore e decodificatore Base64, codificala e poi decodificala come byte: l’array di byte tra encoder e decoder è lo stream UTF-8 che questa sezione descrive.
UTF-16 e surrogate pair — perché length mente in JavaScript
La ricerca più comune attorno a utf-8 vs utf-16 è in realtà “perché "😀".length vale 2 nel mio codice?” La risposta sono le surrogate pair, ed è una decisione degli anni ‘90 che JavaScript, Java, C# e Windows hanno tutti ereditato.
UTF-16 in un paragrafo
UTF-16 rappresenta Unicode usando code unit a 16 bit. I caratteri nel BMP (U+0000–U+FFFF) occupano esattamente una code unit. I caratteri nei piani supplementari (U+10000–U+10FFFF) occupano due code unit, chiamate surrogate pair: un high surrogate in U+D800–U+DBFF seguito da un low surrogate in U+DC00–U+DFFF. Quel blocco U+D800–U+DFFF è riservato permanentemente in Unicode, quindi nessun carattere reale ci vive. UTF-16 è il formato di stringa interno per JavaScript, Java, C# (.NET), le API kernel di Windows, NSString di Objective-C e Qt, tutti progettati quando 65.536 caratteri sembravano più che sufficienti.
La trappola di String.length
"a".length // 1 — BMP, single code unit
"é".length // 1 — BMP (U+00E9), single code unit
"中".length // 1 — BMP (U+4E2D), single code unit
"😀".length // 2 — supplementary plane (U+1F600), surrogate pair!
"a😀".length // 3 — one BMP + two surrogate units
String.prototype.length riporta il numero di code unit UTF-16, non il numero di caratteri. Qualsiasi cosa dal piano supplementare risulta come 2. La stessa trappola esiste in String.length() di Java e in string.Length di C#.
Contare correttamente i code point in JS
[..."😀"].length // 1 — spread iterator walks codepoints
Array.from("😀").length // 1 — Array.from also walks codepoints
"😀".match(/./gu).length // 1 — /u flag = unicode-aware regex
// "😀".charAt(0) returns the lone high surrogate (visually broken)
"😀".codePointAt(0) // 128512 — the full codepoint U+1F600
Lo spread operator e Array.from usano il protocollo di iterazione, che la specifica del linguaggio definisce come iterazione sui code point. L’accesso semplice per indice (str[0], charAt) restituisce ancora code unit e ti consegna metà di una surrogate pair sugli emoji.
Python — len() già fa la cosa giusta (quasi)
len("😀") # 1 — Python 3 strings are codepoint-indexed
len("👨👩👧") # 5 — codepoints (3 humans + 2 ZWJ), not graphemes
# Python 2 was byte-indexed by default — len("😀") returned 4
Python 3 memorizza le stringhe in una rappresentazione flessibile a 1, 2 o 4 byte (PEP 393) e le indicizza per code point. len("😀") è 1, ma comunque non è il conteggio dei grapheme: l’emoji famiglia continua a contare 5. Per contare i caratteri percepiti dall’utente serve una libreria di grapheme: Intl.Segmenter in JavaScript (Node 22+, tutti i browser attuali), grapheme o regex in Python, oppure Swift, il cui String.count è l’unico linguaggio mainstream che di default conta i grapheme.
UTF-16 vs UCS-2 — la migrazione silenziosa
Prima del 1996, Unicode prometteva di entrare in 16 bit e la codifica corrispondente era UCS-2, una mappatura fissa a 2 byte. Unicode 2.0 ha rotto quella promessa aggiungendo i piani supplementari. UTF-16 è la versione corretta che usa le surrogate pair. La specifica JavaScript cita ancora il vecchio vocabolario UCS-2 in vari punti, ed è il motivo per cui il linguaggio tollera surrogati isolati che dovrebbero essere illegali (le battute su “WTF-16” hanno una loro ragion d’essere). Le API della piattaforma web (DOM, fetch, TextEncoder) rifiutano i surrogati isolati perché non possono essere codificati in UTF-8 valido.
UTF-32, BOM e la questione del byte order
UTF-32 — quella semplice e sprecona
UTF-32 usa 4 byte fissi per code point. U+0041 è memorizzato come 0x00000041, U+1F600 come 0x0001F600. Il vantaggio è l’accesso casuale in tempo costante: l’n-esimo code point sta all’offset di byte 4n. Lo svantaggio è la dimensione: il testo ASCII puro si gonfia a quattro volte la sua occupazione UTF-8, e anche il testo CJK raddoppia. Quasi nessun sistema memorizza UTF-32 su disco. Internamente Python 3 sceglie 1, 2 o 4 byte per stringa in base al code point più alto; lo stack fontconfig di Linux usa UTF-32 per le sue tabelle di glifi in memoria.
Byte order — perché l’endianness conta per UTF-16 / UTF-32
UTF-8 è uno stream di byte singoli, quindi l’endianness non si applica. UTF-16 e UTF-32 operano su unità multi-byte, e CPU diverse non sono d’accordo su quale estremità di un numero venga prima.
U+0041 ('A') in UTF-16 BE → 00 41
U+0041 ('A') in UTF-16 LE → 41 00
Le CPU x86 e ARM sono little-endian; i più vecchi PowerPC e il “network byte order” sono big-endian. Quando scrivi un file UTF-16 devi scegliere uno dei due e dirlo al lettore, ed è proprio a questo che serve il BOM.
Il BOM — cos’è, quando usarlo
Un Byte Order Mark è un U+FEFF all’inizio di un file. Una volta codificato, annuncia sia la codifica sia (per UTF-16 / UTF-32) il byte order.
| Codifica | Byte del BOM |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16 BE | FE FF |
| UTF-16 LE | FF FE |
| UTF-32 BE | 00 00 FE FF |
| UTF-32 LE | FF FE 00 00 |
Il BOM utf-8 esiste, ma non porta informazioni di byte order perché UTF-8 non ha byte order. Il suo unico compito è dichiarare “questo file è UTF-8”: utile per strumenti che non hanno altro segnale, dannoso per strumenti che si aspettano che il file inizi con un magic number o una direttiva.
Matrice decisionale del BOM — devo aggiungerlo?
| Formato | BOM UTF-8 | BOM UTF-16 | BOM UTF-32 |
|---|---|---|---|
| HTML | No (rompe il rilevamento di <!doctype> nei vecchi parser) | — | — |
| JSON | No (la RFC 8259 lo vieta) | — | — |
| Sorgenti JavaScript / CSS | Evita (le vecchie versioni di Node e IE si strozzano) | — | — |
| CSV aperto in Excel | Sì (Excel legge UTF-8 senza BOM come ANSI e maciulla CJK) | — | — |
| XML | Opzionale (la dichiarazione XML indica già la codifica) | Obbligatorio | Obbligatorio |
Testo semplice .txt | Opzionale (Notepad di Windows ne aggiunge uno di default) | Obbligatorio | Obbligatorio |
La regola breve: togli il BOM UTF-8 da qualsiasi cosa servita sul web; aggiungilo ai CSV che vuoi aprire in Excel; lascia decidere al lettore per tutto il resto.
9 linguaggi affiancati — comportamento di codifica di default
Il lavoro cross-language è dove questa conoscenza ripaga. La stessa stringa "a😀é" produce una lunghezza diversa in ogni runtime che chiami dal tuo script Bash.
La tabella di comportamento cross-language
| Linguaggio | Codifica file sorgente | Memorizzazione stringa | Cosa conta length / len | Codifica I/O di default | Emoji a 4 byte sicuro? |
|---|---|---|---|---|---|
| JavaScript (V8 / SpiderMonkey) | UTF-8 | UTF-16 | code unit UTF-16 | UTF-8 (Node, Web) | Sì, ma .length === 2 |
| Python 3 | UTF-8 (PEP 3120) | dinamico 1 / 2 / 4 byte (PEP 393) | code point | UTF-8 (PEP 540 da 3.7) | Sì, len === 1 |
| Java | UTF-8 (default di javac) | UTF-16 | code unit UTF-16 | charset di piattaforma → UTF-8 (JEP 400, JDK 18+) | Sì, ma .length() === 2 |
| Go | UTF-8 | byte UTF-8 | byte (utf8.RuneCountInString per i code point) | UTF-8 | Sì, len(s) restituisce byte |
| Rust | UTF-8 | byte UTF-8 (invariante String) | .len() byte, .chars().count() code point | UTF-8 | Sì, esplicito |
| C# (.NET) | UTF-8 (default da .NET Core 3.0) | UTF-16 | code unit UTF-16 | UTF-8 (Encoding.Default da .NET 5) | Sì, ma .Length === 2 |
| Ruby | UTF-8 (da 2.0) | tag di codifica per-stringa | code point (.length) | UTF-8 | Sì, length === 1 |
| PHP | (nessuna codifica sorgente) | stringa di byte | byte (strlen); mb_strlen per i code point | dipende da default_charset | Sì, con la famiglia mb_* |
| MySQL | — | charset di colonna | byte (LENGTH), caratteri (CHAR_LENGTH) | variabili di sistema character_set_* | Solo con utf8mb4 |
Cosa ti dice davvero questa tabella
Tre filosofie, tre insiemi di bug:
- UTF-8 interno (Go, Rust, Ruby). La stringa nativa è in byte;
lengthè ben definita ma conta byte. Converti in code point o grapheme solo quando attraversi un confine di UI o di validazione. - UTF-16 interno (JavaScript, Java, C#). Eredità degli assunti degli anni ‘90:
lengthrestituisce code unit, una surrogate pair conta come 2. Usa l’iterazione consapevole dei code point per qualsiasi conteggio rivolto all’utente. - Indicizzato per code point (Python 3).
lendà i code point e sembra giusto, finché non incontri gli emoji con ZWJ: a quel punto ti serve comunque una libreria di grapheme.
PHP è il caso speciale. Le sue funzioni str* integrate operano tutte sui byte, trattando le sequenze UTF-8 come blob opachi. Ogni progetto non-ASCII deve usare la famiglia mb_* (multibyte), e i bug report anno dopo anno mostrano quanto spesso ci si scordi.
In pratica: tieni UTF-8 come formato di trasporto ovunque (file, body HTTP, colonne di database) e converti al tipo stringa nativo del runtime al confine. Questo è il “sandwich UTF-8” su cui torniamo nella sezione 8.
8 trappole di ingegneria del mondo reale
I pattern qui sotto saltano fuori in ogni code review su una codebase globalizzata.
Trappola 1: utf8 di MySQL è una bugia a 3 byte — passa a utf8mb4
Sintomo. INSERT INTO users (bio) VALUES ('Hello 😀'); restituisce Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio'.
Causa principale. Lo storico utf8 di MySQL è un alias per utf8mb3, una variante di UTF-8 limitata a tre byte per carattere. Qualsiasi code point sopra U+FFFF (ogni emoji, diverse migliaia di caratteri CJK rari, tutti i sistemi di scrittura storici) richiede quattro byte UTF-8 e viene rifiutato.
Soluzione.
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SET NAMES utf8mb4; -- client connection
# my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
MySQL 8.0 distribuisce ancora utf8 come alias di utf8mb3. utf8mb3 è deprecato ma non ancora rimosso. Usa utf8mb4 per ogni nuova colonna, ogni nuovo database, ogni nuova connessione: la variante legacy non offre alcun vantaggio.
Trappola 2: il fallback su Windows-1252 — il mistero del punto interrogativo
Sintomo. Un .txt esportato dal Notepad di un collega Windows mostra "smart quotes" e una lineetta em sulla sua macchina. Sul tuo server diventa ? o U+FFFD (carattere di sostituzione).
Causa principale. Il vecchio Notepad usa di default Windows-1252 (CP-1252), che codifica la virgoletta curva " come 0x93. Un decoder UTF-8 vede 0x93 come un byte di continuazione orfano (high bit 10) senza un byte di inizio che lo preceda e lo sostituisce con il carattere di sostituzione.
Soluzione. Rileva la codifica sorgente (file su Unix, chardet / charset-normalizer in Python, jschardet in Node), decodifica con il codec corretto, poi ri-codifica in UTF-8 prima di salvare. Standardizzare su UTF-8 al momento dell’ingestione elimina la ricorrenza.
Trappola 3: la percent-encoding URL ≠ UTF-8 (ma ci si appoggia)
Sintomo. fetch("/search?q=中文") restituisce 404 da un framework backend e funziona da un altro.
Causa principale. La percent-encoding opera sui byte, non sui code point. 中 è un code point ma sono tre byte UTF-8 (E4 B8 AD), ognuno codificato separatamente come %E4%B8%AD — nove caratteri ASCII nell’URL. Un framework che decodifica l’URL come Latin-1 invece di UTF-8 passerà all’handler i tre byte storpiati interpretati come tre caratteri singolo-byte.
Soluzione. Usa encodeURIComponent("中文") sul client (i browser fanno UTF-8 + percent-encode in un solo passo) e verifica che il framework server decodifichi gli URL come UTF-8 (tutti i framework moderni lo fanno di default). Per una conferma visiva, incolla 中文 nel Codificatore e decodificatore URL e guardalo diventare %E4%B8%AD%E6%96%87. L’intera catena è trattata nella Codifica e decodifica URL: guida online alla codifica percentuale.
Trappola 4: l’input di Base64 sono byte, ma hai scritto una stringa
Sintomo. btoa("你好") lancia InvalidCharacterError: The string contains characters outside the Latin1 range.
Causa principale. btoa è stato progettato nell’era ASCII / Latin-1. Si aspetta che ogni carattere di input entri in un singolo byte (code point 0-255). 你好 è in UTF-16 nel motore JS con code point U+4F60 U+597D, entrambi ben sopra 255.
Soluzione. Codifica prima in byte UTF-8, poi codifica in Base64 quei byte.
// Wrong:
btoa("你好"); // throws
// Correct:
const bytes = new TextEncoder().encode("你好");
// Uint8Array(6) [228, 189, 160, 229, 165, 189]
const b64 = btoa(String.fromCharCode(...bytes));
// "5L2g5aW9"
La storia più lunga si trova in Cos’è la codifica Base64 online? Una guida per principianti e nella Base64 avanzato online: MIME, data URL, prestazioni e sicurezza; il Codificatore e decodificatore Base64 fa la conversione in un solo passo e mostra lo stream di byte intermedio.
Trappola 5: String.length per la validazione (limiti di Twitter / SMS)
Sintomo. Un compositore da 280 caratteri valida lato client, poi l’API restituisce 422. O viceversa: un post perfettamente valido viene rifiutato dal client.
Causa principale. Il .length di JavaScript conta code unit UTF-16; un singolo emoji conta come 2. Twitter conta code point (emoji = 1). Il conteggio dei caratteri sbaglia in direzioni opposte a seconda di quale API ti fidi.
Soluzione. Usa [...text].length per il conteggio dei code point, oppure Intl.Segmenter per il vero conteggio dei grapheme (l’approccio di Bluesky / iMessage). Numeri piattaforma per piattaforma e i confini SMS GSM-7 vs UCS-2 sono catalogati nei Limiti di caratteri e parole 2026 — Twitter, SMS, SEO, Instagram.
Trappola 6: gli emoji famiglia con ZWJ contano come N code point, 1 grapheme
Sintomo. "👨👩👧".length === 8. Contare i code point dà 5. Per l’utente è un’immagine sola.
Causa principale. Lo Zero-Width Joiner (U+200D) incolla insieme più code point di emoji in un unico cluster renderizzato: tre emoji persona più due ZWJ fanno cinque code point, otto code unit UTF-16, un grapheme.
Soluzione.
const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
[...seg.segment("👨👩👧")].length; // 1
Intl.Segmenter è disponibile in Node 22+ e in ogni browser attuale. Per runtime più vecchi, il pacchetto grapheme-splitter implementa UAX #29.
Trappola 7: l’escape \uXXXX di JSON — i code point sopra U+FFFF richiedono una surrogate pair
Sintomo. Un payload JSON contiene "😀" e il decoder ricevente o lo rende correttamente come 😀 o mostra due quadratini, a seconda che capisca o no le surrogate pair in JSON.
Causa principale. L’escape \uXXXX di JSON accetta esattamente quattro cifre esadecimali, cioè una sola code unit UTF-16. Codificare 😀 (U+1F600) richiede la surrogate pair 😀. In JSON non esiste la sintassi a parentesi graffe \u{...}.
Soluzione. Accetta la surrogate pair (ogni parser conforme alla specifica la gestisce) oppure scrivi l’emoji letteralmente: JSON ammette qualsiasi carattere UTF-8 fuori dalla sintassi di escape, e la maggior parte dei parser moderni preferisce questa forma.
Trappola 8: i default di Content-Type: charset= HTTP non sono quelli che pensi
Sintomo. Una pagina HTML UTF-8 viene resa come Mojibake in un browser e correttamente in un altro.
Causa principale. La RFC 2616 imponeva originariamente ISO-8859-1 come default per le risposte text/* senza charset esplicito. La RFC 7231 (2014) ha rimosso quel default, lasciando ogni browser a tirare a indovinare. Alcuni fanno sniffing del contenuto, alcuni cadono di default su UTF-8, alcuni vanno sul locale di sistema.
Soluzione. Invia sempre Content-Type: text/html; charset=utf-8 dal server e <meta charset="utf-8"> nell’head del documento. Uno dei due basta; entrambi insieme proteggono dai vecchi proxy che strappano gli header.
Per ispezionare a livello di byte una qualsiasi di queste trappole, il Codificatore e decodificatore Base64 è lo strumento più rapido: incolla una stringa, codificala in Base64 e il payload decodificato è lo stream UTF-8.
Scegliere la codifica giusta — matrice decisionale
Per la domanda utf-8 vs utf-16, la risposta è quasi sempre UTF-8. La tabella qui sotto copre i casi limite.
Matrice decisionale
| Scenario | Scelta | Perché |
|---|---|---|
| Pagine web, JSON di API, file sorgente | UTF-8 (senza BOM) | Compatibile con ASCII, nessun byte order, più compatto per testo latino, la RFC 8259 impone UTF-8 per JSON |
| Storage CJK pesante (DB cinesi, dati di gioco giapponesi) | UTF-8 (utf8mb4) | UTF-8 usa 3 byte per carattere CJK contro i 2 di UTF-16, ma l’overhead ASCII di markup e chiavi JSON lascia comunque UTF-8 in vantaggio nella pratica — e l’ecosistema circostante è UTF-8 |
| API native Windows, codice Java / C# legacy | UTF-16 | Default della piattaforma; convertire a ogni chiamata API è un invito ai bug |
| Elaborazione di testo in memoria con accesso intensivo per indice | UTF-32 | Accesso ai code point in tempo costante; ne vale la pena solo per gli hot path dei parser |
| CSV aperto in Excel su Windows | UTF-8 con BOM | Excel legge UTF-8 senza BOM come ANSI e maciulla le intestazioni CJK |
| Nuovo progetto, nessun vincolo | UTF-8 (senza BOM) | L’ecosistema è ormai convergente su UTF-8 |
Due regole pratiche
- Default a UTF-8 ovunque, salvo che una piattaforma non imponga altro. W3C, IETF e Unicode Consortium sono tutti d’accordo.
- Converti al confine, non nel mezzo. Decodifica i byte nel tipo stringa nativo del tuo linguaggio in ingresso. Opera sulle stringhe, mai sui byte, nella logica di business. Ricodifica in UTF-8 in uscita. Questo “sandwich UTF-8” elimina l’intera categoria di bug di mojibake a metà pipeline.
Domande frequenti
UTF-8 è sempre retrocompatibile con ASCII?
Sì. Qualsiasi file ASCII valido è bit-identico alla propria rappresentazione UTF-8. I primi 128 code point (U+0000–U+007F) si codificano in un singolo byte con il bit alto a zero. Gli strumenti legacy che gestiscono solo ASCII (i primi grep, sed, le classiche pipe della shell) elaborano file UTF-8 di solo ASCII senza modifiche. I problemi iniziano solo quando entrano nello stream byte non-ASCII (bit alto impostato).
Devo usare il BOM UTF-8 nei miei file?
Default: no. HTML, JSON, JavaScript e CSS si rompono o danno warning in alcuni parser quando un BOM compare all’inizio. L’eccezione standard è il CSV destinato a Excel su Windows — senza BOM, Excel tira a indovinare ANSI e maciulla le intestazioni cinesi, giapponesi o coreane. Vedi la matrice decisionale del BOM nella sezione 5.
Perché in JavaScript "😀".length === 2?
Le stringhe JavaScript sono memorizzate come UTF-16, e .length restituisce il numero di code unit, non di caratteri. 😀 (U+1F600) vive nel piano supplementare e richiede una surrogate pair (due code unit a 16 bit), quindi .length è 2. Usa [..."😀"].length, Array.from("😀").length o Intl.Segmenter per un conteggio veritiero.
Qual è la differenza tra Unicode e UTF-8?
Unicode è la tabella di caratteri che assegna un code point (un numero come U+1F600) a ogni carattere. UTF-8 è una delle varie codifiche che traducono quei code point in byte (da 1 a 4 byte per code point). Unicode definisce cos’è un carattere; UTF-8 definisce come viaggia attraverso un file o una rete. UTF-16 e UTF-32 sono codifiche alternative della stessa tabella Unicode.
utf8mb4 è sempre più sicuro di utf8 in MySQL?
Sì per i nuovi progetti. Lo utf8 di MySQL è la variante mal denominata utf8mb3 limitata a 3 byte, che non può memorizzare alcun carattere sopra U+FFFF: ogni emoji, molti caratteri CJK rari, tutti i sistemi di scrittura storici. utf8mb4 è UTF-8 a 4 byte completi. L’unico avvertimento è la lunghezza degli indici: ogni carattere utf8mb4 può occupare 4 byte, quindi il limite legacy di indice InnoDB a 767 byte cappa gli indici unici a 191 caratteri (risolto da innodb_large_prefix in MySQL 5.7+ e di default in 8.0).
Come rilevo la codifica di un file sconosciuto?
Usa file su Unix, chardet o charset-normalizer in Python, o jschardet in Node. Nessuno è perfetto: tirano a indovinare statisticamente dalla distribuzione dei byte. Il rilevamento di UTF-8 è molto affidabile grazie al pattern dei byte di continuazione. Windows-1252, ISO-8859-1 e altre codifiche legacy a singolo byte sono quasi indistinguibili tra loro, quindi il rilevamento spesso si riduce a euristiche sulla lingua.
UTF-16 può rappresentare ogni carattere Unicode?
Sì. UTF-16 copre tutti i 1.114.112 code point. I caratteri del BMP (U+0000–U+FFFF) usano una code unit a 16 bit (2 byte), e i caratteri dei piani supplementari (U+10000–U+10FFFF) usano surrogate pair (4 byte). La copertura è identica a UTF-8 e UTF-32; differiscono solo il layout dei byte e la semantica di elaborazione. La scelta tra loro riguarda l’adattamento all’ecosistema, non le capacità.