Belajar Routing i18n next-intl di Next.js untuk Pemula
Belajar routing i18n next-intl di Next.js: prefix locale, middleware, getTranslations vs useTranslations, Link terlokalisasi, static params, dan hreflang.
Oleh Matthews WongDitinjau pada
Poin utama
next-intl menambahkan routing berbasis locale ke App Router Next.js. Setiap URL diawali locale seperti /en atau /id, middleware menegosiasikan dan mengarahkan ke sana, folder [locale] meneruskannya ke setiap halaman, dan satu request config memuat messages untuk locale itu. Pelajaran ini mengikuti satu request lewat file nyata situs ini.
Setelah pelajaran ini, Anda bisa
- Menelusuri sebuah request dari URL polos sampai halaman terjemahan, file demi file.
- Memilih getTranslations atau useTranslations, serta Link, router, dan static params yang tepat.
- Menghasilkan hreflang yang diterima mesin pencari, sekali saja, dari satu tempat.
Ikuti diagramnya langkah demi langkah
Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.
Satu request melewati next-intl
Path polos diarahkan middleware ke prefix locale, layout locale memvalidasinya, request.ts memuat messages, halaman merender HTML terjemahan, dan alternates menghasilkan hreflang.
Tombol: ← → langkah · Home / End · K putar atau jeda
Satu request melewati lima tempat: middleware, layout locale, request config, file messages, dan halaman. Masing-masing punya satu tugas, dan URL membawa locale-nya.
Pengunjung membuka path polos /about dengan browser berbahasa Indonesia. Prefix locale tidak ada, jadi situs belum tahu bahasa mana yang harus disajikan.
Tebak langkah berikutnya Path itu tidak punya prefix locale. Apa yang dilakukan middleware terhadapnya? (Menyajikan Inggris tanpa redirect · Mengarahkan ke URL berprefix)next-intl menegosiasikan locale dari prefix, header accept-language, atau default, lalu mengarahkan path polos ke /id/about. Situs ini mematikan cookie locale.
localePrefix: "always",Tebak langkah berikutnya Layout menerima locale fr. Apa yang dilakukannya? (Beralih ke bahasa Inggris · Menjawab dengan 404)Layout memeriksa locale. id lolos; selainnya memanggil notFound. Hanya en dan id yang di-prerender, dan dynamicParams false mengubah path lain menjadi 404. setRequestLocale menjaga halaman tetap statis.
getRequestConfig di request.ts menentukan locale dan mengimpor messages-nya. Di sini id.json, yang struktur kuncinya harus sama persis dengan en.json.
Halaman async me-await getTranslations dengan locale dan namespace, lalu merender teks Indonesia ke HTML dengan lang bernilai id. Client component akan memanggil useTranslations.
Metadata halaman menghasilkan hreflang untuk en, id, dan x-default sebagai URL absolut. Halaman Inggris memuat set yang sama, sehingga tiap versi saling menunjuk.
Bagaimana routing locale bekerja di next-intl?
Anda mendeskripsikan locale sekali saja dengan defineRouting: daftar locale, locale default, dan perilaku prefix. Dengan localePrefix bernilai always, yang juga merupakan default, setiap pathname diawali locale, sehingga halaman About bahasa Inggris adalah /en/about. Situs ini memastikan pilihan itu di i18n/routing.ts agar tidak berubah tanpa sengaja.
Baca selengkapnya (2)Tutup
Middleware mengubah konfigurasi itu menjadi perilaku. Menurut dokumentasi next-intl, ia menangani negosiasi locale, menerapkan redirect dan rewrite, serta menyediakan alternate link untuk mesin pencari. Negosiasi membaca prefix locale lebih dulu, lalu cookie, lalu header accept-language, lalu locale default. File aplikasi kemudian dipindahkan ke folder [locale] tingkat atas, sehingga setiap layout dan page menerima locale sebagai param.
Perhatikan matcher. Middleware di sini melewati api, _next, dan path yang mengandung titik, sehingga file statis di public dilayani apa adanya. Situs ini memakai Next.js 15, jadi filenya middleware.ts. Dokumentasi next-intl mencatat bahwa proxy.ts bernama middleware.ts sampai Next.js 16.
getTranslations atau useTranslations: pakai yang mana?
Di dalam komponen yang bukan async, panggil hook useTranslations. next-intl memberi komponen bersama implementasi yang cocok untuk lingkungan tempatnya berjalan, jadi hook yang sama bekerja di Server maupun Client Component. Hook tidak bisa dipanggil dari komponen async. Async Server Component, seperti halaman About situs ini, memakai await getTranslations dengan locale dan namespace.
Baca selengkapnya (1)Tutup
Client Component hanya melihat messages yang Anda berikan. Messages yang dikirim ke NextIntlClientProvider dimasukkan ke halaman saat render streaming, jadi mengirim semuanya mudah tetapi menambah payload, dan dokumentasi menyarankan selektif serta mengukur dulu. Layout root situs ini membuang isi artikel blog dan teks game sebelum provider, dan halaman game menyediakan ulang namespace-nya sendiri.
Kenapa Link dan useRouter diimpor dari @/i18n/navigation?
createNavigation(routing), dipanggil sekali, mengembalikan pembungkus ringan di atas API navigasi Next.js: Link, redirect, usePathname, useRouter, dan getPathname. Pembungkus itu menangani locale pengguna dan pathname di balik layar. Link melokalkan pathname, useRouter melokalkan navigasi terprogram, dan usePathname mengembalikan path tanpa prefix locale, itulah sebabnya SiteChrome bisa membandingkannya dengan /play.
Baca selengkapnya (1)Tutup
Import polos kehilangan itu. Tautan dengan next/link menunjuk ke /about, path tanpa prefix, yang di situs ini hanya bisa dilayani setelah redirect. Redirect juga butuh locale. getPathname membangun path untuk locale tertentu, berguna untuk sitemap serta tautan canonical atau hreflang.
Bagaimana generateStaticParams dan setRequestLocale menjaga halaman tetap statis?
Karena [locale] adalah segmen dinamis, generateStaticParams harus mendaftar locale agar tiap route dirender saat build. Layout locale mengembalikan satu entri per locale. Next.js juga memungkinkan dynamicParams mengatur apa yang terjadi untuk param yang tidak terdaftar: bila false, path yang tidak terdaftar menjadi 404. Situs ini mengaturnya di layout locale, jadi /fr/about tidak pernah dirender.
Baca selengkapnya (1)Tutup
Dokumentasi next-intl memperingatkan bahwa saat ini next-intl beralih ke dynamic rendering ketika API seperti useTranslations dipakai di Server Component. Solusinya adalah setRequestLocale, dipanggil di setiap layout dan page sebelum fungsi next-intl apa pun. Dokumentasi kini menyebutnya API legacy, dan situs ini masih memakainya di Next.js 15.
Bagaimana menghasilkan hreflang yang diterima Google?
Aturan Google singkat. Setiap versi bahasa harus mencantumkan dirinya dan semua versi lain, anotasi harus timbal balik atau akan diabaikan, dan URL alternatif harus lengkap, termasuk https. Entri x-default adalah cadangan bagi pengunjung yang bahasanya tidak cocok dengan apa pun. Anda bisa memakai tag HTML, header HTTP, atau sitemap, dan memakai ketiganya tidak menambah manfaat.
Baca selengkapnya (1)Tutup
Field metadata Next.js alternates.languages merender tag link itu di head. Situs ini membuat tiga entri yang sama untuk setiap halaman dalam satu fungsi, buildLanguageAlternates, dan mematikan header bawaan next-intl dengan alternateLinks false. Alasannya ada di komentar kode: header middleware menunjuk x-default ke URL tanpa prefix yang melakukan redirect, sehingga berbeda dari HTML.
Bagaimana menjaga en.json dan id.json tetap sepadan?
Kunci yang hilang di satu bahasa adalah error runtime untuk bahasa itu, jadi aturan di sini tegas: kedua file harus punya struktur kunci yang persis sama. Untuk post blog, skrip merge membandingkan kedua struktur sebelum menulis apa pun dan gagal jika ada perbedaan. Untuk proyek Anda sendiri, tes kecil yang menelusuri kedua struktur adalah pengaman termurah yang bisa ditambahkan.
Konfigurasi nyata di balik pelajaran ini
Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.
export const routing = defineRouting({
locales,
defaultLocale: "en",
localePrefix: "always",
// Locale is always in the URL path, so the NEXT_LOCALE cookie is redundant.
// Disabling it removes the per-response Set-Cookie that blocks Cloudflare
// from edge-caching the static HTML.
localeCookie: false,
// Every page already emits hreflang in <head> via `alternates.languages`
// (see buildLanguageAlternates in lib/seo.ts). Leaving this on made the
// middleware emit a SECOND set as an HTTP Link header whose x-default
// pointed at the unprefixed "/blog/<slug>/" — a URL that 307-redirects,
// and a different target from the "/en/blog/<slug>/" in the HTML. Google
// may discard hreflang entirely when the annotations conflict, which on a
// bilingual site risks losing the en/id pairing.
alternateLinks: false,
});Seluruh kontrak routing. localePrefix always berarti /en/... dan /id/..., tidak pernah path polos. Cookie dimatikan karena locale sudah ada di URL, dan Set-Cookie di tiap respons menghalangi caching di edge. alternateLinks dimatikan karena HTML sudah membawa hreflang.
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// Ensure that a valid locale is used
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});Konfigurasi per request. requestLocale adalah promise, jadi di-await, dan nilai tidak valid jatuh ke default. Import dinamis hanya memuat messages untuk locale itu. Dokumentasi next-intl terbaru menandai requestLocale sebagai legacy dan menyarankan argumen locale, dan situs ini belum bermigrasi.
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);Satu-satunya tempat kelima pembungkus ini dibuat. Setiap Link, useRouter, dan usePathname di situs diimpor dari sini, bukan dari next/link atau next/navigation, sehingga semuanya membawa locale.
const { locale } = await params;
setRequestLocale(locale);
const pageLocale = asSiteLocale(locale);
const t = await getTranslations({ locale, namespace: "aboutPage" });Halaman async Server Component. Ia menyetel locale request lebih dulu, lalu await getTranslations dengan namespace. Hook tidak diizinkan di sini. Header, sebuah Client Component, memakai versi hook, useTranslations dengan namespace nav.
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}Satu entri per locale, sehingga segmen [locale] di-prerender untuk en dan id. Tepat di bawahnya di file yang sama, export const dynamicParams = false mengubah setiap locale lain menjadi 404 sebelum apa pun dirender.
export function buildLanguageAlternates(path = ""): Record<string, string> {
return {
en: buildCanonical("en", path),
id: buildCanonical("id", path),
"x-default": buildCanonical("en", path),
};
}Satu-satunya sumber hreflang. Tiga URL absolut untuk setiap halaman, dan x-default menunjuk ke bahasa Inggris. Halaman meneruskannya ke alternates.languages, yang dirender Next.js sebagai tag link di head.
Kesalahan yang benar-benar membuang waktu
Link dari next/link menghilangkan locale
Tautan dari next/link ke /about hanya berfungsi karena middleware mengarahkan path polos, dan useRouter dari next/navigation berperilaku sama. Itu memakan satu redirect dan bisa menghilangkan bahasa pengunjung. Impor Link, useRouter, dan usePathname dari file yang memanggil createNavigation.
Kunci ada di en.json tetapi tidak ada di id.json
Halaman bahasa Indonesia melempar error pada kunci yang hilang sementara halaman Inggris normal, sehingga baru ketahuan setelah ganti bahasa. Jaga kedua struktur identik dan periksa lewat tes, karena next-intl melempar error pada kunci yang hilang.
Dua set hreflang yang saling bertentangan
Middleware next-intl menambahkan hreflang sebagai header Link secara default, dan metadata menambahkannya di head. Di sini x-default pada header menunjuk ke URL yang melakukan redirect, target berbeda dari HTML. Anotasi yang bertentangan berisiko diabaikan, jadi keluarkan satu set saja.
Request file yang tidak ada berubah menjadi 500
Middleware melewati path yang mengandung titik, sehingga path gambar yang tidak ada bisa mencapai router dan cocok dengan [locale] berisi locale asal. Merender itu membuat next-intl membaca header request saat render statis, dan Next.js menjawab 500. Menolak param tak dikenal di awal dengan dynamicParams false membuatnya menjadi 404 yang bersih.
| getTranslations | useTranslations | |
|---|---|---|
| Gaya | Fungsi yang di-await | Hook |
| Async Server Component | Pakai ini | Tidak diizinkan |
| Client Component | Pakai ini | Pakai ini |
| Butuh messages di provider klien | Tidak | Ya, di Client Component |
Lakukan di komputer Anda sendiri
Anda butuh proyek Next.js App Router dengan next-intl terpasang. Anda akan menambah locale kedua dan melihat middleware mengarahkan path polos.
Deskripsikan locale sekali saja, dengan prefix selalu ada.
// i18n/routing.ts import { defineRouting } from "next-intl/routing"; export const routing = defineRouting({ locales: ["en", "id"], defaultLocale: "en", localePrefix: "always", });Buat pembungkusnya sekali, dan impor di mana-mana sebagai pengganti next/link.
// i18n/navigation.ts import { createNavigation } from "next-intl/navigation"; import { routing } from "./routing"; export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);Tambahkan middleware dengan matcher yang melewati api, _next, dan file bertitik.
// middleware.ts import createMiddleware from "next-intl/middleware"; import { routing } from "./i18n/routing"; export default createMiddleware(routing); export const config = { matcher: ["/((?!api|_next|.*\\..*).*)"], };Pindahkan halaman ke app/[locale]. Di layout, validasi locale, daftarkan kedua locale, dan tolak sisanya.
// app/[locale]/layout.tsx import { notFound } from "next/navigation"; import { routing } from "@/i18n/routing"; export const dynamicParams = false; export function generateStaticParams() { return routing.locales.map((locale) => ({ locale })); }Saat dev server berjalan, minta path polos dan baca redirect-nya. Lalu minta locale yang tidak pernah Anda daftarkan.
curl -sI -H 'Accept-Language: id' http://localhost:3000/about | head -n 3 curl -sI http://localhost:3000/fr/about | head -n 1
Tiga pertanyaan sebelum lanjut
Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.
Jawab dalam hati, lalu cek
1.Apa yang diubah localePrefix always pada URL?Lihat jawabannya
Jawaban:Setiap pathname diawali locale, sehingga halaman About adalah /en/about dan /id/about, tanpa versi tanpa prefix untuk dilayani. Path polos diarahkan ke yang berprefix.
2.Sebutkan dua hal yang menghentikan /fr/about dari dirender.Lihat jawabannya
Jawaban:generateStaticParams hanya mendaftar en dan id, dan dynamicParams false mengubah setiap locale yang tidak terdaftar menjadi 404. Layout juga memanggil notFound bila locale tidak ada di routing.locales.
3.Untuk apa x-default di hreflang?Lihat jawabannya
Jawaban:Ia adalah cadangan bagi pengunjung yang bahasa atau regionnya tidak cocok dengan versi mana pun yang terdaftar. Situs ini mengarahkannya ke halaman bahasa Inggris.
Bahasan mendalam terkait di situs ini
Sumber
Sumber primer yang diperiksa pada tanggal riset. Ikuti tautannya untuk versi terbaru.
- Dokumentasi next-intl: setup routing
- Dokumentasi next-intl: konfigurasi routing
- Dokumentasi next-intl: API navigasi
- Dokumentasi next-intl: Server dan Client Component
- Dokumentasi Next.js: generateStaticParams
- Dokumentasi Next.js: generateMetadata dan alternates
- Google Search Central: versi terlokalisasi dan hreflang