1. Nomor karakter berbeda dari byte yang disimpan
Bayangkan sebuah dokumen berbahasa Korea diekspor, lalu saat dibuka kembali judulnya menjadi karakter aneh. Bahasa orang yang mengetiknya tidak berubah; aturan pembacaan angka yang tersimpan mungkin tidak cocok. Unicode adalah standar yang memberikan nomor bernama titik kode kepada berbagai karakter dan simbol, sedangkan UTF-8 adalah pengodean yang menyatakan nilainya sebagai urutan byte. Misalnya, suku kata Hangul ‘가’ memiliki titik kode U+AC00. Bagian setelah U+ ditulis dalam heksadesimal, bukan menunjukkan urutan dalam berkas atau alamat memori. Nomor ini tidak menentukan bentuk huruf pada fon ataupun posisi piksel di layar. Titik kode yang sama dapat digambar berbeda menurut fon dan tata letaknya. Karena itu, dukungan Unicode, penyimpanan UTF-8, dan pemasangan fon Hangul saling berhubungan, tetapi harus diperiksa secara terpisah. Penjelasan UTF dari Unicode merupakan titik awal untuk membedakan karakter dari format penyimpanannya.
Dalam UTF-8, ‘가’ menjadi tiga byte heksadesimal EA B0 80. Sebaliknya, huruf A memiliki titik kode U+0041 dan hanya memakai satu byte, 41, dalam UTF-8. Byte merupakan satuan delapan bit, sehingga panjang byte tiap karakter dalam dokumen yang sama dapat berbeda. Ekstensi .txt atau .csv saja juga tidak mengungkap pengodean sebenarnya. UTF-8 menyatakan nilai skalar Unicode yang valid dengan satu hingga empat byte; ukuran berkas tidak dapat dihitung tepat hanya dengan mengalikan banyaknya karakter yang terlihat. Contoh suku kata Hangul ini pun tidak boleh diterapkan pada semua simbol terkait Hangul atau semua bahasa. Pengodean adalah aturan mengubah informasi karakter menjadi byte, bukan kompresi atau enkripsi. Ukuran keseluruhan kalimat yang sama dapat berbeda karena informasi tambahan format berkas, pergantian baris, atau kompresi saat pengiriman. Memisahkan konversi byte dan karakter dalam WHATWG Encoding Standard dari ukuran seluruh berkas dan fungsi kerahasiaan membantu mempersempit masalah.
2. Jumlah byte, unit kode, dan karakter yang terlihat
Di JavaScript, length sebuah string memberikan jumlah unit kode UTF-16. Angka itu bukan ukuran berkas UTF-8 dalam byte maupun jumlah karakter yang dirasakan manusia. Wajah tersenyum ‘😀’, misalnya, mempunyai satu titik kode U+1F600, tetapi menggunakan dua unit kode UTF-16 sehingga '😀'.length bernilai 2. Hasil pengodean UTF-8-nya adalah F0 9F 98 80, yaitu empat byte. Angka desimal 128512 dari codePointAt(0) adalah nomor yang sama dengan U+1F600, bukan jumlah karakter atau ukuran berkas. Menurut ketiga ukuran tersebut, ‘가’ adalah satu titik kode, satu unit kode UTF-16, dan tiga byte UTF-8. Angka untuk string yang sama berbeda karena satuannya berbeda. Penjelasan panjang string di MDN menyebutkan satuan length. Angka ini juga tidak cukup untuk memastikan berapa byte memori yang benar-benar digunakan mesin JavaScript.
Menghitung titik kode satu per satu pun tidak selalu sama dengan menghitung karakter yang dilihat pengguna. Karakter dasar dapat disertai tanda gabung, atau beberapa titik kode dapat membentuk satu emoji. Aturan segmentasi teks Unicode menjelaskan batas gugus grafem diperluas untuk menangani kelompok seperti ini. Menguraikan string per titik kode dengan Array.from(text) juga tidak otomatis menghitung gabungan beberapa titik kode sebagai satu kelompok. Untuk menghitungnya di peramban, periksa dukungan lingkungan sebelum mempertimbangkan Intl.Segmenter dengan granularity 'grapheme'. Namun, gugus grafem tidak dijamin selalu berpadanan satu banding satu dengan satu gambar yang dibuat fon; harapan pengeditan juga dapat berbeda antarbahasa. Kolom perkenalan sebaiknya memakai hitungan karakter yang dipahami pengguna, sedangkan batas penyimpanan atau komunikasi memakai jumlah byte secara terpisah. Antarmuka yang menulis ‘20 karakter’, tetapi memotong hanya berdasarkan unit kode UTF-16, dapat memutus emoji atau karakter gabungan di tengah.
3. Pisahkan penyimpanan, pengiriman, dan tampilan saat teks rusak
Karakter rusak dapat muncul ketika pengodean saat menulis byte berbeda dari aturan dekode saat membacanya. Berkas UTF-8 yang ditafsirkan sebagai pengodean lain dapat menampilkan karakter keliru walaupun berkas itu sendiri tidak berubah. Untuk HTML, periksa bersama-sama format penyimpanan sebenarnya, charset pada Content-Type respons HTTP, dan deklarasi meta charset di bagian awal dokumen. Atribut HTML lang='ko' menyatakan bahasa isinya; fungsinya berbeda dari charset yang memilih cara membaca byte Hangul. Panduan deklarasi pengodean W3C menjelaskan bahwa deklarasi UTF-8 harus disertai penyimpanan berkas dalam UTF-8, serta kesesuaian deklarasi server dan dokumen. Tanda pengodean juga dapat memengaruhi prioritas penentuan, sehingga satu meta tidak selalu mengalahkan informasi lain. Jika hasil membuka berkas langsung berbeda dari hasil melalui server, pengaturan tahap pengiriman merupakan petunjuk penting.
Fon yang tidak memiliki bentuk karakter yang dibutuhkan dapat menampilkan kotak, dan penyebabnya mungkin berbeda dari dekode yang salah. Jika data karakternya benar tetapi bentuknya tidak tersedia, mengubah pengodean penyimpanan tidak menyelesaikannya. Sebaliknya, mengganti fon tidak mengembalikan karakter asli yang sudah ditafsirkan sebagai karakter lain. Saat menyelidiki, simpan salinan berkas bermasalah dan bandingkan pengodean pilihan editor, deklarasi respons server, karakter yang ditafsirkan peramban, serta fon yang digunakan. Jangan memastikan penyebab hanya dari tampilannya; periksa titik kode saat bagian yang sama disalin. Catat apakah masalah hanya terjadi di peramban tertentu dan apakah editor lain juga salah membaca berkas yang sama, untuk memisahkan masalah tampilan dari perubahan data. Menyimpan berulang kali sampai karakter tampak benar dapat menghilangkan byte asli. Jika semua deklarasi halaman benar tetapi satu tulisan saja bermasalah, telusuri juga tahap perubahan sebelum isinya masuk ke basis data.
4. Memeriksa nilai yang sama dengan TextEncoder dan TextDecoder
Mulailah eksperimen sederhana dengan string pendek buatan sendiri. TextEncoder mengubah string JavaScript menjadi larik byte UTF-8. Hasil new TextEncoder().encode('가') bila dibaca secara heksadesimal adalah EA B0 80, dan length lariknya adalah 3. Alat pengembang mungkin menampilkan angka desimal 234, 176, 128. Itu bukan data berbeda, melainkan penulisan dengan basis bilangan berbeda. Bandingkan dengan length string biasa yang bernilai 1 untuk melihat perbedaan satuannya. Membaca larik tersebut dengan new TextDecoder('utf-8').decode(bytes) menghasilkan ‘가’; bytes di sini menunjuk larik yang baru dibuat. Enkoder ini menghasilkan UTF-8, bukan konverter serbaguna yang dapat menyimpan ke sembarang pengodean lama. Untuk menyelidiki berkas nyata, gunakan byte asli yang terlebih dahulu diperoleh. Mengodekan ulang kalimat layar yang sudah salah dibaca menjadi UTF-8 tidak membuktikan byte berkas awal.
Memberikan 'utf-8' dan { fatal: true } kepada konstruktor TextDecoder membuat masukan yang melanggar aturan diperlakukan sebagai kesalahan, alih-alih diganti diam-diam. Sebagai contoh, satu byte FF bukan masukan UTF-8 yang valid sehingga dekode dalam mode ini gagal. Dalam mode penggantian bawaan, bagian yang salah dapat menjadi karakter pengganti U+FFFD. Namun, tidak adanya kesalahan bukan bukti bahwa pengodean asli sudah ditemukan atau isi sudah dipastikan benar. Ada byte yang beririsan pada beberapa pengodean, dan hasil yang sudah berubah menjadi karakter lain bisa saja disimpan kembali sebagai UTF-8 yang valid. Pemeriksaan ini memastikan apakah dekoder yang dipilih dapat menerima masukan. Pemeriksaan tersebut tidak otomatis menguji sumber dokumen, kesesuaiannya dengan teks asli, ataupun apakah berkas diubah di tengah perjalanan.
5. Normalisasi adalah proses terpisah untuk menyelaraskan representasi
Dua string yang terbaca dengan benar pun dapat memiliki urutan titik kode berbeda. ‘가’ dapat dinyatakan sebagai satu U+AC00 atau sebagai urutan jamo awal penggabung U+1100 dan jamo vokal U+1161. Dua nilai terakhir berbeda dari contoh yang sekadar menjajarkan jamo kompatibilitas sehari-hari ‘ㄱ’ dan ‘ㅏ’, sehingga nomornya perlu ikut diperiksa. Kedua representasi biasanya digambar sebagai suku kata yang sama, tetapi perbandingan string mentah JavaScript dapat menganggapnya tidak sama. Menerapkan NFC menurut aturan normalisasi Unicode pada keduanya menyamakan representasi contoh yang setara secara kanonis ini untuk dibandingkan. NFD menggunakan dekomposisi kanonis. Pada contoh ini, UTF-8 bentuk NFC memiliki tiga byte, sedangkan dua jamo bentuk NFD memiliki enam byte; tampilan yang sama belum tentu berukuran penyimpanan sama. Jika kata pencarian dan nama tersimpan tampak sama tetapi tidak cocok, selidiki perbedaan representasi ini, terpisah dari kerusakan karakter akibat salah menafsirkan byte.
Pilihan bentuk normalisasi juga bergantung pada tujuan. NFC menangani kesetaraan kanonis, sedangkan NFKC mencakup dekomposisi kompatibilitas yang dapat mengubah representasi masukan lebih luas, termasuk angka lebar penuh atau angka dalam lingkaran yang semula dibedakan. Ini dapat membantu membuat nilai pembanding untuk pencarian, tetapi perbedaan tersebut mungkin bermakna dalam notasi matematika atau bahan asli. Karena itu, berguna untuk mempertahankan nilai asli lalu menentukan bentuk yang diterapkan pada salinan khusus perbandingan. Normalisasi tidak menggantikan perbandingan huruf besar-kecil, pengurutan menurut bahasa, atau pencarian kata yang sama bunyinya. Mengonversi massal nama berkas atau pengenal akun yang ada dapat menyatukan entri berbeda ke nilai pembanding sama, sehingga aturan penamaan layanan dan penanganan benturan harus ditinjau terlebih dahulu. Menyimpan semua karakter yang tampak mirip sebagai nilai yang sama bukan tujuan umum normalisasi.
6. Pertahankan teks asli dan catat batas setiap konversi
Sebelum menimpa berkas awal ketika mencari penyebab kerusakan, simpan byte asli dan catatan asal perolehannya. Cobalah membaca salinan dengan pengodean yang diperkirakan, lalu periksa apakah isi tetap sama setelah hasil yang tampak benar disimpan kembali. Penanda awal UTF-8 dalam penjelasan BOM W3C adalah EF BB BF, dan byte tersebut tidak wajib ada. UTF-8 tidak memerlukan pilihan urutan byte, sehingga penanda ini tidak boleh dianggap mempunyai fungsi yang sama dengan pemilihan urutan byte pada UTF-16. Menambah atau menghapus BOM juga berbeda dari mengonversi byte isi ke pengodean lain. Jika nilai yang tak terbaca telah disimpan sebagai karakter pengganti atau tanda tanya, banyak nilai awal mungkin menyatu menjadi satu; pemulihan otomatis tidak dapat dijamin. Kemungkinan membalikkan perubahan baru dapat dinilai setelah pengodean asli dan riwayat konversinya diketahui. Normalisasi bukan alat untuk mengembalikan byte yang hilang.
Jangan mengakhiri pengujian fitur multibahasa hanya dengan satu baris bahasa Inggris. Sertakan suku kata Hangul, huruf Latin bertanda gabung, emoji bidang tambahan, dan tulisan kanan-ke-kiri seperti Arab atau Ibrani dalam kumpulan kalimat pendek yang sama, lalu bandingkan masukan, penyimpanan, pembacaan ulang, dan ekspor. Pada setiap tahap, periksa tampilan, titik kode, jumlah byte UTF-8, dan standar hitungan karakter yang ditentukan produk. Arah penulisan tidak diselesaikan dengan mengganti UTF-8 menjadi pengodean lain; bahasa dan arah antarmuka juga harus diperiksa tersendiri. Saat mengubah batas masukan atau perbandingan pencarian, pertahankan teks asli dan jelaskan kriteria yang berubah. Pastikan angka petunjuk peramban dan batas penerimaan server menggunakan satuan sama; jika batasnya perlu berbeda, tampilkan masing-masing. Angka dan kode dalam tulisan ini memperagakan konversi string pendek, bukan hasil pengukuran gangguan layanan tertentu. Dengan memeriksa nomor karakter, byte penyimpanan, satuan string, serta aturan tampilan dan perbandingan secara berurutan, perubahan pada setiap batas dapat direproduksi.