Dynamic Language Switching — i18n

Dynamic Language Switching User harus bisa mengganti bahasa kapan saja tanpa reload halaman. Pilihan bahasa harus di-persist agar tidak hilang saat kembali…

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-JP belum 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 seperti room? 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>
  );
}

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

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>

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari