1. Il numero di un carattere non è la sequenza di byte
Immagina di esportare un documento coreano e riaprirlo trovando simboli strani nel titolo. Non è cambiata la lingua di chi ha scritto: potrebbero non coincidere le regole con cui vengono letti i numeri salvati. Unicode attribuisce numeri, detti punti di codice, a caratteri e simboli di molte scritture. UTF-8 è una codifica che rappresenta i valori corrispondenti come sequenze di byte. La sillaba coreana ‘가’, per esempio, ha il punto di codice U+AC00. La notazione dopo U+ è esadecimale; non indica una posizione nel file o un indirizzo di memoria. Il numero non stabilisce neppure il disegno del carattere tipografico o i pixel sullo schermo. Lo stesso punto di codice può essere disegnato diversamente secondo il font e la composizione del testo. Supporto Unicode, salvataggio UTF-8 e installazione di font coreani sono collegati, ma vanno verificati separatamente. La spiegazione degli UTF di Unicode aiuta a distinguere caratteri e formato di memorizzazione.
In UTF-8, ‘가’ corrisponde ai tre byte esadecimali EA B0 80. La lettera latina A ha il punto di codice U+0041 e occupa un solo byte, 41. Un byte comprende otto bit: anche nello stesso documento i caratteri possono richiedere quantità diverse di byte. Neppure l’estensione .txt o .csv basta a identificare la codifica effettiva. UTF-8 rappresenta i valori scalari Unicode validi con uno-quattro byte; non si può quindi ottenere la dimensione esatta del file moltiplicando soltanto il numero di caratteri visibili. L’esempio di questa sillaba non si generalizza a tutti i segni coreani o a ogni lingua. La codifica trasforma informazioni testuali in byte, ma non è compressione né cifratura. Informazioni aggiuntive del formato, interruzioni di riga e compressione durante il trasporto possono cambiare la dimensione complessiva della stessa frase. Separare la conversione tra byte e caratteri definita dall’Encoding Standard del WHATWG dalla dimensione totale e dalla riservatezza permette di circoscrivere meglio il problema.
2. Byte, unità di codice e caratteri percepiti
La proprietà length di una stringa JavaScript conta unità di codice UTF-16. Non misura i byte di un file UTF-8 e non coincide necessariamente con i caratteri percepiti da una persona. La faccina sorridente ‘😀’ è un punto di codice, U+1F600, ma richiede due unità UTF-16: '😀'.length vale 2. La sua rappresentazione UTF-8 F0 9F 98 80 occupa quattro byte. Il numero decimale 128512 ottenuto con codePointAt(0) è lo stesso numero di U+1F600, non una quantità di caratteri o la dimensione del file. ‘가’ rappresenta invece un punto di codice, un’unità UTF-16 e tre byte UTF-8. I risultati sulla stessa stringa differiscono perché differiscono le unità. La documentazione MDN sulla lunghezza delle stringhe chiarisce l’unità di length. Questo valore non permette nemmeno di determinare da solo quanti byte effettivi di memoria usi internamente un motore JavaScript.
Anche contare uno per uno i punti di codice non corrisponde sempre al numero di caratteri visibili. Un carattere di base può ricevere segni combinanti e più punti possono comporre una sola emoji. Le regole Unicode di segmentazione descrivono i confini dei gruppi di grafemi estesi per gestire queste combinazioni. Nemmeno Array.from(text), che percorre una stringa per punti di codice, conta automaticamente tali gruppi come una sola unità. Nel browser, dopo aver verificato il supporto, si può considerare Intl.Segmenter con granularity impostata su 'grapheme'. Un gruppo di grafemi, però, non corrisponde necessariamente a un unico disegno del font, e le aspettative di modifica variano secondo la lingua. Un campo di presentazione deve offrire un conteggio comprensibile; memoria e comunicazioni possono richiedere limiti distinti in byte. Se l’interfaccia indica «20 caratteri» ma tronca soltanto unità UTF-16, può spezzare a metà un’emoji o un carattere combinato.
3. Separare salvataggio, trasmissione e visualizzazione
Il testo può risultare alterato quando la codifica usata per scriverlo differisce dalla decodifica applicata per leggerlo. Interpretare un file UTF-8 con un’altra codifica può mostrare caratteri sbagliati anche senza modificare il file. In HTML vanno confrontati il formato effettivamente salvato, il charset del Content-Type nella risposta HTTP e la dichiarazione meta charset all’inizio del documento. L’attributo HTML lang='ko' indica la lingua del contenuto: ha un ruolo diverso da charset, che riguarda l’interpretazione dei byte. La guida W3C alle dichiarazioni di codifica spiega che non basta cambiare la dichiarazione: occorre salvare davvero il file in UTF-8 e rendere coerenti server e documento. Anche la presenza di marcatori influenza l’ordine di priorità; un elemento meta non prevale sempre su tutte le altre informazioni. Se l’apertura diretta e la ricezione dal server danno risultati diversi, la fase di trasmissione fornisce un indizio importante.
I quadratini dovuti a un font privo dei glifi necessari possono avere un’origine diversa dalla decodifica errata. Se i dati sono corretti e manca soltanto una forma da disegnare, cambiare la codifica di salvataggio non risolve. Viceversa, un font nuovo non ripristina un carattere originale se i dati sono già stati interpretati come altri caratteri. Conserva una copia del file problematico e confronta nell’ordine la codifica scelta nell’editor, le dichiarazioni della risposta, i caratteri interpretati dal browser e il font usato. Non giudicare soltanto dall’aspetto: controlla anche quali punti di codice compaiono copiando lo stesso passaggio. Annota se il problema riguarda un solo browser o se anche altri editor leggono male il file. Salvare ripetutamente finché il risultato sembra normale può far perdere i byte originali. Se tutte le dichiarazioni sono corrette ma solo alcuni articoli sono alterati, verifica anche in quale passaggio il testo è cambiato prima dell’inserimento nel database.
4. Verificare gli stessi valori con TextEncoder e TextDecoder
Inizia un semplice esperimento con una breve stringa creata appositamente. TextEncoder converte una stringa JavaScript in un array di byte UTF-8. new TextEncoder().encode('가') produce EA B0 80 in esadecimale e la length dell’array è 3. Gli strumenti di sviluppo possono mostrare gli stessi valori come decimali 234, 176, 128: non sono dati diversi, ma cambia la base numerica. Il confronto con la normale lunghezza 1 della stringa rende evidente la differenza di unità. new TextDecoder('utf-8').decode(bytes) restituisce ‘가’, dove bytes è l’array ottenuto prima. L’encoder genera UTF-8; non è un convertitore universale con cui scegliere qualsiasi vecchia codifica di uscita. Quando si analizza un file reale bisogna partire dai byte originali conservati. Ricodificare in UTF-8 un testo già letto male sullo schermo non dimostra quali byte contenesse il primo file.
Passando 'utf-8' e { fatal: true } al costruttore TextDecoder, un ingresso non valido può provocare un errore invece di essere sostituito silenziosamente. Per esempio il singolo byte FF non è un ingresso UTF-8 valido e in questa modalità la decodifica fallisce. Nella modalità ordinaria di sostituzione può comparire il carattere U+FFFD. L’assenza di errori non significa però che sia stata identificata la codifica originale o verificata l’esattezza del contenuto. Alcune sequenze di byte sono valide in più codifiche e un testo già cambiato può essere stato risalvato come UTF-8 valido. Il test stabilisce se il decoder scelto accetta l’ingresso. Non controlla automaticamente provenienza del documento, corrispondenza all’originale o assenza di modifiche durante il percorso.
5. Normalizzare significa uniformare rappresentazioni equivalenti
Anche due stringhe lette correttamente possono avere sequenze interne di punti di codice differenti. ‘가’ può essere rappresentato dal solo U+AC00 oppure dallo jamo iniziale combinante U+1100 seguito dalla vocale U+1161. Gli ultimi due valori non sono semplicemente gli jamo di compatibilità comuni ‘ㄱ’ e ‘ㅏ’ affiancati: controlla anche i numeri. Le rappresentazioni appaiono normalmente come la stessa sillaba, ma possono differire in un confronto diretto di stringhe JavaScript. Applicare NFC a entrambi i lati secondo le regole Unicode di normalizzazione consente di confrontare nella stessa forma questo esempio di equivalenza canonica. NFD usa la decomposizione canonica. Qui la forma NFC occupa tre byte UTF-8, mentre i due jamo della forma NFD ne occupano sei: un aspetto uguale non garantisce una dimensione uguale. Questa differenza può spiegare una mancata corrispondenza tra ricerca e nome salvato apparentemente identici. Va distinta dalla lettura dei byte con una codifica sbagliata.
Anche la scelta della forma di normalizzazione dipende dallo scopo. NFC tratta l’equivalenza canonica; NFKC include la decomposizione di compatibilità e può cambiare più ampiamente forme come cifre a larghezza piena o cerchiate. Può essere utile per creare un valore di confronto nelle ricerche, ma certe differenze hanno significato in notazione matematica o documenti originali. Conviene conservare la fonte e stabilire quale trasformazione applicare a una copia per il confronto. Normalizzare non sostituisce confronti tra maiuscole e minuscole, ordinamento linguistico o ricerca di parole con uguale pronuncia. Convertire in massa nomi di file o identificativi di account può far convergere elementi diversi sullo stesso valore: esamina prima regole dei nomi e gestione delle collisioni del servizio. Salvare con lo stesso valore tutti i caratteri visivamente simili non è un obiettivo generale della normalizzazione.
6. Conservare l’originale e documentare le trasformazioni
Prima di sovrascrivere il file iniziale, conserva i byte e annota come è stato acquisito. Prova la decodifica prevista su una copia e verifica se il contenuto leggibile rimane invariato dopo un nuovo salvataggio. Il marcatore iniziale UTF-8 descritto nella spiegazione W3C del BOM è EF BB BF e non è obbligatorio. UTF-8 non richiede la scelta di un ordine dei byte; il marcatore non ha quindi lo stesso ruolo dell’indicazione d’ordine in UTF-16. Aggiungere o rimuovere un BOM è anche diverso dal convertire i byte del testo in un’altra codifica. Se valori illeggibili sono già stati sostituiti da caratteri di rimpiazzo o punti interrogativi e salvati, più valori originali possono essere confluiti in uno: non si può garantire un recupero automatico. Solo conoscendo codifica iniziale e cronologia delle conversioni si può valutare una possibile inversione. La normalizzazione non ripristina byte perduti.
Non concludere la verifica di una funzione multilingue con una sola riga inglese. Prepara brevi frasi con sillabe coreane, lettere latine con segni combinanti, un’emoji di un piano supplementare e scritture da destra a sinistra come arabo o ebraico; confronta inserimento, salvataggio, recupero ed esportazione. In ogni fase controlla aspetto, punti di codice, byte UTF-8 e unità di conteggio definita dal prodotto. La direzione di lettura non si risolve sostituendo UTF-8 con un’altra codifica: lingua e direzione dell’interfaccia vanno verificate separatamente. Quando modifichi limiti di inserimento o confronti di ricerca, conserva anche gli originali e spiega il nuovo criterio. Contatore del browser e limite di accettazione del server devono usare unità coerenti; se servono limiti diversi, presentali distintamente. Numeri e codice di questo articolo illustrano conversioni di brevi stringhe, non misurazioni del guasto di un servizio specifico. Esaminare in sequenza numero del carattere, byte, unità della stringa, visualizzazione e confronto permette di riprodurre i cambiamenti a ogni confine.