Tutorial Lengkap Internationalization Next.js dengan next-intl: Panduan 2025

Apa itu i18n dan mengapa penting

Pernah gak sih kamu mikir gimana caranya website besar kayak Netflix atau Spotify bisa nampil dalam berbagai bahasa?

Apa itu Internationalization (i18n)?

Internationalization atau yang biasa disingkat i18n (karena ada 18 huruf antara 'i' dan 'n') adalah proses mempersiapkan aplikasi web supaya bisa mendukung berbagai bahasa dan budaya.

Bukan cuma soal nerjemahin teks aja lho! Ada banyak aspek yang harus diperhatiin:

  • Format tanggal (DD/MM/YYYY vs MM/DD/YYYY)
  • Mata uang (Rupiah vs Dollar vs Euro)
  • Format angka (1,000 vs 1.000)
  • Arah baca teks (kiri ke kanan untuk bahasa Latin, kanan ke kiri untuk bahasa Arab)

Kenapa i18n Penting di Era Digital?

Di era digital sekarang ini, membuat aplikasi yang cuma support satu bahasa itu kayak ngebatesin diri sendiri. Bayangin aja, internet itu diakses sama orang dari seluruh dunia. Kalau aplikasi kamu cuma bahasa Indonesia doang, ya pasti bakal kehilangan potensi user dari negara lain kan?

Impact untuk Developer dan Bisnis

Untuk developer yang kerja di startup atau perusahaan yang pengen go international, skill i18n ini wajib banget dikuasai. Soalnya implementasi internationalization yang salah bisa jadi mimpi buruk - mulai dari layout yang rusak sampai performance yang menurun drastis.

Keuntungan menggunakan next-intl vs alternatif lain

Sebelum kita masuk ke next-intl, mungkin kamu bertanya-tanya kenapa harus pake library ini? Kan bisa aja pake react-i18next atau bahkan bikin sistem sendiri?

Didesign Khusus untuk Next.js

next-intl ini emang didesign khusus untuk Next.js. Jadi dia udah optimized banget untuk fitur-fitur Next.js yang terbaru:

  • Server Side Rendering (SSR)
  • Static Site Generation (SSG)
  • App Router Next.js 13+

Ini yang bikin dia unggul dibanding alternatif lain.

Integrasi Seamless dengan App Router

Pertama, next-intl punya integrasi yang seamless sama App Router Next.js 13+. Dia support middleware untuk routing berdasarkan bahasa, jadi URL kamu bisa jadi kayak /en/about atau /id/tentang. Keren kan?

Performance yang Optimal

Kedua, performance-wise dia lebih optimal. Kenapa? Karena next-intl cuma load translation files yang dibutuhin aja per halaman. Jadi gak kayak beberapa library lain yang load semua translation sekaligus dan bikin bundle size membengkak.

Developer Experience yang Top

Ketiga, developer experience-nya juga top. Dia punya:

  • TypeScript support yang bagus banget
  • Auto-completion untuk translation keys
  • Error handling yang informatif
  • Jadi kalau ada typo di translation key, langsung ketauan

Community Support yang Solid

Yang paling penting, next-intl ini aktif banget development-nya dan community supportnya solid. Regular updates, bug fixes yang cepat, dan dokumentasi yang lengkap. Beda sama beberapa library i18n lain yang udah jarang di-maintain.

Instalasi next-intl di proyek Next.js

Oke, sekarang kita masuk ke bagian yang seru nih - setup project dari awal! Pertama-tama pastikan kamu udah punya project Next.js dengan App Router. Kalau belum, bisa bikin dulu pake command berikut:

npx create-next-app@latest bwa-intl
cd bwa-intl

Pastikan pilih opsi App Router waktu setup ya, karena kita bakal pake fitur-fitur terbaru Next.js. Setelah project Next.js kamu siap, sekarang waktunya install next-intl. Jalankan command ini di terminal:

npm install next-intl

Simple kan? Cuma satu library doang yang perlu diinstall. Ini salah satu kelebihan next-intl dibanding alternatif lain - dependency-nya minimal banget dan gak ada conflict dengan dependencies lain.

Setelah berhasil diinstall, kita bisa cek di package.json apakah next-intl udah masuk ke dependencies. Pastikan juga Next.js kamu versi 13 ke atas ya, karena next-intl butuh App Router yang baru ada di versi tersebut.

Untuk tutorial kali ini, kita mulai dengan approach tanpa routing dulu supaya lebih mudah dipahami. Nanti di bagian selanjutnya baru kita implementasi locale-based routing kayak /en/courses atau /id/kursus.

Konfigurasi dasar next.config.ts

Setelah instalasi berhasil, langkah selanjutnya adalah setup plugin next-intl di file next.config.ts. File ini adalah jantung konfigurasi Next.js kita dan plugin ini bakal handle linking antara konfigurasi i18n dengan next-intl.

Buka file next.config.ts di root project dan update seperti ini:

import {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

const nextConfig: NextConfig = {};

const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

Plugin ini bakal handle beberapa hal penting secara otomatis:

  • Membuat linking ke file konfigurasi i18n yang bakal kita buat
  • Optimasi bundling untuk translation files
  • Setup yang diperlukan untuk Server Components

Perlu diingat, kalau kamu udah punya konfigurasi lain di next.config.ts (kayak images, experimental features, atau plugins lain), tinggal gabungin aja:

import {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin();

const nextConfig: NextConfig = {
  images: {
    domains: ['buildwithangga.com'],
  },
  experimental: {
    serverActions: true,
  },
};

export default withNextIntl(nextConfig);

TypeScript memberikan beberapa keuntungan disini:

  • Auto-completion untuk NextConfig properties
  • Type checking untuk mencegah typo di konfigurasi
  • Better IDE support dengan intellisense

Struktur folder untuk file terjemahan

Nah, sekarang kita setup struktur folder dan file-file yang diperlukan. Ini penting banget supaya kode kita tetep organized dan sesuai dengan standar next-intl terbaru.

Pertama, kita bakal bikin struktur folder kayak gini:

bwa-intl/
├── messages/
│   ├── en.json
│   └── id.json
├── next.config.ts
└── src/
    ├── i18n/
    │   └── request.ts
    └── app/
        ├── layout.tsx
        └── page.tsx

Step 1: Buat folder dan file messages

Buat folder messages di root project kamu (sejajar sama package.json). Di dalam folder ini, bikin file JSON untuk setiap bahasa.

File messages/en.json untuk bahasa English:

{
  "HomePage": {
    "title": "Learn Programming with BuildWithAngga",
    "subtitle": "Premium online courses for modern developers",
    "cta": "Browse Courses"
  },
  "Navigation": {
    "home": "Home",
    "courses": "Courses",
    "about": "About",
    "contact": "Contact"
  }
}

File messages/id.json untuk bahasa Indonesia:

{
  "HomePage": {
    "title": "Belajar Programming dengan BuildWithAngga",
    "subtitle": "Kursus online premium untuk developer modern",
    "cta": "Lihat Kursus"
  },
  "Navigation": {
    "home": "Beranda",
    "courses": "Kursus",
    "about": "Tentang",
    "contact": "Kontak"
  }
}

Step 2: Setup file konfigurasi i18n/request.ts

File ini adalah konfigurasi utama next-intl yang bakal di-call setiap ada request. Buat file src/i18n/request.ts:

import {getRequestConfig} from 'next-intl/server';

export default getRequestConfig(async () => {
  // Static dulu, nanti kita ubah jadi dynamic
  const locale = 'en';

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default
  };
});

File ini supported out-of-the-box dengan path ./i18n/request.ts baik di folder src maupun di project root. Ekstensinya bisa .ts, .tsx, .js, atau .jsx.

Step 3: Update app/layout.tsx

Biar konfigurasi bisa diakses di Client Components, wrap children di root layout dengan NextIntlClientProvider:

import {NextIntlClientProvider} from 'next-intl';
import {ReactNode} from 'react';

interface RootLayoutProps {
  children: ReactNode;
}

export default async function RootLayout({children}: RootLayoutProps) {
  return (
    <html>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

Step 4: Test di app/page.tsx

Sekarang kita bisa test translation di homepage. Update file app/page.tsx:

import {useTranslations} from 'next-intl';

export default function HomePage(): JSX.Element {
  const t = useTranslations('HomePage');

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('subtitle')}</p>
      <button>{t('cta')}</button>
    </div>
  );
}

Kalau kamu pake async component, bisa pake getTranslations function:

import {getTranslations} from 'next-intl/server';

export default async function HomePage(): Promise<JSX.Element> {
  const t = await getTranslations('HomePage');

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('subtitle')}</p>
      <button>{t('cta')}</button>
    </div>
  );
}

Dengan menggunakan TypeScript, kita dapat:

  • Type safety untuk translation keys
  • Auto-completion di IDE
  • Runtime error prevention
  • Better code maintainability

Struktur yang rapi kayak gini bakal ngebantu banget pas project udah gede dan developer lain juga bisa dengan mudah ngerti flow internationalization-nya.

Routing berdasarkan locale

Oke, sekarang kita mau bikin website yang bisa ganti bahasa kayak di contoh course page di atas. Coba klik dropdown bahasa dan lihat gimana harga langsung berubah dari Dollar ke Rupiah - itu yang namanya internationalization!

Langkah 1: Bikin file routing

Buat folder i18n di dalam src, terus bikin file baru:

// src/i18n/routing.ts
import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['en', 'id'],
  defaultLocale: 'en'
});

Langkah 2: Bikin middleware

Bikin file baru di src/middleware.ts:

// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)'
};

Langkah 3: Pindahin page ke folder [locale]

Bikin folder baru: src/app/[locale] terus pindahin file layout.tsx dan page.tsx kesana.

Update layout.tsx:

// src/app/[locale]/layout.tsx
import {NextIntlClientProvider} from 'next-intl';

export default async function LocaleLayout({children, params}) {
  const {locale} = await params;

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Format mata uang dan tanggal

Sekarang kita bikin supaya harga otomatis berubah sesuai bahasa yang dipilih.

Update file messages

File messages/en.json:

{
  "title": "Complete Web Development Course",
  "price": "Price",
  "enroll": "Enroll Now"
}

File messages/id.json:

{
  "title": "Kursus Lengkap Pengembangan Web",
  "price": "Harga",
  "enroll": "Daftar Sekarang"
}

Bikin halaman dengan Tailwind CSS

File src/app/[locale]/page.tsx:

// src/app/[locale]/page.tsx
import {useTranslations, useLocale} from 'next-intl';

export default function HomePage() {
  const t = useTranslations();
  const locale = useLocale();

  // Harga berbeda sesuai mata uang
  const price = locale === 'id' ? 299000 : 25;
  const originalPrice = locale === 'id' ? 499000 : 45;
  const studentCount = 1847;

  // Format mata uang
  const formatPrice = (amount) => {
    if (locale === 'id') {
      return `Rp${amount.toLocaleString('id-ID')}`;
    }
    return `$${amount}`;
  };

  // Format tanggal
  const publishDate = new Date('2024-02-20');
  const formattedDate = publishDate.toLocaleDateString(locale);

  // Pluralization - singular vs plural
  const getStudentText = (count) => {
    if (locale === 'id') {
      return `${count.toLocaleString('id-ID')} siswa`;
    }
    return count === 1 ? '1 student' : `${count.toLocaleString('en-US')} students`;
  };

  return (
    <div className="p-6 max-w-2xl mx-auto">
      <h1 className="text-3xl font-bold text-gray-800 mb-6">
        {t('title')}
      </h1>

      {/* Pricing Card */}
      <div className="bg-blue-50 p-6 rounded-xl mb-6 border border-blue-200">
        <h2 className="text-2xl font-semibold text-gray-800 mb-2">
          {t('price')}: {formatPrice(price)}
        </h2>
        <p className="text-gray-500 line-through mb-2">
          Was: {formatPrice(originalPrice)}
        </p>
        <p className="text-green-600 font-bold text-lg">
          {locale === 'id' ? 'Hemat' : 'Save'} {formatPrice(originalPrice - price)}!
        </p>
      </div>

      {/* Pluralization Demo */}
      <div className="bg-yellow-50 p-4 rounded-lg mb-6 border border-yellow-200">
        <p className="text-lg font-medium text-gray-800 mb-2 flex items-center gap-2">
          📊 {getStudentText(studentCount)} enrolled
        </p>
        <p className="text-sm text-gray-600">
          {locale === 'id' ?
            'Lihat gimana angka 1.847 vs 1,847 dan "siswa" gak berubah bentuk' :
            'Notice how 1,847 vs 1.847 and "students" changes to singular/plural'
          }
        </p>
      </div>

      <button className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors duration-200 mb-6">
        {t('enroll')}
      </button>

      <p className="text-sm text-gray-500">
        Published: {formattedDate}
      </p>
    </div>
  );
}

image.png

Pluralization (singular vs plural)

Nah, sekarang kita udah lihat contoh pluralization di page.tsx di atas. Lihat gimana "1.847 siswa" vs "1,847 students" - angkanya beda format dan kata "siswa" gak berubah bentuk, tapi "students" bisa jadi "student" kalau cuma 1.

Ini kenapa pluralization penting:

  • Bahasa English: 1 student, 2 students (ada tambahan 's')
  • Bahasa Indonesia: 1 siswa, 2 siswa (gak berubah)
  • Format angka juga beda: 1,847 (English) vs 1.847 (Indonesia)

Contoh untuk shopping cart

Coba klik tombol Enroll di course page di atas, lihat text cart berubah!

const getCartText = (itemCount) => {
  if (locale === 'id') {
    return itemCount === 0 ? 'Keranjang kosong' : `${itemCount} item`;
  }

  if (itemCount === 0) return 'Cart empty';
  return itemCount === 1 ? '1 item' : `${itemCount} items`;
};

image.png

Loading messages per halaman

Kalau website udah besar, kita bisa pisah translation per halaman biar loading lebih cepat. Jadi daripada load semua translation sekaligus, kita load cuma yang dibutuhin aja.

Pisah messages

Ganti struktur jadi:

messages/
├── en/
│   ├── common.json    ← Header, button, loading text
│   └── course.json    ← Khusus halaman course
└── id/
    ├── common.json
    └── course.json

File messages/en/common.json:

{
  "loading": "Loading course...",
  "error": "Failed to load",
  "header": "BuildWithAngga"
}

File messages/id/common.json:

{
  "loading": "Memuat kursus...",
  "error": "Gagal mengambil data",
  "header": "BuildWithAngga"
}

File messages/en/course.json:

{
  "title": "Complete Web Development Course",
  "description": "Learn React, Next.js and more",
  "instructor": "Instructor",
  "duration": "Duration",
  "students": "students enrolled"
}

File messages/id/course.json:

{
  "title": "Kursus Lengkap Pengembangan Web",
  "description": "Belajar React, Next.js dan lainnya",
  "instructor": "Instruktur",
  "duration": "Durasi",
  "students": "siswa"
}

Component dengan loading states

File src/components/CourseDetail.tsx:

// src/components/CourseDetail.tsx

"use client";

import { useLocale, useTranslations } from "next-intl";
import { useEffect, useState } from "react";

interface CourseDetailProps {
  courseId: string;
}

interface CourseMessages {
  title: string;
  description: string;
  instructor: string;
  duration: string;
  students: string;
}

export default function CourseDetail({ courseId }: CourseDetailProps) {
  const [courseMessages, setCourseMessages] = useState<CourseMessages | null>(
    null,
  );
  const [isLoading, setIsLoading] = useState(true);
  const locale = useLocale();
  const t = useTranslations();

  useEffect(() => {
    let isMounted = true;

    const loadCourseMessages = async () => {
      try {
        const messages = await import(`../../messages/${locale}/course.json`);
        if (isMounted) {
          setCourseMessages(messages.default as CourseMessages);
        }
      } catch (error) {
        console.error("Error loading course messages:", error);
      } finally {
        if (isMounted) {
          setIsLoading(false);
        }
      }
    };

    loadCourseMessages();

    return () => {
      isMounted = false;
    };
  }, [locale]);

  // Loading state
  if (isLoading) {
    return (
      <div className="p-10 text-center bg-gray-50 rounded-lg">
        <div className="text-4xl mb-3">⏳</div>
        <p className="text-gray-600">{t("loading")}</p>
      </div>
    );
  }

  // Error state
  if (!courseMessages) {
    return (
      <div className="p-10 text-center bg-yellow-50 rounded-lg border border-yellow-200">
        <div className="text-4xl mb-3">❌</div>
        <p className="text-yellow-800">{t("error")}</p>
      </div>
    );
  }

  // Success state
  return (
    <div className="p-6">
      <h1 className="text-3xl font-bold text-gray-800 mb-6">
        {courseMessages.title}
      </h1>

      <div className="bg-blue-50 p-6 rounded-lg mb-6 border border-blue-200">
        <p className="font-semibold text-gray-800 mb-3">
          {courseMessages.description}
        </p>
        <p className="text-gray-600">
          {courseMessages.instructor}: Angga Risky | {courseMessages.duration}:
          8 hours
        </p>
      </div>

      <p className="text-green-600 font-bold flex items-center gap-2">
        <span>✅</span>
        1,847 {courseMessages.students}
      </p>
    </div>
  );
}

Cara pakainya di page

File src/app/[locale]/courses/[id]/page.tsx:

// src/app/[locale]/courses/[id]/page.tsx

import { setRequestLocale } from "next-intl/server";
import CourseDetail from "@/components/CourseDetail";

interface CourseDetailPageProps {
  params: {
    locale: string;
    id: string;
  };
}

export default async function CourseDetailPage({
  params,
}: CourseDetailPageProps) {
  const { locale, id } = params;

  setRequestLocale(locale);

  return (
    <div className="max-w-4xl mx-auto p-6">
      <CourseDetail courseId={id} />
    </div>
  );
}

image.png
image.png

Keuntungan approach ini:

  1. Loading lebih cepat - Cuma load translation yang dibutuhin
  2. User experience bagus - Ada loading state yang jelas
  3. Error handling - Kalau gagal load, ada fallback
  4. Scalable - Gampang tambah halaman baru

Tips praktis:

  • Selalu kasih loading state yang jelas
  • Jangan lupa error handling
  • Test dengan internet lambat biar tau user experience-nya
  • Common messages (header, button) tetep di-load di awal

Yang paling penting: jangan takut salah! Coba-coba dulu, nanti juga terbiasa.

Test sekarang - coba switch bahasa di course page dan lihat magic-nya!

Penutup

Nah, kita udah sampe di akhir tutorial internationalization Next.js dengan next-intl ini. Dari awal setup sampai implementasi fitur lanjutan, kamu udah belajar gimana caranya bikin website yang bisa diakses sama user dari berbagai negara dengan bahasa yang berbeda.

Ingat, internationalization bukan cuma soal nerjemahin teks aja. Kamu juga udah belajar tentang format mata uang, tanggal, pluralization, dan lazy loading yang semua itu penting banget buat user experience yang optimal. Website kayak BuildWithAngga yang punya user global pasti butuh fitur-fitur ini supaya setiap user merasa nyaman pake platform kita.

Yang paling penting dari tutorial ini adalah kamu udah praktek langsung dengan contoh real course page. Coba inget lagi gimana harga otomatis berubah dari Dollar ke Rupiah pas kamu ganti bahasa, atau gimana text "students" jadi "siswa" dengan format angka yang berbeda. Itu semua hasil dari implementasi yang kita bahas step by step tadi.

Jangan lupa, internationalization ini skill yang sangat dicari di industri teknologi sekarang. Startup dan perusahaan besar kayak Gojek, Tokopedia, sampai Netflix semua butuh developer yang bisa bikin aplikasi multilingual. Jadi skill yang kamu pelajari hari ini bisa jadi modal buat karir kamu kedepannya.

Kalau kamu mau belajar lebih dalam lagi tentang Next.js dan pengembangan web modern, BuildWithAngga punya kursus-kursus lengkap yang bisa ngebantu kamu jadi full-stack developer yang handal. Di BuildWithAngga kamu bakal dapet pembelajaran yang terstruktur, mentor berpengalaman, dan yang pasti praktek dengan project real-world kayak yang udah kita coba di tutorial ini.

Keep practicing dan jangan takut buat eksperimen dengan fitur-fitur lain dari next-intl. Developer terbaik itu yang selalu penasaran dan mau terus belajar hal baru. Good luck dengan journey programming kamu!