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>
);
}

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`;
};

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>
);
}


Keuntungan approach ini:
- Loading lebih cepat - Cuma load translation yang dibutuhin
- User experience bagus - Ada loading state yang jelas
- Error handling - Kalau gagal load, ada fallback
- 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!