Ganti bahasa dinamis artinya halaman yang sedang terbuka berganti bahasa tanpa reload, tanpa kehilangan pilihan tamu, dan pilihannya diingat untuk kunjungan berikutnya. Bayangin TV di ruang tamu Homestay Kabut. Tamu dari Jepang sedang menonton film, lalu mengganti subtitle ke bahasa Jepang. Filmnya gak diulang dari awal, adegannya tetap di menit yang sama, subtitle Jepangnya mungkin butuh sedetik untuk diunduh, dan besok TV-nya ingat pilihan itu. Empat hal itu yang dibahas di sini: tanpa reload, state tetap, kamus dimuat saat dipilih, pilihan disimpan.
🧪 Tebakan cepat: Tamu memilih 3 malam di kartu Bu Sri yang berbahasa Indonesia, lalu klik "日本語". Kamus
ja-JPbelum dimuat dan butuh 300 ms. Dengan setelan bawaan react-i18next, apa yang tampil selama 300 ms itu: kartu kosong dengan spinner, kartu bahasa Indonesia yang lama, atau key mentah sepertiroom? Jawabannya ada di bagian "Lazy-load: kamus ja dan ar dimuat saat dipilih".
Ganti tanpa reload: changeLanguage
Cara lama mengganti bahasa adalah memuat ulang halaman, misalnya location.href = '?lng=ja-JP'. Semua JavaScript diunduh ulang, dan pilihan 3 malam kembali ke 2 malam, nilai awalnya. Dengan i18next (pelajaran "i18next Setup"), cukup satu panggilan:
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
function BookingCard() {
const { t, i18n } = useTranslation('booking');
const [nights, setNights] = useState(2);
return (
<section>
<h1>{t('room')}</h1>
<button onClick={() => setNights(3)}>{t('nights', { count: 3 })}</button>
<p>{t('nights', { count: nights })}</p>
<button onClick={() => i18n.changeLanguage('ja-JP')}>日本語</button>
</section>
);
}
i18n.changeLanguage('ja-JP')mengembalikan Promise. Dia memuat kamus kalau belum ada, menggantii18n.language, lalu memancarkan eventlanguageChanged.- Tiap komponen yang memakai
useTranslationrender ulang dengan bahasa baru. State React gak tersentuh, jadinightstetap 3: "Kamar Teras" jadi "テラスルーム", "3 malam" jadi "3泊".
Lazy-load: kamus ja dan ar dimuat saat dipilih
Tamu cuma memakai satu bahasa, jadi jangan masukkan keempat kamus ke bundle utama. Kamus bahasa sumber ikut bundle, sisanya dimuat dengan import() saat dipilih. Vite memecah tiap file JSON yang di-import() jadi file sendiri.
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import idID from './locales/id-ID/booking.json';
i18next
.use(resourcesToBackend((lng, ns) => import(`./locales/${lng}/${ns}.json`)))
.use(initReactI18next)
.init({
lng: 'id-ID',
fallbackLng: ['en-US', 'id-ID'],
ns: ['booking'],
defaultNS: 'booking',
partialBundledLanguages: true, // id-ID ada di bundle, bahasa lain dimuat
resources: { 'id-ID': { booking: idID } },
interpolation: { escapeValue: false },
});
Kami menjalankan setup ini dengan i18next 26.4.3, react-i18next 17.0.16, dan i18next-resources-to-backend 1.2.3, dengan kamus yang sengaja diperlambat 300 ms:
| Waktu | Yang terjadi |
|---|---|
| 0 ms | changeLanguage('ja-JP') dipanggil. Event languageChanging. i18n.language masih "id-ID", kamus ja-JP mulai dimuat. |
| 0 sampai 300 ms | Kartu tetap bahasa Indonesia: "3 malam". |
| 300 ms | Kamus tiba. Event languageChanged, i18n.language jadi "ja-JP", kartu jadi "3泊". |
| Klik bahasa yang sudah dimuat | Langsung pindah, tanpa memuat apa pun. |
Jadi yang tampil selama memuat adalah kartu bahasa Indonesia yang lama. Itu jawaban tebakannya. React-i18next bawaannya cuma mendengar languageChanged (opsi react.bindI18n). Kalau ditambah languageChanging, komponen menunggu lewat Suspense dan fallback-nya yang tampil selama memuat. Teks lama biasanya lebih enak dilihat; tandai saja tombol bahasa yang diklik dengan aria-busy="true" dan spinner kecil, supaya tamu tahu klik-nya diterima.
Kalau kamusnya gagal dimuat (misalnya 404), changeLanguage('ar-EG') tetap selesai tanpa error. Hasil ukur kami: i18n.language jadi "ar-EG", tapi i18n.resolvedLanguage jadi "en-US", dan kartunya tampil dalam bahasa Inggris dari fallbackLng. resolvedLanguage adalah bahasa yang benar-benar dipakai t().
Yang ikut diganti: lang, dir, dan Intl
Mengganti kata saja belum cukup. <html lang> dan dir harus ikut, dan ambil dari resolvedLanguage: kalau dari language, kamus Arab yang gagal dimuat menghasilkan teks Inggris di halaman rtl.
i18next.on('languageChanged', () => {
const used = i18next.resolvedLanguage; // 'ja-JP'
document.documentElement.lang = used;
document.documentElement.dir = i18next.dir(used); // 'rtl' untuk ar-EG
});
Pemformat Intl juga terikat ke satu locale sejak dibuat. Pemformat yang dibuat sekali di luar komponen dengan 'id-ID' tetap menulis "Rp 1.050.000" di kartu berbahasa Jepang. Buat ulang tiap kali bahasanya berganti:
const { i18n } = useTranslation();
const money = useMemo(
() => new Intl.NumberFormat(i18n.resolvedLanguage, { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }),
[i18n.resolvedLanguage],
);
money.format(1050000); // id-ID "Rp 1.050.000", ja-JP "IDR 1,050,000", ar-EG "١٬٠٥٠٬٠٠٠ IDR"
Simpan pilihannya: localStorage, cookie, URL, atau database
| Disimpan di | Server tahu di request pertama? | Ikut kalau link dibagikan? | Ikut ke device lain? |
|---|---|---|---|
localStorage |
gak, cuma JavaScript di browser yang bisa baca | gak | gak |
cookie lang=ja-JP |
ya, cookie ikut tiap request | gak | gak |
URL /ja/テラスルーム |
ya | ya | ya, lewat link-nya |
database, kolom locale |
ya, kalau tamunya login | gak | ya |
- localStorage: kalau halaman dirender di server, server gak tahu pilihannya. HTML pertama berbahasa Indonesia, lalu JavaScript membaca
localStoragedan mengganti ke bahasa Jepang. Tamu melihat kedipan bahasa Indonesia. - Cookie:
document.cookie = 'lang=ja-JP; path=/; max-age=31536000; samesite=lax'. Server membacanya dan langsung mengirim HTML berbahasa Jepang. - URL menang untuk halaman publik: tiap bahasa punya alamat sendiri, link yang dikirim tamu ke temannya membawa bahasanya, dan Google bisa mengindeks tiap versi (pelajaran "SEO untuk Multi-language"). Ganti bahasa artinya pindah ke halaman yang sama dengan awalan lain; router SPA melakukannya tanpa reload.
- Database untuk tamu yang login: simpan
localedi tabel user lewat request ke API, supaya ikut di HP-nya dan dipakai untuk email (pelajaran berikutnya).
Kombinasinya: cookie atau localStorage untuk tamu yang belum login, database untuk yang login, dan URL untuk halaman yang dibagikan dan dicari. Urutan membacanya saat halaman dibuka: awalan URL, lalu locale user, lalu cookie, lalu header Accept-Language, lalu id-ID.
Pemilih bahasa yang benar
<nav aria-label="Bahasa / Language">
<a href="/id/kamar-teras" lang="id" hreflang="id" aria-current="page">Bahasa Indonesia</a>
<a href="/en/terrace-room" lang="en" hreflang="en">English</a>
<a href="/ja/テラスルーム" lang="ja" hreflang="ja">日本語</a>
<a href="/ar/غرفة-التراس" lang="ar" hreflang="ar">العربية</a>
</nav>
- Tulis tiap bahasa dalam bahasanya sendiri. Tamu Jepang yang nyasar ke halaman Arab gak bisa membaca "اليابانية" (bahasa Jepang dalam bahasa Arab), tapi pasti mengenali "日本語".
new Intl.DisplayNames(['ja'], { type: 'language' }).of('ja')memberi "日本語" dan untukarmemberi "العربية", sama di Chromium 147, Firefox 146, WebKit 26.4, dan Node 22.22.1. Untukidhasilnya "Indonesia", jadi banyak situs menulis "Bahasa Indonesia" sendiri. - Jangan pakai bendera. Bendera menandai negara, bukan bahasa: bahasa Arab resmi di lebih dari 20 negara, dan bahasa Inggris gak cuma milik Amerika.
langdi tiap link membuat screen reader membaca "日本語" dengan lafal Jepang.aria-current="page"menandai bahasa yang sedang dipakai.- Link biasa lebih baik daripada
<select>untuk halaman publik: bisa dibuka di tab baru dan bisa diikuti Google.
⚠️ Jebakan yang sering ketemu
- Reload untuk ganti bahasa. Pilihan tamu (3 malam, tanggal, isi form) hilang. Pakai
changeLanguage. - Pemformat
Intldibuat sekali. Kata-katanya bahasa Jepang, harganya masih "Rp 1.050.000". Buat ulang per locale. dirdarii18n.language. Kalau kamus Arab gagal dimuat, teks Inggris tampil kanan ke kiri. PakairesolvedLanguage.- Cuma
localStoragedi situs yang dirender server. HTML pertama selalu bahasa sumber, lalu berkedip. Pakai cookie atau URL. - Bendera dan nama bahasa dalam bahasa yang sedang aktif. Tulis "日本語" dan "العربية", tanpa bendera.
TL;DR: i18n.changeLanguage('ja-JP') mengganti bahasa tanpa reload, dan state React (3 malam) tetap. Kamus ja-JP dan ar-EG dimuat dengan import() saat dipilih; selama memuat, react-i18next bawaan tetap menampilkan bahasa lama (languageChanging lalu languageChanged), dan kamus yang gagal dimuat membuat resolvedLanguage jatuh ke fallbackLng. Sesudah ganti, set <html lang> dan dir dari resolvedLanguage, dan buat ulang pemformat Intl. Simpan pilihan di cookie atau localStorage untuk tamu, di database untuk user yang login, dan di URL untuk halaman yang dibagikan dan dicari Google (URL menang di sana). Pemilih bahasa menulis tiap bahasa dalam bahasanya sendiri ("日本語", "العربية"), dengan lang di tiap pilihan dan tanpa bendera.