Character Encoding & Unicode — Web Fundamentals

Pernah melihat teks Café padahal harusnya Café? Atau emoji yang jadi ??? Atau "😀".length mengembalikan 2 di JavaScript? Semua itu gara-gara character…

Pernah melihat teks Café padahal harusnya Café? Atau emoji yang jadi ??? Atau "😀".length mengembalikan 2 di JavaScript? Semua itu gara-gara character encoding — cara komputer mengubah huruf menjadi byte (dan sebaliknya).

Sejarah singkat encoding:

Era Encoding Karakter didukung
1960an ASCII 128 karakter (A-Z, a-z, 0-9, simbol dasar) — 7 bit
1980an Latin-1 (ISO-8859-1) 256 karakter (ASCII + huruf Eropa Barat: é, ñ, ü) — 8 bit
1990an Kode halaman nasional Windows-1252, Shift-JIS, GB2312 (Cina) — kacau untuk web internasional
1991+ Unicode Lebih dari 149.000 karakter semua bahasa + emoji

Unicode = standar universal:

Unicode memberi setiap karakter sebuah code point — angka unik yang ditulis U+XXXX:

Unicode hanya mendefinisikan peta angka ke karakter. Bagaimana angka itu disimpan sebagai byte adalah urusan encoding: UTF-8, UTF-16, atau UTF-32.

UTF-8 — juara web:

UTF-8 adalah encoding variable-length: 1 sampai 4 byte per karakter.

Range code point Byte
U+0000 – U+007F (ASCII) 1 byte
U+0080 – U+07FF 2 byte
U+0800 – U+FFFF 3 byte
U+10000 – U+10FFFF 4 byte
"A"  (U+0041)   → 0x41           (1 byte)
"é"  (U+00E9)   → 0xC3 0xA9      (2 byte)
"中" (U+4E2D)   → 0xE4 0xB8 0xAD (3 byte)
"😀" (U+1F600)  → 0xF0 0x9F 0x98 0x80 (4 byte)

Keunggulan UTF-8:

UTF-16 — dipakai internal JavaScript, Java, Windows:

UTF-16 pakai unit 2 byte (16 bit). Karakter yang masuk 16 bit (BMP) = 1 unit. Yang di luar BMP (emoji!) butuh surrogate pair = 2 unit.

"A"  → 1 unit  (2 byte total)
"中" → 1 unit  (2 byte total)
"😀" → 2 unit  (4 byte total, surrogate pair)

Mengapa "😀".length === 2 di JavaScript?

JavaScript string adalah urutan UTF-16 code unit, bukan code point. length mengembalikan jumlah unit.

"A".length        // 1
"中".length       // 1
"😀".length       // 2  ← surrogate pair
"👨‍👩‍👧".length   // 8  ← emoji keluarga = banyak surrogate + zero-width joiner

Cara menghitung karakter sebenarnya:

[..."😀"].length                    // 1 (array iterator = code point)
[..."👨‍👩‍👧"].length                  // 5
Array.from("😀").length             // 1
new Intl.Segmenter().segment("👨‍👩‍👧")  // handles grapheme clusters

Code point vs code unit vs byte — jangan bingung:

Untuk 😀 (U+1F600):

Percent encoding (URL encoding):

URL hanya boleh mengandung ASCII "aman". Karakter lain harus di-encode:

Spasi     → %20
é         → %C3%A9         (byte UTF-8 di-encode)
中        → %E4%B8%AD
😀        → %F0%9F%98%80
?, &, =   → %3F, %26, %3D  (punya makna khusus di URL)
encodeURIComponent("hello world");   // "hello%20world"
encodeURIComponent("café");          // "caf%C3%A9"
encodeURIComponent("😀");            // "%F0%9F%98%80"

decodeURIComponent("%F0%9F%98%80");  // "😀"

encodeURI vs encodeURIComponent:

Gunakan encodeURIComponent untuk nilai parameter:

const query = "cari&tampilkan=true";
const url = `/search?q=${encodeURIComponent(query)}`;
// "/search?q=cari%26tampilkan%3Dtrue"  ← aman

Deklarasi encoding di HTML:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">   <!-- WAJIB, harus dalam 1024 byte pertama -->
  <title>...</title>
</head>

Tanpa deklarasi, browser menebak — dan sering salah.

Deklarasi encoding di HTTP:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

HTTP header menang atas meta tag. Pastikan server kamu selalu kirim charset=utf-8.

BOM (Byte Order Mark):

BOM adalah 3 byte 0xEF 0xBB 0xBF di awal file UTF-8 — penanda "ini UTF-8."

Masalahnya: BOM bisa merusak banyak hal (PHP output error, CSV parser bingung, JSON gagal parse). Untuk UTF-8, jangan pakai BOM.

<?php
// File dengan BOM:
  ↑ 3 byte tersembunyi di sini bisa bikin "headers already sent" error

Mojibake — bug encoding klasik:

Mojibake (文字化け, "huruf rusak" dalam Jepang) terjadi saat byte di-decode dengan encoding salah:

Teks asli UTF-8: "Café"   (byte: 43 61 66 C3 A9)
Di-decode sebagai Latin-1: "Café"  ← bug!

Cara menghindari:

  1. Gunakan UTF-8 di semua lapisan: database, HTTP header, meta tag, source file
  2. MySQL: pakai utf8mb4 (UTF-8 penuh, termasuk emoji) — bukan utf8 (yang cuma 3 byte)
  3. PHP: mb_* functions, header("Content-Type: text/html; charset=utf-8")
  4. Node.js: default UTF-8, tapi hati-hati dengan Buffer.toString() — selalu specify encoding
  5. File source code: simpan sebagai UTF-8 tanpa BOM

Contoh bug emoji length:

const name = "😀Budi";
if (name.length <= 10) {   // BUG: emoji dihitung 2 unit
  // "😀Budi" punya length 6, tapi visually 5 karakter
}

// Solusi:
const charCount = [...name].length;  // 5 — benar!

Contoh konversi byte ke teks:

const encoder = new TextEncoder();                // default UTF-8
const bytes = encoder.encode("Halo 😀");
console.log(bytes);  // Uint8Array [72, 97, 108, 111, 32, 240, 159, 152, 128]

const decoder = new TextDecoder("utf-8");
const text = decoder.decode(bytes);
console.log(text);  // "Halo 😀"

Tips praktis:

Encoding adalah hal yang mudah dilupakan sampai terjadi bug — lalu sulit di-debug. Investasi waktu sekarang untuk memahaminya.

Yang akan kamu pelajari