28. Internationalization (i18n)

Dalam pengembangan aplikasi berskala global atau regional (seperti di Indonesia yang sering kali membutuhkan pilihan bahasa Indonesia dan Inggris), kemampuan aplikasi untuk beradaptasi dengan bahasa lokal pengguna sangatlah krusial. Proses ini dibagi menjadi dua konsep utama:

  • Internationalization (i18n): Proses merancang dan membangun aplikasi agar dapat diadaptasi ke berbagai bahasa dan wilayah dengan mudah tanpa perlu menulis ulang kode program.
  • Localization (l10n): Proses menerjemahkan teks, menyesuaikan format tanggal, angka, mata uang, dan aspek kultural lainnya untuk wilayah tertentu.

Dokumentasi ini menjelaskan standar implementasi i18n menggunakan next-intl untuk Next.js (App Router) dan react-i18next untuk React Single Page Application (SPA).


A. Perbandingan Library i18n Populer

Kriterianext-intl (Direkomendasikan untuk Next.js)react-i18next (Direkomendasikan untuk React SPA)
Integrasi RouterSangat erat dengan Next.js App Router (Routing, Middleware, Link, Redirect).Independen dari router, biasanya dikelola secara manual dengan state/URL parameter.
Kinerja SSR & RSCTerintegrasi penuh dengan React Server Components (RSC) tanpa overhead JavaScript di sisi klien.Membutuhkan setup khusus agar bekerja baik di SSR, cenderung berpusat di client-side.
Struktur FileFile terjemahan JSON terpusat di direktori utama (misal /messages).File terjemahan JSON di folder /public/locales atau di dalam bundel source code.
Tipe Data (Type-Safety)Mendukung validasi tipe data kunci terjemahan berbasis TypeScript bawaan.Membutuhkan konfigurasi manual untuk mendukung type-safe translation keys.

B. Alur Kerja Deteksi & Rendering Bahasa (Next.js)

Mermaid diagram di bawah mengilustrasikan alur pemrosesan dari request pengguna hingga halaman disajikan dalam bahasa yang sesuai:

graph TD
    A[Pengguna mengakses URL] --> B{Apakah URL memiliki awalan bahasa? /id/ atau /en/}
    B -- Tidak --> C[Middleware mendeteksi bahasa dari Cookie / Accept-Language Header]
    C --> D[Redirect ke URL dengan awalan yang sesuai, misal: /id/dashboard]
    B -- Ya --> E[Middleware memvalidasi kecocokan locale yang didukung]
    E --> F[Next.js memuat file terjemahan JSON yang relevan, misal: messages/id.json]
    F --> G[Render halaman di Server dengan Server Components]
    G --> H[Kirim HTML terjemahan ke Browser]
    H --> I[Hydration di Client-side untuk elemen interaktif]

C. Implementasi Praktis: next-intl di Next.js (App Router)

Berikut adalah arsitektur direktori proyek Next.js yang mendukung multibahasa secara terstruktur:

├── messages/           # Direktori penyimpanan file JSON terjemahan
│   ├── en.json
│   └── id.json
├── src/
│   ├── middleware.ts   # Interseptor untuk mendeteksi bahasa di URL/Cookie
│   ├── i18n/
│   │   ├── routing.ts  # Definisi rute dan bahasa yang didukung
│   │   └── request.ts  # Konfigurasi Next-intl request
│   └── app/
│       └── [locale]/   # Dynamic route segment untuk membungkus seluruh halaman
│           ├── layout.tsx
│           └── page.tsx

Langkah 1: Setup File Konfigurasi Rute (src/i18n/routing.ts)

Buat file untuk mengelola daftar bahasa yang didukung dan utilitas navigasi:

import {defineRouting} from 'next-intl/routing';
import {createNavigation} from 'next-intl/navigation';
 
export const routing = defineRouting({
  // Daftar bahasa yang didukung oleh aplikasi
  locales: ['en', 'id'],
  
  // Bahasa default jika tidak ada bahasa yang cocok atau tertera di URL
  defaultLocale: 'id',
  
  // Menghilangkan prefix locale di URL untuk default locale (opsional)
  localePrefix: 'as-needed' 
});
 
// Ekspor utilitas navigasi yang sadar-bahasa (locale-aware)
export const {Link, redirect, usePathname, useRouter} = createNavigation(routing);

Langkah 2: Setup Request Configuration (src/i18n/request.ts)

Next-intl membutuhkan konfigurasi ini untuk memuat pesan JSON secara dinamis saat proses render server-side berjalan:

import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
 
export default getRequestConfig(async ({locale}) => {
  // Validasi jika locale tidak didukung
  if (!routing.locales.includes(locale as any)) {
    return {
      messages: (await import('../../messages/id.json')).default
    };
  }
 
  return {
    messages: (await import(`../../messages/${locale}.json`)).default
  };
});

Langkah 3: Setup Next Config (next.config.mjs)

Integrasikan plug-in next-intl pada bundler webpack Next.js Anda:

import createNextIntlPlugin from 'next-intl/plugin';
 
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Konfigurasi Next.js Anda di sini
  reactStrictMode: true,
};
 
export default withNextIntl(nextConfig);

Langkah 4: Setup Middleware (src/middleware.ts)

Konfigurasikan middleware untuk mengarahkan pengguna secara otomatis ke bahasa yang sesuai:

import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
 
export default createMiddleware(routing);
 
export const config = {
  // Hanya jalankan middleware pada rute aplikasi, abaikan file statis & API
  matcher: ['/', '/(id|en)/:path*', '/((?!api|_next|_vercel|.*\\..*).*)']
};

Langkah 5: Layout Utama dengan [locale] Wrapper (src/app/[locale]/layout.tsx)

Bungkus aplikasi Anda untuk menyediakan payload terjemahan ke seluruh komponen:

import {NextIntlClientProvider} from 'next-intl';
import {getMessages, setRequestLocale} from 'next-intl/server';
import {routing} from '@/i18n/routing';
import {notFound} from 'next/navigation';

export function generateStaticParams() {
  return routing.locales.map((locale) => ({locale}));
}

export default async function LocaleLayout({
  children,
  params: {locale}
}: {
  children: React.ReactNode;
  params: {locale: string};
}) {
  // Validasi parameter rute
  if (!routing.locales.includes(locale as any)) {
    notFound();
  }

  // Mengatur locale secara sinkron untuk Server Components
  setRequestLocale(locale);
 
  // Memuat pesan terjemahan di server
  const messages = await getMessages();
 
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

D. Struktur Berkas Terjemahan JSON (Best Practices)

Terapkan standarisasi penamaan berkas JSON dan buatlah struktur berjenjang (nested) berdasarkan domain atau nama komponen agar tidak menumpuk.

Contoh File: messages/id.json

{
  "Common": {
    "buttons": {
      "save": "Simpan Perubahan",
      "cancel": "Batal",
      "delete": "Hapus"
    },
    "alerts": {
      "success": "Aksi berhasil diselesaikan."
    }
  },
  "Dashboard": {
    "title": "Dasbor Utama",
    "welcome": "Selamat datang kembali, {name}!",
    "status": "{count, plural, =0 {Tidak ada tugas pending} one {Ada 1 tugas pending} other {Ada # tugas pending}}"
  }
}

Contoh File: messages/en.json

{
  "Common": {
    "buttons": {
      "save": "Save Changes",
      "cancel": "Cancel",
      "delete": "Delete"
    },
    "alerts": {
      "success": "Action completed successfully."
    }
  },
  "Dashboard": {
    "title": "Main Dashboard",
    "welcome": "Welcome back, {name}!",
    "status": "{count, plural, =0 {No pending tasks} one {You have 1 pending task} other {You have # pending tasks}}"
  }
}

[!NOTE]

  • Interpolasi: Gunakan kurung kurawal {name} untuk memasukkan nilai dinamis.
  • Pluralisasi: Gunakan format sintaks ICU Message ({count, plural, ...}) untuk menangani variasi jumlah barang secara tata bahasa.

E. Cara Penggunaan Translation Hooks & Utilities

1. Di React Server Components (RSC)

Untuk menjaga performa dan kecepatan render, gunakan utilitas server-side yang bersifat asinkronous:

// src/app/[locale]/page.tsx
import {getTranslations} from 'next-intl/server';
 
export default async function Page() {
  // Memuat namespace secara spesifik
  const t = await getTranslations('Dashboard');
 
  return (
    <main>
      <h1>{t('title')}</h1>
      {/* Hasil dengan Interpolasi: */}
      <p>{t('welcome', {name: 'Alex'})}</p> 
    </main>
  );
}

2. Di React Client Components

Untuk komponen interaktif (dengan direktif 'use client'), gunakan hook standar:

'use client';
 
import {useTranslations} from 'next-intl';
import {Link} from '@/i18n/routing';
 
export default function Counter() {
  const t = useTranslations('Common.buttons');
 
  return (
    <div>
      <button onClick={() => alert('Saved')}>{t('save')}</button>
      <Link href="/about">{t('cancel')}</Link>
    </div>
  );
}

[!WARNING] Selalu pastikan menggunakan Link yang diekspor dari @/i18n/routing (bukan dari next/link bawaan). Link khusus ini akan secara otomatis menyisipkan prefix bahasa aktif ke URL tujuan (misal dari /about menjadi /id/about).


F. Penanganan Kasalahan & Gotchas (Troubleshooting)

1. Masalah Hydration Mismatch (Server vs Client)

Kesalahan hydration mismatch sering terjadi apabila nilai waktu, tanggal, atau mata uang diformat di server menggunakan locale default server Node.js, tetapi ketika sampai di browser diterjemahkan ulang menggunakan locale browser klien.

// ❌ HINDARI (Rentan memicu hydration mismatch):
<div>{new Date().toLocaleDateString()}</div>

Solusi Terstruktur: Gunakan utilitas format bawaan dari next-intl yang membaca konfigurasi locale dari context terpusat:

import {useFormatter} from 'next-intl';

export default function DateTimeComponent() {
  const format = useFormatter();
  const dateTime = new Date('2026-06-26T12:00:00Z');

  return (
    <div>
      {/* Output id: 26 Juni 2026 | Output en: June 26, 2026 */}
      <p>{format.dateTime(dateTime, {dateStyle: 'long'})}</p>
      
      {/* Output id: Rp150.000,00 | Output en: $150,000.00 */}
      <p>{format.number(150000, {style: 'currency', currency: 'IDR'})}</p>
    </div>
  );
}

2. Menghindari Hardcoded String dengan ESLint

Dalam tim besar, developer sering kali lupa dan tidak sengaja menulis teks langsung (hardcoded string) di dalam JSX. Untuk mencegah hal ini, Anda dapat memasang plugin ESLint:

npm install -D eslint-plugin-i18next

Tambahkan pengaturan di berkas konfigurasi .eslintrc.json:

{
  "plugins": ["i18next"],
  "rules": {
    "i18next/no-literal-string": ["warn", {
      "markupOnly": true,
      "ignoreAttribute": ["className", "style", "type", "id", "href", "src"]
    }]
  }
}

Aturan ini akan memberikan peringatan jika ada teks literal yang ditulis di JSX tanpa menggunakan pemanggilan fungsi t().

3. Penanganan Kunci Terjemahan yang Hilang (Fallback)

Jika kunci terjemahan tidak ditemukan pada file JSON, secara default next-intl akan mengembalikan nama kunci itu sendiri (misal Dashboard.welcome). Anda dapat mengonfigurasi fallback khusus di src/i18n/request.ts jika ingin menampilkan teks default atau pesan tertentu agar UI tidak rusak.


G. Optimasi SEO untuk Website Multibahasa

Untuk memastikan robot perayap mesin pencari (seperti Googlebot) dapat memahami variasi bahasa di situs Anda dengan benar:

  1. Gunakan Tag hreflang: Informasikan kepada mesin pencari semua URL alternatif yang memiliki konten serupa dalam bahasa berbeda.
  2. Gunakan Tag <html lang="...">: Pastikan elemen HTML memiliki atribut bahasa yang akurat sesuai bahasa aktif di halaman saat ini.
  3. Hindari Redireksi Berdasarkan IP secara Agresif: Jangan langsung memaksa pengguna berpindah bahasa secara otomatis berdasarkan alamat IP mereka tanpa memberikan opsi kembali. Tindakan ini menyulitkan Googlebot untuk mengindeks halaman dengan variasi bahasa non-default.

Di Next.js, Anda dapat mengimplementasikan konfigurasi metadata bahasa secara dinamis:

// src/app/[locale]/layout.tsx
import {Metadata} from 'next';
 
type Props = {
  params: {locale: string};
};
 
export async function generateMetadata({params: {locale}}: Props): Promise<Metadata> {
  return {
    title: locale === 'id' ? 'Beranda' : 'Home',
    alternates: {
      languages: {
        'id-ID': '/id',
        'en-US': '/en'
      }
    }
  };
}

H. Referensi Tambahan

© 2026 Bit Group Asia. All rights reserved.

ArchitectureSecurityGuidelines