i18next Setup — i18n

i18next Setup i18next adalah framework i18n paling populer di ekosistem JavaScript. Bisa dipakai di browser, Node.js, React, Vue, dan framework lainnya…

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-JP belum punya key cancel, dan init ditulis tanpa fallbackLng. Di locale ja-JP, t('cancel') menampilkan apa: teks bahasa Indonesia, string kosong, atau tulisan cancel? 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
});

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"

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"

escapeValue: kenapa React mematikannya

Bawaannya interpolation.escapeValue: true: nilai yang masuk slot di-escape untuk HTML (& jadi &amp;, < jadi &lt;, juga >, ", ', dan /). Pemesannya "Rina & Hadi", jadi di ja-JP hasilnya Rina &amp; Hadi様、ありがとうございます。. React merender teks itu apa adanya, dan tamunya melihat tulisan &amp;. 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 },
});

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

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.

Yang akan kamu pelajari