Penulis:
    Dibuat:2026-09-13Terakhir diperbarui:2026-09-27

    next-intl VS Intlayer: Tolok Ukur Internasionalisasi (i18n) React & Next.js

    next-intl VS Intlayer

    next-intl adalah pustaka i18n paling populer untuk Next.js. Intlayer adalah alternatif berbasis kompiler dengan cakupan komponen. Keduanya melokalkan aplikasi App Router. Pertanyaannya adalah berapa biaya masing-masing setelah aplikasi dibangun.

    Artikel ini bukan tutorial. Ini adalah perbandingan yang didukung oleh angka dari Benchmark Bloom, rangkaian tolok ukur sumber terbuka yang membangun aplikasi yang sama dengan setiap pustaka dan mengukur apa yang sebenarnya diunduh dan dijalankan peramban.

    tl;dr: Pada aplikasi Next.js yang sama, next-intl menambahkan +12.6 KB gzip JavaScript di setiap halaman, dibandingkan dengan +0.3 KB untuk Intlayer. Tanpa upaya tambahan, next-intl mengirimkan ~90% string halaman luar pada setiap halaman. Mencapai 0% kebocoran dengan next-intl membutuhkan namespace dan pick(messages, [...]) per halaman. Intlayer mencapai 0% secara default, karena kompilernya mencakup konten per komponen. Jika Anda menginginkan API next-intl dengan output Intlayer, adaptor @intlayer/next-intl berukuran 147.5 KB per halaman dibandingkan 153.6 KB dengan aslinya.

    Ringkasan

    • next-intl - Standar komunitas Next.js. Kamus JSON terpusat per bahasa, dukungan penuh ICU MessageFormat, dan integrasi mendalam dengan penanganan permintaan serta routing Next.js.
    • Intlayer - Model konten yang berpusat pada komponen. File .content.ts berada tepat di sebelah komponennya, kompiler build-time melakukan tree-shaking dan lazy loading per komponen dan per lokal, serta menghasilkan tipe TypeScript yang ketat secara otomatis.
    PustakaBintang GitHubTotal KomitKomit TerakhirVersi PertamaVersi NPMUnduhan NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitApril 2024npmnpm downloads
    amannn/next-intlGitHub Repo starsGitHub commit activityLast CommitMaret 2021npmnpm downloads
    Lencana diperbarui secara otomatis.

    Perbandingan fitur berdampingan

    FiturIntlayer (react-intlayer / next-intlayer)next-intl (next-intl / use-intl)
    Terjemahan dekat komponen✅ Ya, .content.ts berada di samping setiap komponen❌ Kamus JSON terpusat di folder messages/
    Integrasi TypeScript✅ Tipe ketat dihasilkan otomatis dari konten⚠️ Didukung lewat konfigurasi manual global.d.ts
    Deteksi terjemahan yang hilang✅ Error TypeScript + error/peringatan saat build⚠️ Runtime mengembalikan kunci atau melempar error tergantung konfigurasi
    Konten kaya (JSX / Markdown / komponen)✅ Dukungan langsung⚠️ Melalui t.rich() dengan pemetaan komponen
    Dukungan ICU MessageFormat⚠️ Sedang dikembangkan✅ Ya, dukungan penuh ICU
    Komponen server sinkron✅ useIntlayer dari next-intlayer/server bekerja di komponen server turunan❌ Memerlukan penerusan terjemahan melalui props dari server asinkron
    Tree-shaking✅ Otomatis per komponen dan per lokal⚠️ Memerlukan pemisahan namespace manual dan penggunaan pick()
    Pemuatan lambat (Lazy loading)✅ Satu baris konfigurasi (importMode: 'dynamic')⚠️ Memerlukan impor dinamis manual di getRequestConfig
    Visual Editor / CMS✅ Visual Editor gratis + CMS opsional❌ Tidak ada
    Terjemahan bertenaga AI✅ Terintegrasi, menggunakan kunci API Anda sendiri❌ Tidak ada
    Server MCP & Keterampilan Agen✅ Ya❌ Tidak ada

    Tolok ukur

    Apa yang diukur

    Rangkaian pengujian Benchmark Bloom membangun aplikasi yang sama dengan setiap pustaka: 10 halaman (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 lokal (en, fr, es, de, it, pt, zh, ja, ko, ru), komponen identik, dan konten identik. Halaman diukur dalam en dan fr. Setiap pustaka diuji hingga empat strategi pemuatan:

    StrategiDeskripsiSiapa yang menggunakan
    staticSemua lokal dan semua halaman dibundel bersama di awalPrototipe cepat, kode hasil AI
    dynamicHanya lokal aktif yang dimuat, tetapi semua halaman sekaligusMayoritas proyek
    scoped-staticNamespace per rute, tanpa lazy loadingJarang
    scoped-dynamicNamespace per rute + lazy loading. Hanya halaman aktif pada lokal aktif yang dikirimAplikasi dengan anggaran kinerja ketat

    Intlayer tidak memerlukan varian "scoped": kompiler otomatis membatasi konten per komponen, sehingga baris static dan dynamic sudah ter-scope secara bawaan.

    Untuk setiap build, pengujian mencatat:

    • Ukuran pustaka (Lib size): ukuran gzip dari komponen kosong yang hanya mengimpor pustaka i18n.
    • JS Halaman (Page JS): JavaScript gzip terunduh per halaman.
    • % kebocoran lokal (Locale leak %): proporsi string yang bukan milik lokal yang sedang aktif.
    • % kebocoran halaman (Page leak %): proporsi string yang bukan milik halaman yang sedang diakses.
    • Rata-rata komponen (Component avg): ukuran gzip rata-rata dari setiap komponen yang dikompilasi terisolasi.
    • Reaktivitas E2E: waktu antara pergantian lokal hingga pembaruan html[lang] di DOM.
    • Hidrasi: durasi fase hidrasi React.
    Angka di bawah ini berasal dari pengujian bertanggal 2026-09-12 dengan next-intl 4.14.2 dan intlayer 9.5.1.

    Hasil pada Next.js (App Router)

    Pilih metrik dan pustaka yang penting bagi Anda:

    Metrik

    Pemuatan JSON dinamis

    Memuat terjemahan secara lambat saat runtime

    JSON cakupan (namespacing)

    Namespace terjemahan per halaman

    Apa metrik ini?

    Total ukuran kompresi gzip dari bundel pustaka internasionalisasi. Ini hanya mencakup penyedia dan logika pengambilan konten setelah tree-shaking dan minifikasi.

    Mengapa ini penting?

    Ukuran pustaka yang lebih kecil mengurangi muatan JavaScript awal, yang mengarah pada waktu unduh dan eksekusi yang lebih cepat pada klien.

    Lihat sebagai

    PustakaStrategiUkuran Lib (gz)Rata-rata JS Halaman (gz)Bocor LokalBocor HalamanRata-rata Komponen (gz)Reaktivitas E2EHidrasi
    base (tanpa i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.8 ms
    next-intlayerstatic5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayerdynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms
    @intlayer/next-intl (kompat)static8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intl (kompat)dynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms

    Cara membaca hasil

    • Biaya runtime. Aplikasi dasar berukuran 141.0 KB per halaman. next-intl meningkatkannya menjadi 153.6 KB (+12.6 KB gzip di setiap halaman), sedangkan Intlayer hanya 141.3 KB (+0.3 KB).
    • Kebocoran. Dalam pengaturan yang paling sering digunakan (static dan dynamic), next-intl mengirimkan ~90% string halaman lain di setiap halaman, karena seluruh en.json masuk ke penyedia klien. Menghilangkannya membutuhkan pekerjaan pemisahan manual yang teliti. Intlayer langsung 0% secara otomatis.
    • Ukuran komponen. Komponen yang memanggil useTranslations() menghasilkan rata-rata 21.8 KB; komponen yang sama dengan useIntlayer() hanya 6.9 KB.
    Tabel lengkap, setiap pustaka dan strategi, dalam laporan benchmark Next.js.

    Hasil pada TanStack Start (use-intl)

    use-intl adalah inti independen framework dari next-intl. API yang sama, format pesan yang sama. Membandingkannya dengan intlayer di TanStack Start menghilangkan bagian khusus Next.js dari persamaan.

    PustakaStrategiUkuran Lib (gz)Rata-rata JS Halaman (gz)Bocor LokalBocor HalamanRata-rata Komponen (gz)Reaktivitas E2E
    base (tanpa i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms
    @intlayer/use-intl (kompat)dynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms

    Cara membaca hasil

    • Pengaturan sederhana use-intl mengirimkan 68.8 KB lebih banyak JS per halaman dibandingkan aplikasi dasar.
    • Dalam mode dynamic, use-intl mencapai 119.4 KB, namun masih membawa 89.8% kebocoran halaman.
    • Perbedaan arsitektur sangat terlihat pada ukuran komponen: 76-87 KB pada use-intl berbanding 6-8 KB pada Intlayer.
    • Pergantian lokal 2x-4x lebih cepat dengan Intlayer (3 ms vs 7-21 ms).
    Tabel lengkap dalam laporan benchmark TanStack Start.

    Mengapa ada perbedaan? Katalog terpusat vs kamus terkompilasi

    Centralized catalogs versus per-component dictionaries

    next-intl mengikuti model klasik: satu JSON per lokal, dimuat dalam getRequestConfig, diteruskan ke NextIntlClientProvider, dan dibaca lewat t("namespace.key").

    bash
    .
    ├── messages
    │   ├── en.json
    │   └── fr.json
    └── src
        ├── i18n
        │   ├── request.ts
        │   └── routing.ts
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    └── page.tsx
    

    Runtime tidak dapat memprediksi kunci apa yang akan digunakan halaman, sehingga mengirimkan seluruh katalog adalah langkah paling aman.

    Biaya kegagalan mencapainya tumbuh pada dua sumbu sekaligus, halaman dan lokalitas:

    Theoretical content leakage by architecture

    Intlayer membalikkan model tersebut. Konten dideklarasikan langsung di sebelah komponen:

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    ├── page.tsx
                    └── page.content.ts
        └── components
            └── Counter
                ├── index.tsx
                └── index.content.ts
    

    Saat build, kompiler melihat komponen mana yang mengimpor kamus mana, dan hanya membundel kamus tersebut untuk lokal yang aktif.

    Untuk mendapatkan angka pada baris dynamic, atur dictionary.importMode: 'dynamic' di intlayer.config.ts. Lihat panduan optimasi bundle.

    Pengalaman pengembang

    Komponen klien

    messages/en.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/ClientCounter.tsx
    "use client";
    
    import { useState } from "react";
    import { useTranslations, useFormatter } from "next-intl";
    
    export const Counter = () => {
      const t = useTranslations("counter");
      const format = useFormatter();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{format.number(count)}</p>
          <button aria-label={t("label")} onClick={() => setCount((c) => c + 1)}>
            {t("increment")}
          </button>
        </div>
      );
    };
    
    Ingatlah untuk menyertakan namespace counter dalam pesan yang diteruskan ke NextIntlClientProvider di setiap halaman yang merender komponen ini.
    src/components/Counter/index.content.ts
    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;
    
    src/components/Counter/index.tsx
    "use client";
    
    import { useState } from "react";
    import { useIntlayer } from "next-intlayer";
    import { useNumber } from "next-intlayer/format";
    
    export const Counter = () => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label} onClick={() => setCount((c) => c + 1)}>
            {increment}
          </button>
        </div>
      );
    };
    

    Tidak ada yang perlu didaftarkan di halaman: komponen membawa kontennya sendiri.

    Komponen server sinkron

    Elemen UI bersama (navbar, footer, kartu) sering kali merupakan komponen server yang dirender sebagai anak dari komponen klien, sehingga tidak boleh bersifat async.

    src/components/ServerCounter.tsx
    type ServerCounterProps = {
      t: (key: string) => string;
      formattedCount: string;
    };
    
    export const ServerCounter = ({ t, formattedCount }: ServerCounterProps) => (
      <div>
        <p>{formattedCount}</p>
        <button aria-label={t("label")}>{t("increment")}</button>
      </div>
    );
    

    Halaman harus memanggil await getTranslations("counter") dan await getFormatter(), lalu meneruskan hasilnya ke bawah sebagai props. Komponen tidak lagi mandiri.

    src/components/ServerCounter.tsx
    import { useIntlayer } from "next-intlayer/server";
    import { useNumber } from "next-intlayer/server/format";
    
    export const ServerCounter = ({ count }: { count: number }) => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    

    Metadata

    src/app/[locale]/about/page.tsx
    import type { Metadata } from "next";
    import { getTranslations } from "next-intl/server";
    import { routing } from "@/i18n/routing";
    
    const localizedPath = (locale: string, path: string) =>
      locale === routing.defaultLocale ? path : `/${locale}${path}`;
    
    export const generateMetadata = async ({
      params,
    }: {
      params: Promise<{ locale: string }>;
    }): Promise<Metadata> => {
      const { locale } = await params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const languages = Object.fromEntries(
        routing.locales.map((l) => [l, localizedPath(l, "/about")])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, "/about"),
          languages: { ...languages, "x-default": "/about" },
        },
      };
    };
    
    src/app/[locale]/about/page.tsx
    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("about-metadata", locale);
      const multilingualUrls = getMultilingualUrls("/about");
    
      return {
        ...metadata,
        alternates: {
          canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
          languages: { ...multilingualUrls, "x-default": "/about" },
        },
      };
    };
    

    Pertahankan API next-intl, dapatkan hasil Intlayer

    Anda tidak perlu menulis ulang komponen untuk mendapatkan angka performa di atas. Paket @intlayer/next-intl adalah adaptor siap pakai: ia mempertahankan useTranslations, getTranslations, useFormatter, t.rich(), dan bentuk jamak ICU, sambil menyajikannya dari kamus Intlayer yang dikompilasi oleh kompiler Intlayer.

    next.config.ts
    import type { NextConfig } from "next";
    import { createNextIntlPlugin } from "@intlayer/next-intl/plugin";
    
    const withIntlayer = createNextIntlPlugin();
    
    const nextConfig: NextConfig = {};
    
    export default withIntlayer(nextConfig);
    

    Dalam pengujian, build kompatibilitas dari aplikasi yang sama turun dari 153.6 KB ke 147.5 KB per halaman, ukuran komponen turun dari 21.8 KB ke 8.1 KB, dan kebocoran halaman turun dari ~90% ke 0%, tanpa mengubah kode aplikasi. File messages/{locale}.json yang ada dapat tetap menjadi sumber data utama melalui plugin sinkronisasi JSON.

    Lihat panduan migrasi next-intl untuk langkah-langkah detailnya.

    Kapan harus memilih yang mana?

    Anda menginginkan standar ekosistem untuk Next.js, mengandalkan ICU MessageFormat, aplikasi Anda berskala kecil hingga menengah, atau Anda berintegrasi dengan platform penerjemahan (Crowdin, Phrase, Lokalise...) yang mengharapkan JSON terpusat. Alokasikan waktu untuk membagi katalog ke dalam namespace dan memilih pesan dengan pick() per halaman jika performa menjadi prioritas.

    Anda menginginkan konten dengan cakupan komponen, TypeScript yang ketat, kesalahan kunci hilang pada waktu build, tree-shaking dan pemuatan lambat (lazy loading) tanpa usaha, komponen server sinkron, dan alat editorial bawaan (Editor Visual, CMS, terjemahan AI, server MCP). Sangat relevan untuk basis kode modular besar dan sistem desain.

    Anda sudah menggunakan next-intl dan ingin penghematan ukuran bundel tanpa perlu menulis ulang kode. Adaptor kompatibilitas menjaga impor dan file messages/{locale}.json Anda sebagai sumber kebenaran tunggal. Diukur berdampingan dalam next-intl vs @intlayer/next-intl.

    FAQ

    Tidak pada waktu render. Perbedaannya terletak pada apa yang dikirim ke browser: next-intl berbiaya +12.6 KB gzip runtime pada setiap halaman dan pada konfigurasi umum mengirimkan ~90% string halaman lain bersama setiap halaman. Peralihan bahasa dan hidrasi sebanding di Next.js (15-18 ms); di TanStack Start use-intl membutuhkan 7-21 ms dibandingkan 3-4 ms untuk Intlayer.

    Bisa, dengan konfigurasi scoped-dynamic: bagi messages/{locale}.json menjadi satu namespace per rute, lalu gunakan pick(messages, [...]) di setiap halaman dan jaga pemetaan tersebut tetap sinkron saat komponen berpindah. Baris scoped-* dalam tolok ukur mencerminkan pekerjaan tersebut. Intlayer mencapai 0% secara bawaan karena kompilator membatasi konten per komponen. Lihat pengoptimalan bundel.

    Tidak. @intlayer/next-intl mempertahankan useTranslations, getTranslations, useFormatter, t.rich(), bentuk jamak ICU, dan helper navigasi, lalu menyediakannya dari kamus yang dikompilasi. Cukup satu baris plugin di next.config.ts. Panduan langkah demi langkah di panduan migrasi next-intl.

    Dukungan ICU sedang dikembangkan pada API asli. Adaptor kompatibilitas (@intlayer/next-intl, @intlayer/use-intl) sudah menjalankan ICU: bentuk jamak, select, selectordinal, #, dan {ts, date, long} diproses melalui penyelesai ICU Intlayer. Baca format pesan ICU untuk detailnya.

    Bisa. Plugin sinkronisasi JSON membacanya, membagi kunci tingkat atas menjadi kamus, dan menulis kembali terjemahan ke file yang sama ketika CLI atau CMS memperbaruinya. Alur kerja penerjemah Anda tidak berubah.

    Perbandingan terkait

    Tolok ukur yang sama, pustaka lain:

    Mendalami next-intl:

    Dokumentasi referensi:

    Untuk memahami asal-usul pustaka-pustaka ini, baca sejarah i18n di JavaScript.

    Bintang GitHub

    Bintang GitHub adalah indikator kuat dari popularitas proyek, kepercayaan komunitas, dan prospek jangka panjang.

    Grafik Riwayat Bintang

    Aktivitas commit

    Bintang menunjukkan popularitas. Commit menunjukkan seberapa banyak kerja yang masuk ke sebuah proyek. Saat tulisan ini dibuat, Intlayer memiliki sekitar 7.500 commit, lebih banyak dari sebagian besar library yang dibandingkan di sini, dan sekitar 5 kali lebih banyak dari next-intl atau next-i18next.

    • amannn/next-intl
    • aymericzip/intlayer

    Commit pada branch default, sumber: GitHub API.

    Intlayer adalah monorepo, jadi jumlah ini mencakup setiap paket framework, CLI, dan dokumentasi. Baca commit sebagai sinyal aktivitas, bukan kualitas.

    Unduhan npm

    • next-intl
    • next-intlayer

    Sumber: API unduhan registri npm.

    Jumlah unduhan menghargai solusi yang paling lama, bukan yang terbaik. Library yang dirilis bertahun-tahun lalu masih diinstal oleh setiap proyek yang memilihnya saat itu, oleh setiap eksekusi CI, dan oleh setiap paket yang bergantung padanya. Angka ini lebih mengukur inersia daripada pilihan baru.

    Asisten AI memperkuat efek ini. next-intl, i18next, dan vue-i18n ada di mana-mana dalam kode yang menjadi data latih mereka, sehingga mereka menyarankannya secara default tanpa membandingkan alternatif. Setiap saran menambah unduhan, yang kemudian mendorong saran berikutnya. Bandingkan berdasarkan benchmark, bukan jumlah unduhan.

    Kesimpulan

    next-intl adalah pustaka yang solid dan dirawat dengan baik, dan pengujian mengonfirmasi bahwa pustaka ini merupakan pilihan yang baik di Next.js. Namun model katalog terpusat membebankan setiap optimasi kepada pengembang: pengaturan sederhana membocorkan sekitar 90% konten halaman lain, dan runtime-nya sendiri berbiaya +12.6 KB gzip pada setiap halaman.

    Intlayer memindahkan semua pekerjaan itu ke kompiler. Kamus per komponen, lazy loading per lokal, dan pembersihan konten yang tidak terpakai adalah output build otomatis. Hasilnya pada aplikasi yang sama: +0.3 KB per halaman, 0% kebocoran, komponen 3x lebih kecil, dan pergantian lokal 2x-4x lebih cepat pada TanStack Start.

    Semua data mentah, aplikasi pengujian, dan skrip tersedia di repositori Benchmark Bloom. Anda dapat menjalankannya sendiri.

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

    Komentar

    Belum ada komentar. Jadilah yang pertama membagikan pemikiran Anda.

    Postingan Terkait

    Postingan Terbaru