i18next adalah library i18n JavaScript yang paling banyak dipakai: dia menyimpan kamus per bahasa dan memberi fungsi t() untuk mengambil teks dari kamus itu. react-i18next adalah jembatannya ke React. Bayangin meja resepsionis Homestay Kabut: di raknya ada satu map per bahasa, dan tiap kalimat punya label halaman. Tamu dari Jepang minta halaman "cancel"; resepsionis membuka map Jepang. Kalau halamannya kosong, dia ambil map berikutnya di daftar cadangan. Kalau di semua map gak ada, dia membacakan label halamannya saja. t('cancel') bekerja persis begitu.
🧪 Tebakan cepat: Kamus
ja-JPbelum punya keycancel, daninitditulis tanpafallbackLng. Di localeja-JP,t('cancel')menampilkan apa: teks bahasa Indonesia, string kosong, atau tulisancancel? Jawabannya ada di bagian "Kalau key gak ketemu".
Pasang dan init
Versi terbaru (Oktober 2026): i18next 26.4.3 dan react-i18next 17.0.16 (butuh i18next 26.2.0 ke atas dan React 16.8 ke atas). Pasang dengan npm install i18next react-i18next.
// src/i18n.js: dijalankan sekali, sebelum aplikasi dirender
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import idID from './locales/id-ID/booking.json';
import enUS from './locales/en-US/booking.json';
import jaJP from './locales/ja-JP/booking.json';
import arEG from './locales/ar-EG/booking.json';
i18next.use(initReactI18next).init({
lng: 'ja-JP', // bahasa yang dipakai sekarang
fallbackLng: ['en-US', 'id-ID'], // cadangan, dicoba berurutan
ns: ['booking'], // namespace = satu file kamus per fitur
defaultNS: 'booking', // namespace untuk t('room') tanpa awalan
resources: { 'id-ID': { booking: idID }, 'en-US': { booking: enUS }, 'ja-JP': { booking: jaJP }, 'ar-EG': { booking: arEG } },
interpolation: { escapeValue: false }, // React sudah meng-escape teks
});
.use(initReactI18next)memasang plugin yang membuat komponen denganuseTranslationrender ulang saat bahasa berganti.lngadalah bahasa aktif.fallbackLngadalah daftar cadangan; nilai bawaannya'dev', bahasa pura-pura yang gak punya kamus.- Namespace adalah satu file kamus per fitur (
booking.json).nsmendaftarnya,defaultNSdipakai kalau key ditulis tanpa awalan. Key dari namespace lain ditulis dengan titik dua:t('admin:report').
File kamus dan t()
Kamus i18next memakai format JSON v4. Ini kamus sumber Bu Sri, locales/id-ID/booking.json:
{
"room": "Kamar Teras",
"nights_other": "{{count}} malam",
"total_other": "Total {{count}} malam: {{price}}",
"book": "Pesan kamar, bayar di tempat",
"cancel": "Gratis batal sampai {{days}} hari sebelum check-in.",
"breakfast": "Sudah termasuk sarapan",
"thanks": "Terima kasih, {{name}}!"
}
{{price}} adalah slot interpolasi, diisi dari objek kedua t(). Akhiran _other adalah bentuk jamak; en-US punya nights_one dan nights_other. Kamus ja-JP belum lengkap: belum ada book, cancel, dan breakfast. en-US belum punya breakfast, key baru dari Bu Sri.
import { useTranslation } from 'react-i18next';
function BookingCard({ nights, price }) {
const { t, i18n } = useTranslation('booking');
return (
<article>
<h2>{t('room')}</h2>
<p>{t('total', { count: nights, price })}</p>
<button>{t('book')}</button>
<button onClick={() => i18n.changeLanguage('ar-EG')}>العربية</button>
</article>
);
}
// ja-JP, nights = 2: "テラスルーム", "2泊の合計:IDR 700,000", "Book now, pay at the homestay"
useTranslation('booking')memberituntuk namespacebookingdan objeki18n.countmemilih bentuk jamak sekaligus mengisi{{count}}. Harga diformat dulu denganIntl.NumberFormat(pelajaran Intl API), lalu masuk lewat{{price}}.i18n.changeLanguage('ar-EG')mengganti bahasa; semua komponen denganuseTranslationikut render ulang.
Rantai fallback: ja-JP, ja, en-US, id-ID
Untuk lng: 'ja-JP', i18next membaca bahasa berurutan: ['ja-JP', 'ja', 'en-US', 'id-ID']. ja (bahasa tanpa region) ditambahkan sendiri; sisanya dari fallbackLng. Hasil i18next 26.4.3:
t(...) di ja-JP |
ja-JP | ja | en-US | id-ID | Hasil |
|---|---|---|---|---|---|
t('room') |
ada | テラスルーム | |||
t('cancel', { days: 3 }) |
gak ada | gak ada | ada | Free cancellation up to 3 days before check-in. | |
t('breakfast') |
gak ada | gak ada | gak ada | ada | Sudah termasuk sarapan |
t('deposit') |
gak ada | gak ada | gak ada | gak ada | deposit |
Urutannya keputusan bisnis: tamu Jepang lebih mungkin paham bahasa Inggris daripada bahasa Indonesia, jadi en-US di depan. fallbackLng juga boleh berupa objek: { 'ja-JP': ['en-US', 'id-ID'], default: ['id-ID'] }.
Jamak: akhiran _one, _other, dan kawan-kawannya
i18next memilih akhiran dengan Intl.PluralRules. Untuk tiap bahasa di rantai, dia mencoba key_<kategori> lalu key polos; kalau count 0, key_zero dicoba paling dulu, juga di bahasa Inggris. Kamus ar-EG kami cuma punya nights_one, nights_two, dan nights_other:
t('nights', { count }) di ar-EG |
Kategori | Yang ketemu | Hasil |
|---|---|---|---|
| 1 | one | ar-EG nights_one |
ليلة واحدة |
| 2 | two | ar-EG nights_two |
ليلتان |
| 3 | few | en-US nights_other |
3 nights |
| 100 | other | ar-EG nights_other |
100 ليلة |
nights_few gak ada, dan i18next gak memakai nights_other sebagai gantinya: dia lanjut ke en-US, menghitung kategori dengan aturan bahasa Inggris (3 itu other), lalu menulis "3 nights". Jadi tiap bahasa butuh semua kategorinya sendiri. {{count}} ditulis apa adanya ("3", bukan "٣"); {{count, number}} memformatnya sesuai bahasa.
Tim yang sudah punya kamus ICU dari pelajaran react-intl bisa memakai plugin i18next-icu 2.5.0 (butuh intl-messageformat 10.3.3 sampai di bawah 13): i18next.use(ICU), lalu t('nights', { n: 3 }) di ar-EG memberi ٣ ليالٍ.
Kalau key gak ketemu
Kalau key gak ada di semua bahasa di rantai, t() mengembalikan key-nya sendiri. Tanpa fallbackLng, rantai ja-JP cuma ['ja-JP', 'ja', 'dev'], jadi t('cancel') menampilkan tulisan cancel, bukan teks Indonesia dan bukan string kosong. Itu jawaban tebakannya.
i18next.init({
// ...opsi di atas, plus:
saveMissing: true,
missingKeyHandler: (lngs, ns, key, fallbackValue) => console.warn('Key hilang:', lngs, ns, key),
});
t('deposit'); // "deposit", handler: ["en-US","id-ID"] "booking" "deposit"
t('deposit', { defaultValue: 'Uang muka 0' }); // "Uang muka 0"
saveMissing: truememanggilmissingKeyHandlertiap kali key gak ketemu; dengancount, sekali untuk tiap bentuk jamak (deposit_one,deposit_other, ...) per bahasa cadangan.defaultValueadalah teks cadangan yang ditulis di kode.- Slot tanpa nilai tetap tertulis:
t('thanks')tanpanamememberi{{name}}様、ありがとうございます。.
escapeValue: kenapa React mematikannya
Bawaannya interpolation.escapeValue: true: nilai yang masuk slot di-escape untuk HTML (& jadi &, < jadi <, juga >, ", ', dan /). Pemesannya "Rina & Hadi", jadi di ja-JP hasilnya Rina & Hadi様、ありがとうございます。. React merender teks itu apa adanya, dan tamunya melihat tulisan &. React sudah meng-escape semua teks di JSX, jadi setup React memakai escapeValue: false. Kalau hasil t() masuk ke innerHTML atau dangerouslySetInnerHTML, gak ada yang meng-escape lagi: di situ biarkan true.
Deteksi bahasa dan backend
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
i18next.use(HttpBackend).use(LanguageDetector).use(initReactI18next).init({
fallbackLng: ['en-US', 'id-ID'],
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
detection: { order: ['querystring', 'localStorage', 'navigator'], caches: ['localStorage'] },
interpolation: { escapeValue: false },
});
- i18next-browser-languagedetector (8.2.1) adalah plugin yang mendeteksi bahasa tamu. Urutan bawaannya:
querystring(?lng=ja-JP),cookie(namai18next),localStorage(keyi18nextLng),sessionStorage,navigator(bahasa browser),htmlTag(<html lang>). Detektorpath,subdomain, danhashharus ditambahkan keordersendiri.cachesmenyimpan hasilnya, jadi kunjungan berikutnya langsung pakai bahasa itu. - Jangan isi
lngkalau memakai detektor:lngmengalahkan hasil deteksi. - i18next-http-backend (4.0.2) mengambil kamus lewat HTTP dengan
loadPath. Selama kamus dimuat,useTranslationmenunda render lewat React Suspense (useSuspense: truebawaan react-i18next), jadi bungkus aplikasi dengan<Suspense>.
Lab ini: subset i18next kecil yang jujur
i18next gak terpasang di BelajarWeb.dev, dan Playground gak bisa memasang paket npm. Jadi Lab-nya memakai t() buatan sendiri (sekitar 50 baris): ns:key, {{nama}} dengan escapeValue, akhiran _zero dan _<kategori>, rantai fallbackLng, returnEmptyString, defaultValue, dan saveMissing. Yang gak didukung: nesting $t(), {{x, number}}, context, ordinal, plugin, dan backend. Dibandingkan dengan i18next 26.4.3 pada kamus di atas, delapan lng, lima bentuk fallbackLng, delapan nilai count, dan semua kombinasi escapeValue dan returnEmptyString: 79.680 dari 79.680 hasil sama persis, termasuk panggilan missingKeyHandler.
⚠️ Jebakan yang sering ketemu
- Lupa
fallbackLng. Bawaannya'dev', jadi teks yang belum diterjemahkan muncul sebagai key mentah. - Mengira
_otherjadi cadangan semua kategori. Diar-EG, 3 malam butuhnights_few; tanpa itu i18next lompat ke bahasa berikutnya. countberupa string.t('nights', { count: '2' })gak memilih bentuk jamak dan memberi "nights". Kirim angka.escapeValue: falselaludangerouslySetInnerHTML. Nilai dari tamu masuk ke HTML tanpa di-escape.
TL;DR: i18next 26 menyimpan kamus per bahasa dan namespace (JSON v4 dengan {{slot}} dan akhiran _one/_other); react-i18next 17 memberi useTranslation('booking') dan t(). init butuh lng (atau detektor), fallbackLng, ns, defaultNS, dan interpolation.escapeValue: false karena React sudah meng-escape. fallbackLng adalah bahasa cadangan yang dipakai kalau terjemahan untuk bahasa aktif gak ditemukan: ja-JP dibaca sebagai ja-JP, ja, en-US, id-ID. Tanpa itu (bawaannya 'dev'), key yang hilang tampil sebagai key mentah; saveMissing plus missingKeyHandler melaporkannya. Bentuk jamak dipilih Intl.PluralRules per bahasa, dan kategori yang gak ada di file bahasa itu jatuh ke bahasa berikutnya, bukan ke _other. Plugin deteksi bahasanya i18next-browser-languagedetector (querystring, cookie, localStorage, sessionStorage, navigator, htmlTag), kamus lewat HTTP dengan i18next-http-backend, sintaks ICU dengan i18next-icu.