Skip to content
Torna al blog
Tutorial

UTF-8 vs UTF-16 vs Unicode — Guida completa alla codifica

UTF-8, UTF-16 e UTF-32 per sviluppatori online: code point, surrogate pair, BOM, trappole utf8mb4 di MySQL e inganni di length JS. Scegli la codifica giusta.

12 min di lettura

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:

  1. MySQL rifiuta un emoji. Un utente invia Hello 😀 e il server restituisce Incorrect 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).
  2. 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.
  3. 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 da U+0000 a U+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+00E1 oppure due code point a + 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\xa9 per é. 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+0000U+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+10000U+10FFFF. La maggior parte degli emoji (U+1F600 e 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+0000U+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 pointByte UTF-8Pattern di byte
U+0000U+007F1 byte0xxxxxxx
U+0080U+07FF2 byte110xxxxx 10xxxxxx
U+0800U+FFFF3 byte1110xxxx 10xxxxxx 10xxxxxx
U+10000U+10FFFF4 byte11110xxx 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+0800U+FFFF, quindi usiamo il template a 3 byte.

  1. Binario: 0x4E2D = 0100 1110 0010 1101 (16 bit).
  2. Spezza in 4-6-6 per riempire gli slot x: 0100 / 111000 / 101101.
  3. Sostituisci dentro 1110xxxx 10xxxxxx 10xxxxxx: 11100100 10111000 10101101.
  4. 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.

  1. Binario: 0x1F600 = 0 0001 1111 0110 0000 0000 (21 bit, con padding).
  2. Spezza in 3-6-6-6: 000 / 011111 / 011000 / 000000.
  3. Sostituisci dentro 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx: 11110000 10011111 10011000 10000000.
  4. 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

  1. 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.
  2. 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.
  3. 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 byte 0xE0 0x80 0xAF invece di un singolo byte 0x2F. Un tempo fonte fertile di exploit di directory traversal nei validatori di path che decodificavano dopo la sanitizzazione.
  • Code point surrogati isolati (U+D800U+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+0000U+FFFF) occupano esattamente una code unit. I caratteri nei piani supplementari (U+10000U+10FFFF) occupano due code unit, chiamate surrogate pair: un high surrogate in U+D800U+DBFF seguito da un low surrogate in U+DC00U+DFFF. Quel blocco U+D800U+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.

CodificaByte del BOM
UTF-8EF BB BF
UTF-16 BEFE FF
UTF-16 LEFF FE
UTF-32 BE00 00 FE FF
UTF-32 LEFF 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?

FormatoBOM UTF-8BOM UTF-16BOM UTF-32
HTMLNo (rompe il rilevamento di <!doctype> nei vecchi parser)
JSONNo (la RFC 8259 lo vieta)
Sorgenti JavaScript / CSSEvita (le vecchie versioni di Node e IE si strozzano)
CSV aperto in ExcelSì (Excel legge UTF-8 senza BOM come ANSI e maciulla CJK)
XMLOpzionale (la dichiarazione XML indica già la codifica)ObbligatorioObbligatorio
Testo semplice .txtOpzionale (Notepad di Windows ne aggiunge uno di default)ObbligatorioObbligatorio

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

LinguaggioCodifica file sorgenteMemorizzazione stringaCosa conta length / lenCodifica I/O di defaultEmoji a 4 byte sicuro?
JavaScript (V8 / SpiderMonkey)UTF-8UTF-16code unit UTF-16UTF-8 (Node, Web)Sì, ma .length === 2
Python 3UTF-8 (PEP 3120)dinamico 1 / 2 / 4 byte (PEP 393)code pointUTF-8 (PEP 540 da 3.7)Sì, len === 1
JavaUTF-8 (default di javac)UTF-16code unit UTF-16charset di piattaforma → UTF-8 (JEP 400, JDK 18+)Sì, ma .length() === 2
GoUTF-8byte UTF-8byte (utf8.RuneCountInString per i code point)UTF-8Sì, len(s) restituisce byte
RustUTF-8byte UTF-8 (invariante String).len() byte, .chars().count() code pointUTF-8Sì, esplicito
C# (.NET)UTF-8 (default da .NET Core 3.0)UTF-16code unit UTF-16UTF-8 (Encoding.Default da .NET 5)Sì, ma .Length === 2
RubyUTF-8 (da 2.0)tag di codifica per-stringacode point (.length)UTF-8Sì, length === 1
PHP(nessuna codifica sorgente)stringa di bytebyte (strlen); mb_strlen per i code pointdipende da default_charsetSì, con la famiglia mb_*
MySQLcharset di colonnabyte (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: length restituisce 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). len dà 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

ScenarioSceltaPerché
Pagine web, JSON di API, file sorgenteUTF-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# legacyUTF-16Default della piattaforma; convertire a ogni chiamata API è un invito ai bug
Elaborazione di testo in memoria con accesso intensivo per indiceUTF-32Accesso ai code point in tempo costante; ne vale la pena solo per gli hot path dei parser
CSV aperto in Excel su WindowsUTF-8 con BOMExcel legge UTF-8 senza BOM come ANSI e maciulla le intestazioni CJK
Nuovo progetto, nessun vincoloUTF-8 (senza BOM)L’ecosistema è ormai convergente su UTF-8

Due regole pratiche

  1. Default a UTF-8 ovunque, salvo che una piattaforma non imponga altro. W3C, IETF e Unicode Consortium sono tutti d’accordo.
  2. 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+0000U+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+0000U+FFFF) usano una code unit a 16 bit (2 byte), e i caratteri dei piani supplementari (U+10000U+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à.

Tag: unicode utf-8 utf-16 character-encoding surrogate-pair encoding

Articoli correlati

Vedi tutti gli articoli