Ajukan pertanyaan Anda dan dapatkan ringkasan dokumen dengan merujuk halaman ini dan penyedia AI pilihan Anda
Konten halaman ini diterjemahkan menggunakan AI.
Lihat versi terakhir dari konten aslinya dalam bahasa InggrisIf you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
Menerjemahkan website Next.js 15 Anda menggunakan next-i18next dengan Intlayer | Internasionalisasi (i18n)
Untuk siapa panduan ini
- Junior: Ikuti langkah-langkah persis dan salin blok kode. Anda akan mendapatkan aplikasi multibahasa yang berfungsi.
- Mid-level: Gunakan daftar periksa dan catatan praktik terbaik untuk menghindari kesalahan umum.
- Senior: Baca sekilas struktur tingkat tinggi, SEO, dan bagian otomatisasi; Anda akan menemukan pengaturan default yang masuk akal dan titik ekstensi.
Apa yang akan Anda bangun
- Proyek App Router dengan rute yang dilokalkan (misalnya,
/,/fr/...) - Konfigurasi i18n dengan locale, locale default, dukungan RTL
- Inisialisasi i18n sisi server dan penyedia klien
- Terjemahan dengan namespace yang dimuat sesuai permintaan
- SEO dengan
hreflang,sitemapyang dilokalkan,robots - Middleware untuk routing locale
- Integrasi Intlayer untuk mengotomatisasi alur kerja terjemahan (tes, pengisian AI, sinkronisasi JSON)
Catatan: next-i18next dibangun di atas i18next. Panduan ini menggunakan primitif i18next yang kompatibel dengan next-i18next dalam App Router, sambil menjaga arsitektur tetap sederhana dan siap produksi. Untuk perbandingan yang lebih luas, lihat next-i18next vs next-intl vs Intlayer.
1) Struktur proyek
Pasang dependensi next-i18next -
Salin kode ke clipboard
npm install next-i18next i18next react-i18next i18next-resources-to-backendMulailah dengan struktur yang jelas. Simpan pesan yang dipisahkan berdasarkan locale dan namespace.
Salin kode ke clipboard
.├── i18n.config.ts└── src ├── locales │ ├── en │ │ ├── common.json │ │ └── about.json │ └── fr │ ├── common.json │ └── about.json ├── app │ ├── i18n │ │ └── server.ts │ └── [locale] │ ├── layout.tsx │ └── about.tsx └── components ├── I18nProvider.tsx ├── ClientComponent.tsx └── ServerComponent.tsxDaftar periksa (mid/senior):
- Simpan satu JSON per namespace per locale
- Jangan terlalu memusatkan pesan; gunakan namespace kecil yang terfokus pada halaman/fitur
- Hindari mengimpor semua locale sekaligus; muat hanya yang Anda butuhkan
2) Pasang dependensi
Salin kode ke clipboard
bashpnpm add i18next react-i18next i18next-resources-to-backendJika Anda berencana menggunakan API atau interoperabilitas konfigurasi next-i18next, juga jalankan:
Salin kode ke clipboard
pnpm add next-i18next3) Konfigurasi inti i18n
Tentukan locales, locale default, RTL, dan helper untuk path/URL yang dilokalkan.
Salin kode ke clipboard
export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export const rtlLocales = ["ar", "he", "fa", "ur"] as const;export const isRtl = (locale: string) => (rtlLocales as readonly string[]).includes(locale);export function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";export function abs(locale: string, path: string) { return ORIGIN + localizedPath(locale, path);}Catatan senior: Jika Anda menggunakan next-i18next.config.js, pastikan untuk menyelaraskannya dengan i18n.config.ts agar tidak terjadi perbedaan.
4) Inisialisasi i18n sisi server
Inisialisasi i18next di server dengan backend dinamis yang hanya mengimpor JSON locale/namespace yang diperlukan.
Salin kode ke clipboard
import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";// Memuat sumber daya JSON dari src/locales/<locale>/<namespace>.jsonconst backend = resourcesToBackend( (locale: string, namespace: string) => import(`../../locales/${locale}/${namespace}.json`));export async function initI18next( locale: string, namespaces: string[] = ["common"]) { const i18n = createInstance(); await i18n .use(initReactI18next) .use(backend) .init({ lng: locale, fallbackLng: defaultLocale, ns: namespaces, defaultNS: "common", interpolation: { escapeValue: false }, react: { useSuspense: false }, }); return i18n;}Catatan tengah: Jaga daftar namespace agar tetap singkat per halaman untuk membatasi payload. Hindari bundle global “catch-all”.
5) Provider klien untuk komponen React
Bungkus komponen klien dengan provider yang mencerminkan konfigurasi server dan hanya memuat namespace yang diminta.
Salin kode ke clipboard
"use client";import * as React from "react";import { I18nextProvider } from "react-i18next";import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";const backend = resourcesToBackend( (locale: string, namespace: string) => import(`../../locales/${locale}/${namespace}.json`));type Props = { locale: string; namespaces?: string[]; resources?: Record<string, any>; // { ns: bundle } children: React.ReactNode;};export default function I18nProvider({ locale, namespaces = ["common"], resources, children,}: Props) { const [i18n] = React.useState(() => { const i = createInstance(); i.use(initReactI18next) .use(backend) .init({ lng: locale, fallbackLng: defaultLocale, ns: namespaces, resources: resources ? { [locale]: resources } : undefined, defaultNS: "common", interpolation: { escapeValue: false }, react: { useSuspense: false }, }); return i; }); return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;}Tips junior: Anda tidak perlu mengirim semua pesan ke klien. Mulailah hanya dengan namespace halaman saja.
6) Tata letak dan rute yang dilokalkan
Atur bahasa dan arah, serta pra-generate rute per locale untuk mendukung rendering statis.
Salin kode ke clipboard
import type { ReactNode } from "react";import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";export const dynamicParams = false;export function generateStaticParams() { return locales.map((locale) => ({ locale }));}export default function LocaleLayout({ children, params,}: { children: ReactNode; params: { locale: string };}) { const locale: Locale = (locales as readonly string[]).includes(params.locale) ? params.locale : defaultLocale; const dir = isRtl(locale) ? "rtl" : "ltr"; return ( <html lang={locale} dir={dir}> <body>{children}</body> </html> );}7) Contoh halaman dengan penggunaan server + client
Salin kode ke clipboard
import I18nProvider from "@/components/I18nProvider";import { initI18next } from "@/app/i18n/server";import type { Locale } from "@/i18n.config";import ClientComponent from "@/components/ClientComponent";import ServerComponent from "@/components/ServerComponent";// Memaksa rendering statis untuk halaman iniexport const dynamic = "force-static";export default async function AboutPage({ params: { locale },}: { params: { locale: Locale };}) { const namespaces = ["common", "about"] as const; const i18n = await initI18next(locale, [...namespaces]); const tAbout = i18n.getFixedT(locale, "about"); return ( <I18nProvider locale={locale} namespaces={[...namespaces]}> <main> <h1>{tAbout("title")}</h1> <ClientComponent /> <ServerComponent t={tAbout} locale={locale} count={0} /> </main> </I18nProvider> );}Terjemahan (satu JSON per namespace di bawah src/locales/...):
Salin kode ke clipboard
{ "title": "Tentang", "description": "Deskripsi halaman tentang", "counter": { "label": "Penghitung", "increment": "Tambah" }}Salin kode ke clipboard
{ "title": "À propos", "description": "Description de la page À propos", "counter": { "label": "Compteur", "increment": "Incrémenter" }}Komponen client (memuat hanya namespace yang diperlukan):
Salin kode ke clipboard
"use client";import React, { useState } from "react";import { useTranslation } from "react-i18next";const ClientComponent = () => { const { t, i18n } = useTranslation("about"); const [count, setCount] = useState(0); const numberFormat = new Intl.NumberFormat(i18n.language); return ( <div> <p>{numberFormat.format(count)}</p> <button aria-label={t("counter.label")} onClick={() => setCount((c) => c + 1)} > {t("counter.increment")} </button> </div> );};export default ClientComponent;Pastikan halaman/provider hanya menyertakan namespace yang Anda butuhkan (misalnya,
about). Jika Anda menggunakan React < 19, gunakan memo untuk formatter berat sepertiIntl.NumberFormat.
Komponen server sinkron yang disematkan di bawah batas klien:
Salin kode ke clipboard
type ServerComponentProps = { t: (key: string) => string; locale: string; count: number;};const ServerComponent = ({ t, locale, count }: ServerComponentProps) => { const formatted = new Intl.NumberFormat(locale).format(count); return ( <div> <p>{formatted}</p> <button aria-label={t("counter.label")}>{t("counter.increment")}</button> </div> );};export default ServerComponent;8) SEO: Metadata, Hreflang, Sitemap, Robots
Menerjemahkan konten adalah cara untuk meningkatkan jangkauan. Sambungkan SEO multibahasa secara menyeluruh.
Praktik terbaik:
- Atur
langdandirdi root - Tambahkan
alternates.languagesuntuk setiap locale (+x-default) - Daftarkan URL yang diterjemahkan di
sitemap.xmldan gunakanhreflang - Kecualikan area privat yang dilokalkan (misalnya,
/fr/admin) dirobots.txt
Salin kode ke clipboard
import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; // Impor bundel JSON yang benar dari src/locales const messages = (await import("@/locales/" + locale + "/about.json")) .default; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, "/about")]) ); return { title: messages.title, description: messages.description, alternates: { canonical: localizedPath(locale, "/about"), languages: { ...languages, "x-default": "/about" }, }, };}export default async function AboutPage() { return <h1>Tentang</h1>;}Salin kode ke clipboard
import type { MetadataRoute } from "next";import { locales, defaultLocale, abs } from "@/i18n.config";export default function sitemap(): MetadataRoute.Sitemap { const languages = Object.fromEntries( locales.map((locale) => [locale, abs(locale, "/about")]) ); return [ { url: abs(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", priority: 0.7, alternates: { languages }, }, ];}Salin kode ke clipboard
import type { MetadataRoute } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";const ORIGIN = "https://example.com";const expandAllLocales = (path: string) => [ localizedPath(defaultLocale, path), ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => localizedPath(locale, path)),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...expandAllLocales("/dashboard"), ...expandAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: ORIGIN, sitemap: ORIGIN + "/sitemap.xml", };}9) Middleware untuk routing locale
Mendeteksi locale dan mengarahkan ulang ke rute yang dilokalkan jika tidak ada.
Salin kode ke clipboard
import { NextResponse, type NextRequest } from "next/server";import { defaultLocale, locales } from "@/i18n.config";const PUBLIC_FILE = /\.[^/]+$/; // mengecualikan file dengan ekstensiexport function middleware(request: NextRequest) { const { pathname } = request.nextUrl; if ( pathname.startsWith("/_next") || pathname.startsWith("/api") || pathname.startsWith("/static") || PUBLIC_FILE.test(pathname) ) { return; } const hasLocale = locales.some( (locale) => pathname === "/" + locale || pathname.startsWith("/" + locale + "/") ); if (!hasLocale) { const locale = defaultLocale; const url = request.nextUrl.clone(); url.pathname = "/" + locale + (pathname === "/" ? "" : pathname); return NextResponse.redirect(url); }}export const config = { matcher: [ // Cocokkan semua path kecuali yang diawali dengan ini dan file dengan ekstensi "/((?!api|_next|static|.*\\..*).*)", ],};10) Praktik terbaik Performa dan DX
- Set html
langdandir: Dilakukan disrc/app/[locale]/layout.tsx. - Pisahkan pesan berdasarkan namespace: Jaga bundle tetap kecil (
common.json,about.json, dll.). - Minimalkan payload klien: Pada halaman, kirim hanya namespace yang diperlukan ke provider.
- Utamakan halaman statis: Gunakan
export const dynamic = 'force-static'dangenerateStaticParamsper locale. - Sinkronkan komponen server: Kirim string/format yang sudah dihitung sebelumnya daripada panggilan async saat render.
- Memoisasi operasi berat: Terutama di kode klien untuk versi React yang lebih lama.
- Cache dan header: Utamakan statis atau
revalidatedaripada rendering dinamis jika memungkinkan.
11) Pengujian dan CI
- Tambahkan unit test untuk komponen yang menggunakan
tuntuk memastikan kunci ada. - Validasi bahwa setiap namespace memiliki kunci yang sama di semua locale.
- Tampilkan kunci yang hilang selama CI sebelum deploy.
Intlayer akan mengotomatisasi banyak hal ini (lihat bagian berikutnya).
12) Tambahkan Intlayer di atas (otomatisasi)
Intlayer membantu Anda menjaga sinkronisasi terjemahan JSON, menguji kunci yang hilang, dan mengisi dengan AI jika diinginkan.
Pasang dependensi intlayer:
Salin kode ke clipboard
npm install intlayer @intlayer/sync-json-plugin --save-devnpx intlayer initSalin kode ke clipboard
import { type IntlayerConfig, Locales } from "intlayer";import { locales, defaultLocale } from "@/i18n";import { syncJSON } from "@intlayer/sync-json";export const locales = [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH];const config: IntlayerConfig = { internationalization: { locales, defaultLocale, }, ai: { apiKey: process.env.OPENAI_API_KEY, }, plugins: [ syncJSON({ source: ({ locale }) => `./locales/${locale}.json`, }), ],};export default config;Tambahkan skrip package:
Salin kode ke clipboard
{ "scripts": { "i18n:fill": "intlayer fill", "i18n:test": "intlayer test" }}Alur umum:
pnpm i18n:testdi CI untuk gagal membangun jika ada kunci yang hilangpnpm i18n:fillsecara lokal untuk mengusulkan terjemahan AI untuk kunci yang baru ditambahkan
Anda dapat memberikan argumen CLI; lihat dokumentasi Intlayer CLI.
13) Pemecahan Masalah
- Kunci tidak ditemukan: Pastikan halaman/provider mencantumkan namespace yang benar dan file JSON ada di bawah
src/locales/<locale>/<namespace>.json. - Bahasa salah/kedipan bahasa Inggris: Periksa kembali deteksi locale di
middleware.tsdan providerlng. - Masalah tata letak RTL: Verifikasi bahwa
dirberasal dariisRtl(locale)dan CSS Anda menghormati[dir="rtl"]. - Alternatif SEO hilang: Pastikan
alternates.languagesmencakup semua locale danx-default. - Bundle terlalu besar: Pisahkan namespace lebih lanjut dan hindari mengimpor seluruh pohon
localesdi sisi klien.
14) Apa Selanjutnya
- Tambahkan lebih banyak locale dan namespace seiring berkembangnya fitur
- Lokalisasi halaman error, email, dan konten yang digerakkan oleh API
- Perluas workflow Intlayer untuk membuka PR secara otomatis guna pembaruan terjemahan
Jika Anda lebih suka menggunakan starter, coba template ini: https://github.com/aymericzip/intlayer-next-i18next-template.