BerandaSandboxShowcaseAplikasiDokumentasiBlog
    • EnglishInggris
      EN
    • русскийRusia
      RU
    • 日本語Jepang
      JA
    • françaisPrancis
      FR
    • 한국어Korea
      KO
    • 中文Tionghoa
      ZH
    • españolSpanyol
      ES
    • DeutschJerman
      DE
    • العربيةArab
      AR
    • italianoItalia
      IT
    • British EnglishInggris (Britania)
      EN-GB
    • portuguêsPortugis
      PT
    • हिन्दीHindi
      HI
    • TürkçeTurki
      TR
    • polskiPolski
      PL
    • IndonesiaIndonesia
      ID
    • Tiếng ViệtVietnam
      VI
    • українськаUkraina
      UK
    /
    Alt+←
    Apa itu internasionalisasi (i18n)?
    SEO dan i18n
    Panduan
    • i18n dengan next-i18next
    • i18n dengan next-intl
    Gunakan Intlayer di solusi Anda
    • Automatisasi next-i18next
    • Automatisasi react-i18next
    • Automatisasi next-intl
    • Automatisasi react-intl
    • Automatisasi vue-i18n
    Perbandingan
    • next-i18next vs next-intl vs Intlayer
    • react-i18next vs react-intl vs Intlayer
    Dokumentasi
    1. Blog
    2. Next i18next vs next intl vs intlayer
    Dibuat:2025-08-23Terakhir diperbarui:2025-09-29
    Referensikan dokumen ini ke asisten AI favorit Anda
    ChatGPT
    Claude
    DeepSeek
    Google AI mode
    Gemini
    Perplexity
    Mistral
    Grok

    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 Inggris
    Sunting dokumen ini

    Jika Anda memiliki ide untuk meningkatkan dokumentasi ini, silakan berkontribusi dengan mengajukan pull request di GitHub.

    Tautan GitHub ke dokumentasi
    Salin

    Salin Markdown dokumentasi ke clipboard

    next-i18next VS next-intl VS intlayer | Internasionalisasi Next.js (i18n)

    next-i18next VS next-intl VS intlayer

    Mari kita lihat persamaan dan perbedaan antara tiga opsi i18n untuk Next.js: next-i18next, next-intl, dan Intlayer.

    Ini bukan tutorial lengkap. Ini adalah perbandingan untuk membantu Anda memilih.

    Kami fokus pada Next.js 13+ App Router (dengan React Server Components) dan mengevaluasi:

    singkatnya: Ketiganya dapat melokalkan aplikasi Next.js. Jika Anda menginginkan konten yang dibatasi pada komponen, tipe TypeScript yang ketat, pemeriksaan kunci yang hilang saat build-time, kamus yang di-tree-shake, dan App Router kelas satu + pembantu SEO, Intlayer adalah pilihan paling lengkap dan modern.
    Salah satu kebingungan yang sering dibuat oleh pengembang adalah mengira bahwa next-intl adalah versi Next.js dari react-intl. Itu tidak benar, next-intl dikelola oleh Amann, sementara react-intl dikelola oleh FormatJS.

    Singkatnya

    • next-intl - Format pesan yang ringan dan sederhana dengan dukungan Next.js yang solid. Katalog terpusat umum digunakan; DX sederhana, tetapi keamanan dan pemeliharaan skala besar sebagian besar menjadi tanggung jawab Anda.
    • next-i18next - i18next dalam balutan Next.js. Ekosistem matang dan fitur melalui plugin (misalnya, ICU), tetapi konfigurasi bisa verbose dan katalog cenderung terpusat seiring pertumbuhan proyek.
    • Intlayer - Model konten berfokus pada komponen untuk Next.js, pengetikan TS yang ketat, pemeriksaan saat build-time, tree-shaking, middleware & pembantu SEO bawaan, Visual Editor/CMS opsional, dan terjemahan dibantu AI.

    Tampilkan semua isi tabel

    Buka tabel dalam modal untuk melihat semua isi data dengan jelas

    Library GitHub Stars Total Commits Last Commit First Version NPM Version NPM Downloads
    aymericzip/intlayer GitHub Repo stars GitHub commit activity Last Commit April 2024 npm npm downloads
    amannn/next-intl GitHub Repo stars GitHub commit activity Last Commit Nov 2020 npm npm downloads
    i18next/i18next GitHub Repo stars GitHub commit activity Last Commit Jan 2012 npm npm downloads
    i18next/next-i18next GitHub Repo stars GitHub commit activity Last Commit Nov 2018 npm npm downloads
    Lencana diperbarui secara otomatis. Snapshot akan bervariasi seiring waktu.

    Perbandingan Fitur Berdampingan (Fokus pada Next.js)

    Tampilkan semua isi tabel

    Buka tabel dalam modal untuk melihat semua isi data dengan jelas

    Fitur next-intlayer (Intlayer) next-intl next-i18next
    Terjemahan Dekat Komponen ✅ Ya, konten ditempatkan bersama dengan setiap komponen ❌ Tidak ❌ Tidak
    Integrasi TypeScript ✅ Lanjutan, tipe ketat yang dihasilkan secara otomatis ✅ Baik ⚠️ Dasar
    Deteksi Terjemahan yang Hilang ✅ Sorotan error TypeScript dan error/peringatan saat build ⚠️ Cadangan saat runtime ⚠️ Cadangan saat runtime
    Konten Kaya (JSX/Markdown/komponen) ✅ Dukungan langsung ❌ Tidak dirancang untuk node kaya ⚠️ Terbatas
    Terjemahan Berbasis AI ✅ Ya, mendukung beberapa penyedia AI. Dapat digunakan dengan kunci API Anda sendiri. Mempertimbangkan konteks aplikasi dan cakupan konten Anda ❌ Tidak ❌ Tidak
    Editor Visual ✅ Ya, Editor Visual lokal + CMS opsional; dapat mengeksternalisasi konten codebase; dapat disematkan ❌ Tidak / tersedia melalui platform lokalisasi eksternal ❌ Tidak / tersedia melalui platform lokalisasi eksternal
    Routing Lokal ✅ Ya, mendukung jalur lokal secara langsung (berfungsi dengan Next.js & Vite) ✅ Bawaan, App Router mendukung segmen [locale] ✅ Bawaan
    Generasi Rute Dinamis ✅ Ya ✅ Ya ✅ Ya
    Pluralisasi ✅ Pola berbasis enumerasi ✅ Baik ✅ Baik
    Format (tanggal, angka, mata uang) ✅ Formatter yang dioptimalkan (Intl di balik layar) ✅ Baik (bantuan Intl) ✅ Baik (bantuan Intl)
    Format Konten ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml dalam pengembangan) ✅ .json, .js, .ts ⚠️ .json
    Dukungan ICU ⚠️ Sedang dalam pengembangan ✅ Ya ⚠️ Melalui plugin (i18next-icu)
    Bantuan SEO (hreflang, sitemap) ✅ Alat bawaan: bantuan untuk sitemap, robots.txt, metadata ✅ Baik ✅ Baik
    Ekosistem / Komunitas ⚠️ Lebih kecil tetapi tumbuh cepat dan responsif ✅ Baik ✅ Baik
    Rendering sisi server & Komponen Server ✅ Ya, dioptimalkan untuk SSR / Komponen Server React ⚠️ Didukung pada tingkat halaman tetapi perlu meneruskan fungsi t pada pohon komponen untuk komponen server anak ⚠️ Didukung pada tingkat halaman tetapi perlu meneruskan fungsi t pada pohon komponen untuk komponen server anak
    Tree-shaking (memuat hanya konten yang digunakan) ✅ Ya, per-komponen saat build time melalui plugin Babel/SWC ⚠️ Sebagian ⚠️ Sebagian
    Lazy loading ✅ Ya, per-locale / per-dictionary ✅ Ya (per-route/per-locale), perlu manajemen namespace ✅ Ya (per-route/per-locale), perlu manajemen namespace
    Purge unused content ✅ Ya, per-dictionary saat build time ❌ Tidak, dapat dikelola secara manual dengan manajemen namespace ❌ Tidak, dapat dikelola secara manual dengan manajemen namespace
    Manajemen Proyek Besar ✅ Mendorong modular, cocok untuk design-system ✅ Modular dengan setup ✅ Modular dengan setup
    Pengujian Terjemahan yang Hilang (CLI/CI) ✅ CLI: npx intlayer content test (audit ramah CI) ⚠️ Tidak bawaan; dokumentasi menyarankan npx @lingual/i18n-check ⚠️ Tidak bawaan; mengandalkan alat i18next / runtime saveMissing

    Pendahuluan

    Next.js memberikan dukungan bawaan untuk routing internasionalisasi (misalnya segmen locale). Namun fitur tersebut tidak melakukan terjemahan secara otomatis. Anda masih memerlukan pustaka untuk menampilkan konten yang dilokalisasi kepada pengguna Anda.

    Banyak pustaka i18n yang ada, tetapi di dunia Next.js saat ini, tiga yang sedang naik daun: next-i18next, next-intl, dan Intlayer.


    Arsitektur & skalabilitas

    • next-intl / next-i18next: Default ke katalog terpusat per locale (ditambah namespace di i18next). Bekerja dengan baik pada awalnya, tetapi sering menjadi area bersama yang besar dengan peningkatan keterkaitan dan perubahan kunci yang tinggi.
    • Intlayer: Mendorong kamus per-komponen (atau per-fitur) yang berada bersama dengan kode yang mereka layani. Ini mengurangi beban kognitif, mempermudah duplikasi/migrasi bagian UI, dan mengurangi konflik antar tim. Konten yang tidak digunakan secara alami lebih mudah dikenali dan dihapus.

    Mengapa ini penting: Dalam basis kode besar atau pengaturan sistem desain, konten modular lebih mudah diskalakan dibandingkan katalog monolitik.


    Ukuran bundle & dependensi

    Setelah membangun aplikasi, bundle adalah JavaScript yang akan dimuat oleh browser untuk merender halaman. Oleh karena itu, ukuran bundle penting untuk performa aplikasi.

    Ada dua komponen yang penting dalam konteks bundle aplikasi multi-bahasa:

    • Kode aplikasi
    • Konten yang dimuat oleh browser

    Kode Aplikasi

    Pentingnya kode aplikasi dalam kasus ini minimal. Ketiga solusi tersebut mendukung tree-shaking, yang berarti bagian kode yang tidak digunakan tidak akan disertakan dalam bundle.

    Berikut adalah perbandingan ukuran bundle JavaScript yang dimuat oleh browser untuk aplikasi multi-bahasa dengan ketiga solusi tersebut.

    Jika kita tidak memerlukan formatter apapun dalam aplikasi, daftar fungsi yang diekspor setelah tree-shaking adalah:

    • next-intlayer: useIntlayer, useLocale, NextIntlClientProvider, (Ukuran bundle adalah 180.6 kB -> 15.24 kB (gzip))
    • next-intl: useTranslations, useLocale, NextIntlClientProvider, (Ukuran bundle adalah 101.3 kB -> 31.4 kB (gzip))
    • next-i18next: useTranslation, useI18n, I18nextProvider, (Ukuran bundle adalah 80.7 kB -> 25.5 kB (gzip))

    Fungsi-fungsi ini hanya pembungkus di sekitar konteks/state React, sehingga dampak total dari pustaka i18n pada ukuran bundle adalah minimal.

    Konten dan Terjemahan

    Bagian ini sering diabaikan oleh pengembang, tetapi mari kita pertimbangkan kasus sebuah aplikasi yang terdiri dari 10 halaman dalam 10 bahasa. Mari kita asumsikan bahwa setiap halaman mengintegrasikan konten yang 100% unik untuk menyederhanakan perhitungan (dalam kenyataannya, banyak konten yang berulang antar halaman, misalnya judul halaman, header, footer, dll.).

    Seorang pengguna yang ingin mengunjungi halaman /fr/about akan memuat konten dari satu halaman dalam bahasa tertentu. Mengabaikan optimasi konten berarti memuat 8.200% ((1 + (((10 halaman - 1) × (10 bahasa - 1)))) × 100) dari konten aplikasi secara tidak perlu. Apakah Anda melihat masalahnya? Bahkan jika konten ini tetap berupa teks, dan sementara Anda mungkin lebih memilih untuk memikirkan optimasi gambar situs Anda, Anda mengirimkan konten yang tidak berguna ke seluruh dunia dan membuat komputer pengguna memprosesnya tanpa alasan.

    Dua masalah penting:

    • Pemecahan berdasarkan rute:

      Jika saya berada di halaman /about, saya tidak ingin memuat konten dari halaman /home
    • Pemecahan berdasarkan locale:

      Jika saya berada di halaman /fr/about, saya tidak ingin memuat konten dari halaman /en/about

    Sekali lagi, ketiga solusi ini menyadari masalah ini dan memungkinkan pengelolaan optimasi tersebut. Perbedaan antara ketiga solusi ini adalah DX (Developer Experience).

    next-intl dan next-i18next menggunakan pendekatan terpusat untuk mengelola terjemahan, memungkinkan pemecahan JSON berdasarkan locale dan sub-file. Dalam next-i18next, file JSON disebut 'namespaces'; next-intl memungkinkan deklarasi pesan. Dalam intlayer, file JSON disebut 'dictionaries'.

    • Dalam kasus next-intl, seperti next-i18next, konten dimuat pada tingkat halaman/layout, kemudian konten ini dimuat ke dalam context provider. Ini berarti pengembang harus secara manual mengelola file JSON yang akan dimuat untuk setiap halaman.
    Dalam praktiknya, ini berarti pengembang sering melewatkan optimasi ini, lebih memilih untuk memuat semua konten dalam context provider halaman demi kesederhanaan.
    • Dalam kasus intlayer, semua konten dimuat dalam aplikasi. Kemudian sebuah plugin (@intlayer/babel / @intlayer/swc) yang mengurus optimasi bundle dengan memuat hanya konten yang digunakan pada halaman tersebut. Oleh karena itu, pengembang tidak perlu secara manual mengelola kamus yang akan dimuat. Ini memungkinkan optimasi yang lebih baik, pemeliharaan yang lebih mudah, dan mengurangi waktu pengembangan.

    Seiring pertumbuhan aplikasi (terutama ketika beberapa pengembang bekerja pada aplikasi tersebut), seringkali lupa untuk menghapus konten yang tidak lagi digunakan dari file JSON.

    Perlu dicatat bahwa semua JSON dimuat dalam semua kasus (next-intl, next-i18next, intlayer).

    Inilah mengapa pendekatan Intlayer lebih efisien: jika sebuah komponen tidak lagi digunakan, kamusnya tidak dimuat dalam bundle.

    Cara pustaka menangani fallback juga penting. Misalkan aplikasi menggunakan bahasa Inggris secara default, dan pengguna mengunjungi halaman /fr/about. Jika terjemahan dalam bahasa Prancis tidak tersedia, maka fallback ke bahasa Inggris akan digunakan.

    Dalam kasus next-intl dan next-i18next, perpustakaan mengharuskan memuat JSON yang terkait dengan locale saat ini, tetapi juga dengan locale fallback. Dengan demikian, menganggap bahwa semua konten telah diterjemahkan, setiap halaman akan memuat 100% konten yang tidak diperlukan. Sebaliknya, intlayer memproses fallback pada saat pembuatan kamus. Dengan demikian, setiap halaman hanya akan memuat konten yang digunakan.

    Catatan: Untuk mengoptimalkan bundle menggunakan intlayer, Anda perlu mengatur opsi importMode: 'dynamic' di file intlayer.config.ts Anda. Dan pastikan plugin @intlayer/babel / @intlayer/swc terpasang (terpasang secara default menggunakan vite-intlayer).

    Berikut adalah contoh dampak optimasi ukuran bundle menggunakan intlayer dalam aplikasi vite + react:

    Tampilkan semua isi tabel

    Buka tabel dalam modal untuk melihat semua isi data dengan jelas

    Bundle yang dioptimalkan Bundle yang tidak dioptimalkan
    bundle yang dioptimalkan bundle yang tidak dioptimalkan

    TypeScript & keamanan

    next-i18next

    • Typing dasar untuk hooks. pengetikan kunci yang ketat memerlukan alat/konfigurasi tambahan.

    next-intl

    • Dukungan TypeScript yang solid, tetapi kunci tidak secara ketat ditegakkan secara default. Anda harus mempertahankan pola keamanan secara manual.

    intlayer

    • Menghasilkan tipe yang ketat dari konten Anda. Autocompletion IDE dan kesalahan saat kompilasi menangkap kesalahan ketik dan kunci yang hilang sebelum deploy.

    Mengapa ini penting: Pengetikan yang kuat memindahkan kegagalan ke kiri (CI/build) daripada ke kanan (runtime).


    Penanganan terjemahan yang hilang

    next-i18next

    • Mengandalkan fallback saat runtime. Build tidak gagal.

    next-intl

    • Mengandalkan fallback saat runtime. Build tidak gagal.

    intlayer

    • Deteksi saat build dengan peringatan/eror untuk locale atau kunci yang hilang.

    Mengapa ini penting: Menangkap kekurangan saat build mencegah string 'undefined' muncul di produksi.


    Routing, middleware & strategi URL

    next-i18next

    • Memungkinkan routing yang dilokalisasi. Namun middleware tidak disertakan secara bawaan.

    next-intl

    • Memungkinkan routing yang dilokalisasi.
    • Menyediakan middleware.

    intlayer

    • Memungkinkan routing yang dilokalisasi.
    • Menyediakan middleware.

    Mengapa ini penting: Membantu SEO dan penemuan, serta pengalaman pengguna.


    Kesesuaian Server Components (RSC)

    next-i18next

    • Mendukung komponen server halaman dan layout.
    • Tidak menyediakan API sinkron untuk komponen server anak.

    next-intl

    • Mendukung komponen server halaman dan layout.
    • Tidak menyediakan API sinkron untuk komponen server anak.

    intlayer

    • Mendukung komponen server halaman dan layout.
    • Menyediakan API sinkron untuk komponen server anak.

    Mengapa ini penting: Dukungan komponen server adalah fitur utama Next.js 13+, membantu meningkatkan performa. Meneruskan props locale atau fungsi t dari komponen induk ke komponen server anak membuat komponen Anda kurang dapat digunakan ulang.


    Integrasi dengan platform lokalisasi (TMS)

    Organisasi besar sering mengandalkan Sistem Manajemen Terjemahan (TMS) seperti Crowdin, Phrase, Lokalise, Localizely, atau Localazy.

    • Mengapa perusahaan peduli

      • Kolaborasi & peran: Banyak aktor yang terlibat: pengembang, manajer produk, penerjemah, reviewer, tim pemasaran.
      • Skala & efisiensi: lokalisasi berkelanjutan, tinjauan dalam konteks.
    • next-intl / next-i18next

      • Biasanya menggunakan katalog JSON terpusat, sehingga ekspor/impor dengan TMS menjadi mudah.
      • Ekosistem yang matang dan contoh/integrasi untuk platform-platform di atas.
    • Intlayer

      • Mendorong kamus terdesentralisasi per komponen dan mendukung konten TypeScript/TSX/JS/JSON/MD.
      • Ini meningkatkan modularitas dalam kode, tetapi dapat membuat integrasi TMS plug-and-play menjadi lebih sulit ketika sebuah alat mengharapkan file JSON yang terpusat dan datar.
      • Intlayer menyediakan alternatif: terjemahan berbantuan AI (menggunakan kunci penyedia Anda sendiri), Editor Visual/CMS, dan alur kerja CLI/CI untuk menangkap dan mengisi celah.
    Catatan: next-intl dan i18next juga menerima katalog TypeScript. Jika tim Anda menyimpan pesan dalam file .ts atau mendesentralisasikannya berdasarkan fitur, Anda bisa menghadapi gesekan TMS yang serupa. Namun, banyak pengaturan next-intl tetap terpusat di folder locales/, yang sedikit lebih mudah untuk direfaktor ke JSON untuk TMS.

    Pengalaman Pengembang

    Bagian ini membuat perbandingan mendalam antara tiga solusi. Alih-alih mempertimbangkan kasus sederhana, seperti yang dijelaskan dalam dokumentasi 'memulai' untuk setiap solusi, kita akan mempertimbangkan kasus penggunaan nyata, yang lebih mirip dengan proyek nyata.

    Struktur Aplikasi

    Struktur aplikasi penting untuk memastikan pemeliharaan yang baik untuk basis kode Anda.

    bash
    Salin kode

    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.tsx
    bash
    Salin kode

    Salin kode ke clipboard

    .├── i18n.ts├── locales│   ├── en│   │  ├── home.json│   │  └── navbar.json│   ├── fr│   │  ├── home.json│   │  └── navbar.json│   └── es│      ├── home.json│      └── navbar.json└── src  ├── middleware.ts  ├── app  │   ├── i18n  │   │   └── server.ts  │   └── [locale]  │       └── home.tsx  └── components      └── Navbar          └── index.tsx
    bash
    Salin kode

    Salin kode ke clipboard

    .├── intlayer.config.ts└── src  ├── middleware.ts  ├── app  │   └── [locale]  │       ├── layout.tsx  │       └── home  │           ├── index.tsx  │           └── index.content.ts  └── components      └── Navbar          ├── index.tsx          └── index.content.ts

    Perbandingan

    • next-intl / next-i18next: Katalog terpusat (JSON; namespace/pesan). Struktur yang jelas, terintegrasi dengan baik dengan platform terjemahan, tetapi dapat menyebabkan lebih banyak pengeditan lintas file seiring pertumbuhan aplikasi.
    • Intlayer: Kamus .content.{ts|js|json} per-komponen yang ditempatkan bersama komponen. Memudahkan penggunaan ulang komponen dan penalaran lokal; menambah file dan bergantung pada tooling saat build.

    Pengaturan dan Memuat Konten

    Seperti yang disebutkan sebelumnya, Anda harus mengoptimalkan bagaimana setiap file JSON diimpor ke dalam kode Anda. Cara pustaka menangani pemuatan konten sangat penting.

    i18n.config.ts
    Salin kode

    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);// Mengembalikan path yang sudah dilokalkan berdasarkan localeexport function localizedPath(locale: string, path: string) {return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";// Menghasilkan URL absolut berdasarkan locale dan pathexport function abs(locale: string, path: string) {return ORIGIN + localizedPath(locale, path);}
    src/app/i18n/server.ts
    Salin kode

    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;}
    src/components/I18nProvider.tsx
    Salin kode

    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>;}
    src/app/[locale]/layout.tsx
    Salin kode

    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>);}
    src/app/[locale]/about.tsx
    Salin kode

    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>);}
    src/i18n.ts
    Salin kode

    Salin kode ke clipboard

    import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";export const locales = ["en", "fr", "es"] as const;export const defaultLocale = "en" as const;async function loadMessages(locale: string) {// Muat hanya namespace yang dibutuhkan oleh layout/halaman Andaconst [common, about] = await Promise.all([  import(`../locales/${locale}/common.json`).then((m) => m.default),  import(`../locales/${locale}/about.json`).then((m) => m.default),]);return { common, about } as const;}export default getRequestConfig(async ({ locale }) => {if (!locales.includes(locale)) notFound();return {  messages: await loadMessages(locale),};});
    src/app/[locale]/layout.tsx
    Salin kode

    Salin kode ke clipboard

    import type { ReactNode } from "react";import { locales } from "@/i18n";import {getLocaleDirection,unstable_setRequestLocale,} from "next-intl/server";export const dynamic = "force-static";export function generateStaticParams()() {return locales.map((locale) => ({ locale }));}export default async function LocaleLayout({children,params,}: {children: ReactNode;params: Promise<{ locale: string }>;}) {const { locale } = await params;// Atur locale permintaan aktif untuk render server ini (RSC)unstable_setRequestLocale(locale);const dir = getLocaleDirection(locale);return (  <html lang={locale} dir={dir}>    <body>{children}</body>  </html>);}
    src/app/[locale]/about/page.tsx
    Salin kode

    Salin kode ke clipboard

    import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponentExample from "@/components/ClientComponentExample";export const dynamic = "force-static";export default async function AboutPage({params,}: {params: Promise<{ locale: string }>;}) {const { locale } = await params;// Pesan dimuat di sisi server. Kirim hanya yang diperlukan ke klien.const messages = await getMessages();const clientMessages = pick(messages, ["common", "about"]);// Terjemahan/pemformatan yang benar-benar di sisi serverconst tAbout = await getTranslations("about");const tCounter = await getTranslations("about.counter");const format = await getFormatter();const initialFormattedCount = format.number(0);return (  <NextIntlClientProvider locale={locale} messages={clientMessages}>    <main>      <h1>{tAbout("title")}</h1>      <ClientComponentExample />      <ServerComponent        formattedCount={initialFormattedCount}        label={tCounter("label")}        increment={tCounter("increment")}      />    </main>  </NextIntlClientProvider>);}
    intlayer.config.ts
    Salin kode

    Salin kode ke clipboard

    import { type IntlayerConfig, Locales } from "intlayer";const config: IntlayerConfig = {internationalization: {  locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],  defaultLocale: Locales.ENGLISH,},};export default config;
    src/app/[locale]/layout.tsx
    Salin kode

    Salin kode ke clipboard

    import { getHTMLTextDir } from "intlayer";import {IntlayerClientProvider,generateStaticParams,type NextLayoutIntlayer,} from "next-intlayer";export const dynamic = "force-static";const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {const { locale } = await params;return (  <html lang={locale} dir={getHTMLTextDir(locale)}>    <body>      <IntlayerClientProvider locale={locale}>        {children}      </IntlayerClientProvider>    </body>  </html>);};export default LandingLayout;
    src/app/[locale]/about/page.tsx
    Salin kode

    Salin kode ke clipboard

    import { PageContent } from "@components/PageContent";import type { NextPageIntlayer } from "next-intlayer";import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";import { ClientComponent, ServerComponent } from "@components";const LandingPage: NextPageIntlayer = async ({ params }) => {const { locale } = await params;const { title } = useIntlayer("about", locale);return (  <IntlayerServerProvider locale={locale}>    <main>      <h1>{title}</h1>      <ClientComponent />      <ServerComponent />    </main>  </IntlayerServerProvider>);};export default LandingPage;

    Perbandingan

    Ketiganya mendukung pemuatan konten dan penyedia per-locale.

    • Dengan next-intl/next-i18next, Anda biasanya memuat pesan/namespace yang dipilih per rute dan menempatkan penyedia di tempat yang diperlukan.

    • Dengan Intlayer, menambahkan analisis saat build untuk menginfer penggunaan, yang dapat mengurangi pengkabelan manual dan memungkinkan satu penyedia root saja.

    Pilih antara kontrol eksplisit dan otomatisasi berdasarkan preferensi tim.

    Penggunaan dalam komponen klien

    Mari kita ambil contoh komponen klien yang merender sebuah penghitung.

    Terjemahan (satu JSON per namespace di bawah src/locales/...)

    src/locales/en/about.json
    Salin kode

    Salin kode ke clipboard

    {"title": "About","description": "About page description","counter": {  "label": "Counter",  "increment": "Increment"}}
    src/locales/fr/about.json
    Salin kode

    Salin kode ke clipboard

    {"title": "À propos","description": "Description de la page À propos","counter": {  "label": "Compteur",  "increment": "Incrémenter"}}

    Komponen klien (memuat hanya namespace yang diperlukan)

    src/components/ClientComponent.tsx
    Salin kode

    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 memuat namespace yang Anda butuhkan (misalnya about). Jika Anda menggunakan React < 19, gunakan memo untuk formatter berat seperti Intl.NumberFormat.

    Terjemahan (bentuk digunakan ulang; muat ke dalam pesan next-intl sesuai keinginan Anda)

    locales/en/about.json
    Salin kode

    Salin kode ke clipboard

    {"counter": {  "label": "Counter",  "increment": "Increment"}}
    locales/fr/about.json
    Salin kode

    Salin kode ke clipboard

    {"counter": {  "label": "Compteur",  "increment": "Incrémenter"}}

    Komponen klien

    src/components/ClientComponentExample.tsx
    Salin kode

    Salin kode ke clipboard

    "use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => {// Lingkup langsung ke objek bersarangconst t = useTranslations("about.counter");const format = useFormatter();const [count, setCount] = useState(0);return (  <div>    <p>{format.number(count)}</p>    <button      aria-label={t("label")}      onClick={() => setCount((count) => count + 1)}    >      {t("increment")}    </button>  </div>);};
    Jangan lupa untuk menambahkan pesan "about" pada pesan klien halaman

    Konten

    src/components/ClientComponentExample/index.content.ts
    Salin kode

    Salin kode ke clipboard

    import { t, type Dictionary } from "intlayer";const counterContent = {key: "counter",content: {  label: t({ en: "Counter", fr: "Compteur" }),  increment: t({ en: "Increment", fr: "Incrémenter" }),},} satisfies Dictionary;export default counterContent;

    Komponen klien

    src/components/ClientComponentExample/index.tsx
    Salin kode

    Salin kode ke clipboard

    "use client";import React, { useState } from "react";import { useNumber, useIntlayer } from "next-intlayer";const ClientComponentExample = () => {const [count, setCount] = useState(0);const { label, increment } = useIntlayer("counter"); // mengembalikan stringconst { number } = useNumber();return (  <div>    <p>{number(count)}</p>    <button aria-label={label} onClick={() => setCount((count) => count + 1)}>      {increment}    </button>  </div>);};

    Perbandingan

    • Format angka

      • next-i18next: tidak ada useNumber; menggunakan Intl.NumberFormat (atau i18next-icu).
      • next-intl: useFormatter().number(value).
      • Intlayer: useNumber() bawaan.
    • Kunci

      • Pertahankan struktur bertingkat (about.counter.label) dan sesuaikan cakupan hook Anda (useTranslation("about") + t("counter.label") atau useTranslations("about.counter") + t("label")).
    • Lokasi file

      • next-i18next mengharapkan JSON di public/locales/{lng}/{ns}.json.
      • next-intl fleksibel; muat pesan sesuai konfigurasi Anda.
      • Intlayer menyimpan konten dalam kamus TS/JS dan menyelesaikan berdasarkan kunci.

    Penggunaan dalam komponen server

    Kita akan mengambil kasus sebuah komponen UI. Komponen ini adalah komponen server, dan harus dapat dimasukkan sebagai anak dari komponen klien. (halaman (komponen server) -> komponen klien -> komponen server). Karena komponen ini dapat dimasukkan sebagai anak dari komponen klien, maka tidak boleh bersifat async.

    src/components/ServerComponent.tsx
    Salin kode

    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;
    src/components/ServerComponent.tsx
    Salin kode

    Salin kode ke clipboard

    type ServerComponentProps = {t: (key: string) => string;locale: string;count: number;formatter: Intl.NumberFormat;};const ServerComponent = ({t,locale,count,formatter,}: ServerComponentProps) => {const formatted = formatter.format(count);return (  <div>    <p>{formatted}</p>    <button aria-label={t("counter.label")}>{t("counter.increment")}</button>  </div>);};export default ServerComponent;

    Karena komponen server tidak dapat bersifat async, Anda perlu mengoper terjemahan dan fungsi formatter sebagai props.

    Di halaman / layout Anda:

    • import { getTranslations, getFormatter } from "next-intl/server";
    • const t = await getTranslations("about.counter");
    • const formatter = await getFormatter().then((formatter) => formatter.number());
    src/components/ServerComponent.tsx
    Salin kode

    Salin kode ke clipboard

    import { useIntlayer, useNumber } from "next-intlayer/server";type ServerComponentProps = {count: number;};const ServerComponent = ({ count }: ServerComponentProps) => {const { label, increment } = useIntlayer("counter");const { number } = useNumber();return (  <div>    <p>{number(count)}</p>    <button aria-label={label}>{increment}</button>  </div>);};
    Intlayer menyediakan hook aman untuk server melalui next-intlayer/server. Untuk bekerja, useIntlayer dan useNumber menggunakan sintaks mirip hook, serupa dengan hook di sisi klien, tetapi bergantung pada konteks server di bawahnya (IntlayerServerProvider).

    Metadata / Sitemap / Robots

    Menerjemahkan konten itu bagus. Namun orang sering lupa bahwa tujuan utama internasionalisasi adalah membuat situs web Anda lebih terlihat oleh dunia. I18n adalah tuas luar biasa untuk meningkatkan visibilitas situs web Anda.

    Berikut adalah daftar praktik terbaik terkait SEO multibahasa.

    • atur tag meta hreflang di tag <head> > Ini membantu mesin pencari memahami bahasa apa saja yang tersedia di halaman
    • daftarkan semua terjemahan halaman dalam sitemap.xml menggunakan skema XML http://www.w3.org/1999/xhtml >
    • jangan lupa untuk mengecualikan halaman dengan prefix dari robots.txt (misalnya /dashboard, dan /fr/dashboard, /es/dashboard) >
    • gunakan komponen Link khusus untuk mengarahkan ke halaman yang paling terlokalisasi (misalnya dalam bahasa Prancis <a href="/fr/about">A propos</a>) >

    Pengembang sering lupa untuk mereferensikan halaman mereka dengan benar di berbagai locale.

    i18n.config.ts
    Salin kode

    Salin kode ke clipboard

    export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";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);}
    src/app/[locale]/about/layout.tsx
    Salin kode

    Salin kode ke clipboard

    import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";type GenerateMetadataParams = { params: Promise<{   locale: string; }>;};export const generateMetadata = async ({ params,}: GenerateMetadataParams): Promise<Metadata> => { const { locale } = await 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>;}
    src/app/sitemap.ts
    Salin kode

    Salin kode ke clipboard

    import type { MetadataRoute } from "next";import { locales, defaultLocale, abs } from "@/i18n.config";export const 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 },   }, ];};
    src/app/robots.ts
    Salin kode

    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 const robots = (): MetadataRoute.Robots => { const disallow = [   ...expandAllLocales("/dashboard"),   ...expandAllLocales("/admin"), ]; return {   rules: { userAgent: "*", allow: ["/"], disallow },   host: ORIGIN,   sitemap: ORIGIN + "/sitemap.xml", };};
    src/app/[locale]/about/layout.tsx
    Salin kode

    Salin kode ke clipboard

    import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";const localizedPath = (locale: string, path: string) => { return locale === defaultLocale ? path : "/" + locale + path;};type GenerateMetadataParams = { params: Promise<{   locale: string; }>;};export const generateMetadata = async ({ params,}: GenerateMetadataParams): Promise<Metadata> => { const { locale } = await params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries(   locales.map((locale) => [locale, localizedPath(locale, url)]) ); return {   title: t("title"),   description: t("description"),   alternates: {     canonical: localizedPath(locale, url),     languages: { ...languages, "x-default": url },   }, };};// ... Sisa kode halaman
    src/app/sitemap.ts
    Salin kode

    Salin kode ke clipboard

    import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export const sitemap = (): MetadataRoute.Sitemap => { const aboutLanguages = Object.fromEntries(   locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [   {     url: formatterLocalizedPath(defaultLocale, "/about"),     lastModified: new Date(),     changeFrequency: "monthly",     priority: 0.7,     alternates: { languages: aboutLanguages },   }, ];};
    src/app/robots.ts
    Salin kode

    Salin kode ke clipboard

    import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales   .filter((locale) => locale !== defaultLocale)   .map((locale) => "/" + locale + path),];export const robots = (): MetadataRoute.Robots => { const disallow = [   ...withAllLocales("/dashboard"),   ...withAllLocales("/admin"), ]; return {   rules: { userAgent: "*", allow: ["/"], disallow },   host: origin,   sitemap: origin + "/sitemap.xml", };};
    src/app/[locale]/about/layout.tsx
    Salin kode

    Salin kode ke clipboard

    import { getIntlayer, getMultilingualUrls } from "intlayer";import type { Metadata } from "next";import type { LocalPromiseParams } from "next-intlayer";export const generateMetadata = async ({ params,}: LocalPromiseParams): Promise<Metadata> => { const { locale } = await params; const metadata = getIntlayer("page-metadata", locale); const multilingualUrls = getMultilingualUrls("/about"); return {   ...metadata,   alternates: {     canonical: multilingualUrls[locale as keyof typeof multilingualUrls],     languages: { ...multilingualUrls, "x-default": "/about" },   }, };};// ... Sisa kode halaman
    src/app/sitemap.ts
    Salin kode

    Salin kode ke clipboard

    import { getMultilingualUrls } from "intlayer";import type { MetadataRoute } from "next";const sitemap = (): MetadataRoute.Sitemap => [ {   url: "https://example.com/about",   alternates: {     languages: { ...getMultilingualUrls("https://example.com/about") },   }, },];
    src/app/robots.ts
    Salin kode

    Salin kode ke clipboard

    import { getMultilingualUrls } from "intlayer";import type { MetadataRoute } from "next";const getAllMultilingualUrls = (urls: string[]) => urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);const robots = (): MetadataRoute.Robots => ({ rules: {   userAgent: "*",   allow: ["/"],   disallow: getAllMultilingualUrls(["/dashboard"]), }, host: "https://example.com", sitemap: "https://example.com/sitemap.xml",});export default robots;
    Intlayer menyediakan fungsi getMultilingualUrls untuk menghasilkan URL multibahasa untuk sitemap Anda.

    Middleware untuk routing locale

    Tambahkan middleware untuk menangani deteksi locale dan routing:

    src/middleware.ts
    Salin kode

    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(  (l) => pathname === "/" + l || pathname.startsWith("/" + l + "/"));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|.*\\..*).*)",],};

    Tambahkan middleware untuk menangani deteksi locale dan routing:

    src/middleware.ts
    Salin kode

    Salin kode ke clipboard

    import createMiddleware from "next-intl/middleware";import { locales, defaultLocale } from "@/i18n";export default createMiddleware({locales: [...locales],defaultLocale,localeDetection: true,});export const config = {// Lewati API, internal Next, dan aset statismatcher: ["/((?!api|_next|.*\\..*).*)"],};

    Intlayer menyediakan penanganan middleware bawaan melalui konfigurasi paket next-intlayer.

    src/middleware.ts
    Salin kode

    Salin kode ke clipboard

    import { intlayerProxy } from "next-intlayer/proxy";export const middleware = intlayerProxy();// menerapkan middleware ini hanya pada file di direktori appexport const config = {matcher: "/((?!api|_next|static|.*\\..*).*)",};

    Pengaturan middleware dipusatkan dalam file intlayer.config.ts.

    Daftar periksa pengaturan dan praktik terbaik

    • Pastikan lang dan dir disetel pada root <html> di src/app/[locale]/layout.tsx.
    • Pisahkan terjemahan ke dalam namespace (misalnya common.json, about.json) di bawah src/locales/<locale>/.
    • Hanya muat namespace yang diperlukan di komponen klien menggunakan useTranslation('<ns>') dan dengan membatasi I18nProvider pada namespace yang sama.
    • Pertahankan halaman agar statis jika memungkinkan: ekspor export const dynamic = 'force-static' pada halaman; atur dynamicParams = false dan implementasikan generateStaticParams.
    • Gunakan komponen server sinkron yang bersarang di bawah batas klien dengan mengoper string yang sudah dihitung atau fungsi t dan locale.
    • Untuk SEO, atur alternates.languages di metadata, daftarkan URL lokal di sitemap.ts, dan larang rute lokal duplikat di robots.ts.
    • Lebih memilih formatter yang sadar locale (misalnya, Intl.NumberFormat(locale)) dan simpan dalam memo di sisi client jika menggunakan React < 19.
    • Setel html lang dan dir: Di src/app/[locale]/layout.tsx, hitung dir melalui getLocaleDirection(locale) dan setel <html lang={locale} dir={dir}>.
    • Pisahkan pesan berdasarkan namespace: Atur JSON per locale dan namespace (misalnya, common.json, about.json).
    • Minimalkan payload client: Pada halaman, kirim hanya namespace yang diperlukan ke NextIntlClientProvider (misalnya, pick(messages, ['common', 'about'])).
    • Utamakan halaman statis: Ekspor export const dynamic = 'force-static' dan buat parameter statis untuk semua locales.
    • Komponen server sinkron: Pertahankan komponen server agar sinkron dengan mengoper string yang sudah dihitung sebelumnya (label yang diterjemahkan, angka yang diformat) daripada panggilan async atau fungsi yang tidak dapat diserialisasi.
    • Konten modular: Tempatkan kamus konten bersama dengan komponen menggunakan file .content.{ts|js|json}.
    • Keamanan tipe: Manfaatkan integrasi TypeScript untuk validasi konten saat waktu kompilasi.
    • Optimasi saat build: Gunakan alat build Intlayer untuk tree-shaking otomatis dan optimasi bundel.
    • Alat terintegrasi: Manfaatkan routing bawaan, pembantu SEO, dan dukungan editor visual.

    Dan pemenangnya adalah…

    Ini tidak sederhana. Setiap opsi memiliki kelebihan dan kekurangan. Berikut pandangan saya:

    next-i18next

    • matang, penuh fitur, banyak plugin komunitas, tetapi biaya pengaturan lebih tinggi. Jika Anda membutuhkan ekosistem plugin i18next (misalnya, aturan ICU lanjutan melalui plugin) dan tim Anda sudah mengenal i18next, menerima lebih banyak konfigurasi untuk fleksibilitas.

    next-intl

    • paling sederhana, ringan, lebih sedikit keputusan yang dipaksakan pada Anda. Jika Anda menginginkan solusi minimal, Anda nyaman dengan katalog terpusat, dan aplikasi Anda berukuran kecil hingga menengah.

    Intlayer

    • dibangun untuk Next.js modern, dengan konten modular, keamanan tipe, tooling, dan lebih sedikit boilerplate. Jika Anda menghargai konten yang terfokus pada komponen, TypeScript yang ketat, jaminan waktu build, tree-shaking, dan tooling routing/SEO/editor yang lengkap - terutama untuk Next.js App Router, sistem desain, dan codebase besar yang modular.

    Jika Anda lebih memilih pengaturan minimal dan menerima beberapa penyambungan manual, next-intl adalah pilihan yang baik. Jika Anda membutuhkan semua fitur dan tidak keberatan dengan kompleksitas, next-i18next bisa digunakan. Namun jika Anda menginginkan solusi modern, skalabel, modular dengan alat bawaan, Intlayer bertujuan memberikan itu langsung dari kotak.

    Alternatif untuk tim enterprise: Jika Anda membutuhkan solusi yang sudah terbukti dan bekerja sempurna dengan platform lokalisasi yang sudah mapan seperti Crowdin, Phrase, atau sistem manajemen terjemahan profesional lainnya, pertimbangkan next-intl atau next-i18next karena ekosistem mereka yang matang dan integrasi yang sudah terbukti.
    Roadmap masa depan: Intlayer juga berencana mengembangkan plugin yang bekerja di atas solusi i18next dan next-intl. Ini akan memberikan Anda keuntungan dari Intlayer untuk otomasi, sintaks, dan manajemen konten sambil menjaga keamanan dan stabilitas yang disediakan oleh solusi mapan ini dalam kode aplikasi Anda.

    GitHub STARs

    Bintang GitHub adalah indikator kuat dari popularitas sebuah proyek, kepercayaan komunitas, dan relevansi jangka panjang. Meskipun bukan ukuran langsung dari kualitas teknis, bintang tersebut mencerminkan berapa banyak pengembang yang menganggap proyek tersebut berguna, mengikuti perkembangannya, dan kemungkinan akan mengadopsinya. Untuk memperkirakan nilai sebuah proyek, bintang membantu membandingkan daya tarik antar alternatif dan memberikan wawasan tentang pertumbuhan ekosistem.

    Grafik Sejarah Bintang


    Kesimpulan

    Ketiga perpustakaan tersebut berhasil dalam lokalisasi inti. Perbedaannya adalah seberapa banyak pekerjaan yang harus Anda lakukan untuk mencapai pengaturan yang kuat dan skalabel di Next.js modern:

    • Dengan Intlayer, konten modular, TS ketat, keamanan saat build, bundle yang di-tree-shake, dan App Router kelas satu + alat SEO adalah default, bukan tugas yang merepotkan.
    • Jika tim Anda menghargai pemeliharaan dan kecepatan dalam aplikasi multi-locale yang berbasis komponen, Intlayer menawarkan pengalaman yang paling lengkap saat ini.

    Lihat dokumen 'Mengapa Intlayer?' untuk detail lebih lanjut.

    Automatisasi vue-i18n
    react-i18next vs react-intl vs Intlayer
    Alt+→

    Di halaman ini

      Diskusi bersifat anonim dan ditinjau secara berkala untuk mengatasi masalah umum. Jangan ragu untuk berbagi ide fitur, masukan tentang dokumentasi, atau apa pun yang terkait dengan Intlayer, kami menggunakan masukan ini untuk membentuk peta jalan dan meningkatkan produk.

      .├── 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.tsx
      .├── i18n.ts├── locales│   ├── en│   │  ├── home.json│   │  └── navbar.json│   ├── fr│   │  ├── home.json│   │  └── navbar.json│   └── es│      ├── home.json│      └── navbar.json└── src  ├── middleware.ts  ├── app  │   ├── i18n  │   │   └── server.ts  │   └── [locale]  │       └── home.tsx  └── components      └── Navbar          └── index.tsx
      .├── intlayer.config.ts└── src  ├── middleware.ts  ├── app  │   └── [locale]  │       ├── layout.tsx  │       └── home  │           ├── index.tsx  │           └── index.content.ts  └── components      └── Navbar          ├── index.tsx          └── index.content.ts
      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);// Mengembalikan path yang sudah dilokalkan berdasarkan localeexport function localizedPath(locale: string, path: string) {return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";// Menghasilkan URL absolut berdasarkan locale dan pathexport function abs(locale: string, path: string) {return ORIGIN + localizedPath(locale, path);}
      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;}
      "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>;}
      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>);}
      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>);}
      import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";export const locales = ["en", "fr", "es"] as const;export const defaultLocale = "en" as const;async function loadMessages(locale: string) {// Muat hanya namespace yang dibutuhkan oleh layout/halaman Andaconst [common, about] = await Promise.all([  import(`../locales/${locale}/common.json`).then((m) => m.default),  import(`../locales/${locale}/about.json`).then((m) => m.default),]);return { common, about } as const;}export default getRequestConfig(async ({ locale }) => {if (!locales.includes(locale)) notFound();return {  messages: await loadMessages(locale),};});
      import type { ReactNode } from "react";import { locales } from "@/i18n";import {getLocaleDirection,unstable_setRequestLocale,} from "next-intl/server";export const dynamic = "force-static";export function generateStaticParams()() {return locales.map((locale) => ({ locale }));}export default async function LocaleLayout({children,params,}: {children: ReactNode;params: Promise<{ locale: string }>;}) {const { locale } = await params;// Atur locale permintaan aktif untuk render server ini (RSC)unstable_setRequestLocale(locale);const dir = getLocaleDirection(locale);return (  <html lang={locale} dir={dir}>    <body>{children}</body>  </html>);}
      import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponentExample from "@/components/ClientComponentExample";export const dynamic = "force-static";export default async function AboutPage({params,}: {params: Promise<{ locale: string }>;}) {const { locale } = await params;// Pesan dimuat di sisi server. Kirim hanya yang diperlukan ke klien.const messages = await getMessages();const clientMessages = pick(messages, ["common", "about"]);// Terjemahan/pemformatan yang benar-benar di sisi serverconst tAbout = await getTranslations("about");const tCounter = await getTranslations("about.counter");const format = await getFormatter();const initialFormattedCount = format.number(0);return (  <NextIntlClientProvider locale={locale} messages={clientMessages}>    <main>      <h1>{tAbout("title")}</h1>      <ClientComponentExample />      <ServerComponent        formattedCount={initialFormattedCount}        label={tCounter("label")}        increment={tCounter("increment")}      />    </main>  </NextIntlClientProvider>);}
      import { type IntlayerConfig, Locales } from "intlayer";const config: IntlayerConfig = {internationalization: {  locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],  defaultLocale: Locales.ENGLISH,},};export default config;
      import { getHTMLTextDir } from "intlayer";import {IntlayerClientProvider,generateStaticParams,type NextLayoutIntlayer,} from "next-intlayer";export const dynamic = "force-static";const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {const { locale } = await params;return (  <html lang={locale} dir={getHTMLTextDir(locale)}>    <body>      <IntlayerClientProvider locale={locale}>        {children}      </IntlayerClientProvider>    </body>  </html>);};export default LandingLayout;
      import { PageContent } from "@components/PageContent";import type { NextPageIntlayer } from "next-intlayer";import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";import { ClientComponent, ServerComponent } from "@components";const LandingPage: NextPageIntlayer = async ({ params }) => {const { locale } = await params;const { title } = useIntlayer("about", locale);return (  <IntlayerServerProvider locale={locale}>    <main>      <h1>{title}</h1>      <ClientComponent />      <ServerComponent />    </main>  </IntlayerServerProvider>);};export default LandingPage;
      {"title": "About","description": "About page description","counter": {  "label": "Counter",  "increment": "Increment"}}
      {"title": "À propos","description": "Description de la page À propos","counter": {  "label": "Compteur",  "increment": "Incrémenter"}}
      "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;
      {"counter": {  "label": "Counter",  "increment": "Increment"}}
      {"counter": {  "label": "Compteur",  "increment": "Incrémenter"}}
      "use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => {// Lingkup langsung ke objek bersarangconst t = useTranslations("about.counter");const format = useFormatter();const [count, setCount] = useState(0);return (  <div>    <p>{format.number(count)}</p>    <button      aria-label={t("label")}      onClick={() => setCount((count) => count + 1)}    >      {t("increment")}    </button>  </div>);};
      import { t, type Dictionary } from "intlayer";const counterContent = {key: "counter",content: {  label: t({ en: "Counter", fr: "Compteur" }),  increment: t({ en: "Increment", fr: "Incrémenter" }),},} satisfies Dictionary;export default counterContent;
      "use client";import React, { useState } from "react";import { useNumber, useIntlayer } from "next-intlayer";const ClientComponentExample = () => {const [count, setCount] = useState(0);const { label, increment } = useIntlayer("counter"); // mengembalikan stringconst { number } = useNumber();return (  <div>    <p>{number(count)}</p>    <button aria-label={label} onClick={() => setCount((count) => count + 1)}>      {increment}    </button>  </div>);};
      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;
      type ServerComponentProps = {t: (key: string) => string;locale: string;count: number;formatter: Intl.NumberFormat;};const ServerComponent = ({t,locale,count,formatter,}: ServerComponentProps) => {const formatted = formatter.format(count);return (  <div>    <p>{formatted}</p>    <button aria-label={t("counter.label")}>{t("counter.increment")}</button>  </div>);};export default ServerComponent;
      import { useIntlayer, useNumber } from "next-intlayer/server";type ServerComponentProps = {count: number;};const ServerComponent = ({ count }: ServerComponentProps) => {const { label, increment } = useIntlayer("counter");const { number } = useNumber();return (  <div>    <p>{number(count)}</p>    <button aria-label={label}>{increment}</button>  </div>);};
      export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";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);}
      import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";type GenerateMetadataParams = { params: Promise<{   locale: string; }>;};export const generateMetadata = async ({ params,}: GenerateMetadataParams): Promise<Metadata> => { const { locale } = await 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>;}
      import type { MetadataRoute } from "next";import { locales, defaultLocale, abs } from "@/i18n.config";export const 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 },   }, ];};
      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 const robots = (): MetadataRoute.Robots => { const disallow = [   ...expandAllLocales("/dashboard"),   ...expandAllLocales("/admin"), ]; return {   rules: { userAgent: "*", allow: ["/"], disallow },   host: ORIGIN,   sitemap: ORIGIN + "/sitemap.xml", };};
      import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";const localizedPath = (locale: string, path: string) => { return locale === defaultLocale ? path : "/" + locale + path;};type GenerateMetadataParams = { params: Promise<{   locale: string; }>;};export const generateMetadata = async ({ params,}: GenerateMetadataParams): Promise<Metadata> => { const { locale } = await params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries(   locales.map((locale) => [locale, localizedPath(locale, url)]) ); return {   title: t("title"),   description: t("description"),   alternates: {     canonical: localizedPath(locale, url),     languages: { ...languages, "x-default": url },   }, };};// ... Sisa kode halaman
      import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export const sitemap = (): MetadataRoute.Sitemap => { const aboutLanguages = Object.fromEntries(   locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [   {     url: formatterLocalizedPath(defaultLocale, "/about"),     lastModified: new Date(),     changeFrequency: "monthly",     priority: 0.7,     alternates: { languages: aboutLanguages },   }, ];};
      import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales   .filter((locale) => locale !== defaultLocale)   .map((locale) => "/" + locale + path),];export const robots = (): MetadataRoute.Robots => { const disallow = [   ...withAllLocales("/dashboard"),   ...withAllLocales("/admin"), ]; return {   rules: { userAgent: "*", allow: ["/"], disallow },   host: origin,   sitemap: origin + "/sitemap.xml", };};
      import { getIntlayer, getMultilingualUrls } from "intlayer";import type { Metadata } from "next";import type { LocalPromiseParams } from "next-intlayer";export const generateMetadata = async ({ params,}: LocalPromiseParams): Promise<Metadata> => { const { locale } = await params; const metadata = getIntlayer("page-metadata", locale); const multilingualUrls = getMultilingualUrls("/about"); return {   ...metadata,   alternates: {     canonical: multilingualUrls[locale as keyof typeof multilingualUrls],     languages: { ...multilingualUrls, "x-default": "/about" },   }, };};// ... Sisa kode halaman
      import { getMultilingualUrls } from "intlayer";import type { MetadataRoute } from "next";const sitemap = (): MetadataRoute.Sitemap => [ {   url: "https://example.com/about",   alternates: {     languages: { ...getMultilingualUrls("https://example.com/about") },   }, },];
      import { getMultilingualUrls } from "intlayer";import type { MetadataRoute } from "next";const getAllMultilingualUrls = (urls: string[]) => urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);const robots = (): MetadataRoute.Robots => ({ rules: {   userAgent: "*",   allow: ["/"],   disallow: getAllMultilingualUrls(["/dashboard"]), }, host: "https://example.com", sitemap: "https://example.com/sitemap.xml",});export default robots;
      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(  (l) => pathname === "/" + l || pathname.startsWith("/" + l + "/"));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|.*\\..*).*)",],};
      import createMiddleware from "next-intl/middleware";import { locales, defaultLocale } from "@/i18n";export default createMiddleware({locales: [...locales],defaultLocale,localeDetection: true,});export const config = {// Lewati API, internal Next, dan aset statismatcher: ["/((?!api|_next|.*\\..*).*)"],};
      import { intlayerProxy } from "next-intlayer/proxy";export const middleware = intlayerProxy();// menerapkan middleware ini hanya pada file di direktori appexport const config = {matcher: "/((?!api|_next|static|.*\\..*).*)",};