Internationalization, disingkat i18n (huruf i, 18 huruf di tengah, huruf n), adalah menyiapkan halaman supaya bisa tampil dalam bahasa dan kebiasaan daerah lain tanpa menulis ulang kodenya. Mengisi terjemahan untuk satu bahasa disebut localization (l10n). Bayangin papan harga di depan Homestay Kabut. Kalau Bu Sri melukis seluruh kalimatnya langsung di papan, tiap tamu asing butuh papan baru. Kalau papannya punya slot (nama kamar, harga, tanggal) dengan kartu yang bisa diganti, cukup ganti kartunya. i18n membuat slotnya; l10n mengisi kartunya.
🧪 Tebakan cepat: Halaman Bu Sri menulis harga dengan
'Rp ' + total.toLocaleString(). Pak Hadi membukanya di laptop yang bahasa browser-nya English (US). Total 2 malam tampil sebagai apa? Jawabannya ada di bagian "Angka dan uang: Intl.NumberFormat".
Teks keluar dari kode
Langkah pertama: semua teks halaman dipindah ke satu kamus per bahasa, dan kode cuma menyebut kuncinya.
const messages = {
id: { book: 'Pesan kamar, bayar di tempat', cancel: 'Gratis batal sampai 3 hari sebelum check-in.' },
en: { book: 'Book now, pay at the homestay', cancel: 'Free cancellation up to 3 days before check-in.' },
};
const locale = 'en-US';
const lang = locale.slice(0, 2); // 'en'
document.querySelector('.book').textContent = messages[lang].book;
- Tiap bahasa punya kunci yang sama (
book,cancel); isinya kalimat utuh, bukan potongan. - Locale adalah kode bahasa plus wilayah:
id-ID(Indonesia),en-US(Inggris, Amerika),en-GB(Inggris, Britania). Wilayah menentukan format angka dan tanggal. - Jangan menyambung potongan kalimat:
'Total ' + n + ' malam'gak bisa diterjemahkan, karena bahasa Inggris butuh "Total for 2 nights" (urutan dan bentuk jamak berbeda). - Library seperti react-i18next dan FormatJS bekerja dengan cara yang sama. BelajarWeb.dev sendiri memakai react-i18next dengan kamus
id.jsondanen.json.
Angka dan uang: Intl.NumberFormat
Intl.NumberFormat adalah pemformat bawaan browser yang tahu aturan tiap locale: pemisah ribuan, desimal, dan simbol mata uang. Kami menjalankannya di Chromium 147, WebKit 26.4 (mesin Safari), dan Firefox 146:
| Kode | Chrome, Firefox | Safari |
|---|---|---|
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(700000) |
Rp 700.000,00 | Rp700.000,00 |
sama, plus minimumFractionDigits: 0, maximumFractionDigits: 0 |
Rp 700.000 | Rp700.000 |
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(700000) |
IDR 700,000 | IDR 700,000 |
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.5) |
$1,234.50 | $1,234.50 |
(700000).toLocaleString() di browser English (US) |
700,000 | 700,000 |
style: 'currency'pluscurrency: 'IDR'menambah simbol mata uang. Mata uang ditulis terpisah dari locale, karena tamu Amerika tetap membayar dalam rupiah.- Indonesia memakai titik untuk ribuan dan koma untuk desimal:
Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR" })memformat 1234567.89 jadi Rp 1.234.567,89.en-USsebaliknya. - Rupiah punya 2 angka desimal di data locale, jadi
,00muncul kecuali kamu meminta 0 digit. - USD selalu dapat 2 desimal: 1000 jadi $1,000.00, dan 0 jadi $0.00.
Jadi jawaban tebakannya: Rp 700,000, dengan koma. toLocaleString() tanpa locale memakai bahasa browser, jadi di laptop English (US) pemisahnya koma, dan tamu Indonesia membacanya sebagai tujuh ratus rupiah koma nol nol nol. Itu jawaban tebakannya.
Tanggal: Intl.DateTimeFormat
const checkIn = new Date(2026, 11, 12); // bulan dihitung dari 0: 11 = Desember
new Intl.DateTimeFormat('id-ID', { day: 'numeric', month: 'short' }).format(checkIn); // "12 Des"
new Intl.DateTimeFormat('en-US', { day: 'numeric', month: 'short' }).format(checkIn); // "Dec 12"
new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'short' }).format(checkIn); // "12 Dec"
Hasilnya sama di ketiga browser. Dua hal yang sering salah:
- Tanggal angka saja itu ambigu. 3 Januari 2027 jadi
3/1/2027diid-IDdan1/3/2027dien-US, yang di Indonesia terbaca 1 Maret. Pakai nama bulan (month: 'short'). - Zona waktu.
new Date('2024-06-15T00:00:00Z')adalah tengah malam UTC. Di browser yang zonanya Los Angeles,new Intl.DateTimeFormat('en-US').format(...)memberi6/14/2024, sehari lebih awal. Tambahkan{ timeZone: 'UTC' }supaya hasilnya6/15/2024di mana pun. Dengan opsi yang sama,'2024-01-02T00:00:00Z'jadi1/2/2024.
Plural: Intl.PluralRules
Bahasa Indonesia gak mengubah kata benda untuk jumlah: 1 malam, 2 malam. Bahasa Inggris punya dua bentuk: 1 night, 2 nights. Bahasa Arab punya enam. Intl.PluralRules memberi tahu kategori mana yang dipakai:
new Intl.PluralRules('id-ID').select(2); // "other": kategori satu-satunya di bahasa Indonesia
new Intl.PluralRules('en-US').select(1); // "one"
new Intl.PluralRules('en-US').select(2); // "other"
const nightWord = { id: { other: 'malam' }, en: { one: 'night', other: 'nights' } };
const rule = new Intl.PluralRules(locale).select(nights);
const label = `${nights} ${nightWord[lang][rule]}`; // "1 night", "2 nights"
Kami mengecek resolvedOptions().pluralCategories di ketiga browser: id cuma other, en punya one dan other, ar punya zero, one, two, few, many, other. Jangan menulis n === 1 ? 'night' : 'nights' sendiri; aturan itu salah untuk banyak bahasa lain. Di react-i18next dan FormatJS, bentuk jamaknya ditulis di kamus dengan aturan yang sama.
Teks yang memanjang
Terjemahan jarang sama panjang. Di kamus BelajarWeb.dev sendiri (21.318 teks yang ada di id.json dan en.json), teks Indonesia secara total 2,1% lebih pendek, tapi untuk label pendek (sampai 10 huruf dalam bahasa Inggris) teks Indonesia rata-rata 15,4% lebih panjang: "Search" jadi "Cari", tapi "Save" jadi "Simpan" dan "Settings" jadi "Pengaturan". Panduan IBM yang dikutip artikel W3C "Text size in translation" mencatat pola yang sama dari bahasa Inggris ke bahasa lain: teks sampai 10 huruf bisa memanjang 200 sampai 300%, teks di atas 70 huruf sekitar 130%.
Tombol Pesan Bu Sri dulu dibuat pas untuk teks Indonesia. Kami mengukur lebarnya di Chromium 147 (font sistem, 16px, tebal 600):
| Teks tombol | Lebar teks | Di tombol width: 256px; padding: 0 16px (ruang 224px) |
|---|---|---|
| Pesan kamar, bayar di tempat | 223,4px | muat |
| Book now, pay at the homestay | 235,2px | terpotong 11,2px |
.book {
min-inline-size: 16rem; /* boleh lebih lebar, gak boleh lebih sempit */
max-inline-size: 100%;
padding-inline: 16px;
white-space: normal; /* teks yang panjang turun ke baris kedua, bukan terpotong */
}
Font berbeda di tiap sistem, jadi angkanya beda di Windows atau Android, tapi masalahnya sama: ukuran yang dikunci pas untuk satu bahasa.
lang, dir, dan logical properties
<html lang="en"> menyebut bahasa halaman. Screen reader memilih suara dan cara baca dari sini (WCAG 3.1.1 Language of Page), dan hyphens: auto memakai kamus bahasa itu. Satu frasa berbahasa lain di dalam halaman diberi lang sendiri (WCAG 3.1.2 Language of Parts).
dir="rtl" membalik arah tulisan untuk bahasa kanan ke kiri seperti Arab. Supaya tata letaknya ikut terbalik, pakai logical properties: properti yang menyebut awal dan akhir baris, bukan kiri dan kanan.
| Fisik (kiri dan kanan) | Logical (awal dan akhir) |
|---|---|
padding-left |
padding-inline-start |
margin-right |
margin-inline-end |
left |
inset-inline-start |
width |
inline-size |
text-align: left |
text-align: start |
Kami memberi dir="rtl" lalu membaca hasilnya di ketiga browser: padding-inline-start: 16px berpindah ke kanan, padding-left: 16px tetap di kiri (sekarang di sisi yang salah), dan text-align: start ikut ke kanan. Dengan logical properties, browser membalik sendiri; gak perlu stylesheet RTL kedua.
⚠️ Jebakan yang sering ketemu
toLocaleString()tanpa locale. Hasilnya ikut bahasa browser: "Rp 700,000" di laptop English. Selalu sebut locale-nya.- Menyambung potongan kalimat. Urutan kata dan bentuk jamak berbeda tiap bahasa. Simpan kalimat utuh dengan slot.
- Membandingkan hasil format dengan spasi biasa. Chrome dan Firefox menaruh spasi tak terputus (U+00A0) di "Rp 700.000"; Safari gak memberi spasi. Tes yang mengetik
'Rp 700.000'dengan spasi biasa gagal di semua browser. - Tanggal tanpa
timeZonedan tanggal angka saja. Sehari meleset di zona lain, dan 3/1 terbaca dua cara. - Lebar tetap dan
padding-left. Teks Inggris terpotong, tata letak RTL gak terbalik. Pakaimin-inline-sizedan logical properties.
TL;DR: i18n menyiapkan slot, l10n mengisi terjemahannya. Teks keluar dari kode ke kamus per bahasa, sebagai kalimat utuh. Locale (id-ID, en-US) menentukan format: Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR" }) memberi Rp 1.234.567,89; en-US dengan USD memberi $1,234.50 dan $1,000.00; toLocaleString() tanpa locale ikut browser dan bisa menulis Rp 700,000. Intl.DateTimeFormat memberi "12 Des" atau "Dec 12"; pakai nama bulan dan timeZone: 'UTC' (6/15/2024, bukan 6/14/2024). Intl.PluralRules memilih bentuk jamak: Indonesia cuma other, Inggris one dan other. Label pendek Indonesia rata-rata 15,4% lebih panjang dari Inggris, dan terjemahan bisa melebarkan tombol: pakai min-inline-size, bukan lebar tetap. lang menyebut bahasa halaman (WCAG 3.1.1), dan logical properties seperti padding-inline-start membalik sendiri saat dir="rtl".