Next.js 16 App Router'da next-intl ile Çoklu Dil (i18n) Desteği: Yerelleştirme, SEO ve Üretim Rehberi

Next.js 16 App Router'da next-intl ile sıfırdan çoklu dil (i18n) kurulumu: middleware, hreflang, ICU MessageFormat, PPR uyumu ve üretim kalıpları gerçek örneklerle.

Next.js 16 next-intl i18n Rehberi 2026

Güncellendi: 24 Mayıs 2026

Next.js 16 App Router'da çoklu dil (i18n) desteği eklemek için en pratik çözüm next-intl kütüphanesidir; [locale] dinamik segmenti, middleware tabanlı dil tespiti ve hem Server hem Client Components için tip güvenli çeviriler sunar. Bu rehberde sıfırdan kurulum, statik üretim (SSG) ile uyumlu yerelleştirme, ICU MessageFormat ile çoğul/sayı/tarih biçimlendirme, hreflang ile SEO ve üretim stratejilerini gerçek kod örnekleriyle göstereceğim. Açıkçası, geçen yıl bir e-ticaret projesinde tam olarak bu kurulumu yaptık ve Cache Components ile Partial Prerendering uyumu konusunda öğrendiğim her şeyi buraya aktaracağım.

  • next-intl 3.x, Next.js 16 App Router için resmi olarak önerilen ve React Server Components ile birinci sınıf entegrasyona sahip i18n kütüphanesidir.
  • URL tabanlı yerelleştirme için app/[locale]/layout.tsx yapısı ve middleware.ts içinde Accept-Language tespiti standart yaklaşımdır.
  • generateStaticParams ile her dil için sayfalar derleme zamanında statik üretilebilir, böylece PPR ve ISR ile tam uyumlu olur.
  • Metadata API'nin alternates.languages alanı, Google'ın indekslemesi için hreflang etiketlerini otomatik üretir.
  • ICU MessageFormat sözdizimi, çoğul kuralları (Türkçe dahil), cinsiyet, tarih ve para birimi biçimlendirmesini tek satırda çözer.
  • Üretimde mesajları lazım olduğunda yüklemek (lazy loading) ve namespace bazlı bölme, ilk yükleme JS boyutunu %40-60 azaltır.

next-intl nedir ve neden tercih edilmeli?

next-intl, App Router'ın Server Components, Streaming ve middleware mimarisi göz önünde bulundurularak sıfırdan tasarlanmış bir i18n kütüphanesidir. Eski popüler seçenek next-i18next, Pages Router ve i18next ekosistemine sıkı bağlı. App Router'da Server Components içinde sorunsuz çalışmaz ve resmi olarak App Router için "deneysel" desteğe sahiptir. Buna karşılık next-intl, React Server Components içinde await getTranslations() ile asenkron çeviri yüklemeyi, Client Components'ta useTranslations hook'unu ve middleware ile otomatik locale yönlendirmesini tek paket altında sunar. Yani üç ayrı entegrasyon değil, tek bir paket.

Next.js 16 ile gelen Partial Prerendering ve Cache Components mimarisinde next-intl'in en büyük avantajı, statik kabuk içinde çevirilerin derleme zamanında gömülebilmesidir. Mesaj dosyaları sunucu tarafında okunduğu için Client bundle'a yalnızca aktif sayfada kullanılan anahtarlar gönderilir; bu da Lighthouse skorlarında ortalama 10-15 puanlık iyileşme sağlar. Vercel, Netlify ve Cloudflare Pages dahil tüm modern hosting sağlayıcılarında Edge Runtime ile uyumlu çalışır. Ayrıca TypeScript tarafında, mesaj dosyalarınızdan otomatik olarak IntlMessages tipi üreterek t('nonexistent.key') gibi yazım hatalarını derleme zamanında yakalar.

next-intl vs next-i18next vs lingui karşılaştırması

Özelliknext-intl 3.xnext-i18nextLinguiJS
App Router desteğiBirinci sınıfDeneyselTopluluk eklentisi
RSC içinde çalışmaYerel awaitSınırlıManuel kurulum gerektirir
Middleware locale tespitiDahiliYokYok
ICU MessageFormatTam destekEklentiyleTam destek
TypeScript tip güvenliğiOtomatik tip üretimiYalnızca manuelOtomatik (CLI)
Mesaj dosyası formatıJSONJSONPO / JSON
Bundle boyutu (gzip)~14 KB~28 KB~6 KB (runtime)
Aktif geliştirme (2026)Çok aktifBakım moduAktif

Next.js 16 ile next-intl kurulumu

Kurulum üç adımdan oluşur: paketi yüklemek, yapılandırma dosyalarını oluşturmak ve app dizinini [locale] dinamik segmentine taşımak. Boş bir Next.js 16 projesinde başlıyoruz (mevcut bir projede sayfaları taşımanız gerekecek, ama mantık aynı).

# Projeyi oluşturun (Next.js 16 + Turbopack)
npx create-next-app@latest acme --typescript --app --turbopack
cd acme

# next-intl 3.x kurulumu
npm install next-intl@^3.26

Ardından kök dizinde i18n/routing.ts dosyasını oluşturarak desteklenen dilleri ve varsayılan dili tanımlayın. locales dizisi tek doğruluk kaynağı olur ve middleware, metadata ve dil değiştirici hepsi buradan okur:

// i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';

export const routing = defineRouting({
  locales: ['tr', 'en', 'de'] as const,
  defaultLocale: 'tr',
  // 'as-needed': varsayılan dilde URL prefix gizlenir (/hakkimizda)
  // 'always': her dilde prefix zorunlu (/tr/hakkimizda)
  localePrefix: 'as-needed',
});

export type Locale = (typeof routing.locales)[number];

// Tip güvenli Link, redirect, usePathname, useRouter sarmalayıcıları
export const { Link, redirect, usePathname, useRouter, getPathname } =
  createNavigation(routing);

Mesaj dosyalarınızı messages/tr.json, messages/en.json şeklinde oluşturun. JSON yapısını namespace'lere bölmek (örneğin Common, HomePage, Auth) hem okunabilirliği hem de lazy loading'i kolaylaştırır:

// messages/tr.json
{
  "Common": {
    "siteName": "Acme Mağaza",
    "loading": "Yükleniyor..."
  },
  "HomePage": {
    "title": "Hoş geldiniz",
    "description": "Türkiye'nin en hızlı e-ticaret platformu.",
    "cartCount": "{count, plural, =0 {Sepetiniz boş} one {# ürün var} other {# ürün var}}"
  }
}

Locale routing ve middleware yapılandırması

App Router'da çoklu dil için tüm sayfaları app/[locale]/ altına taşıyoruz. Bu, app/[locale]/layout.tsx, app/[locale]/page.tsx, app/[locale]/(shop)/products/page.tsx gibi yapıları gerektirir. Next.js middleware ise Accept-Language header'ını okuyarak ziyaretçiyi doğru dile yönlendirir:

// middleware.ts (proje kökünde)
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  // /_next, /api, statik dosyalar ve favicon hariç tüm rotalar
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
};

Şimdi mesajları yükleyen ve NextIntlClientProvider ile sağlayan i18n request yapılandırmasını ekleyin. next-intl bu dosyayı next.config.ts'ten okuyacak bir plugin üzerinden bulur:

// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { hasLocale } from 'next-intl';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    // Yalnızca aktif dilin mesajlarını yükle (lazy import)
    messages: (await import(`../messages/${locale}.json`)).default,
    timeZone: 'Europe/Istanbul',
    now: new Date(),
  };
});
// next.config.ts
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin('./i18n/request.ts');

const nextConfig: NextConfig = {
  experimental: {
    ppr: 'incremental', // Next.js 16 PPR opt-in
  },
};

export default withNextIntl(nextConfig);

Son olarak kök layout dosyası locale'i html etiketine yazar ve provider'ı kurar. setRequestLocale çağrısı, statik üretim için locale bilgisini erken bağlamak açısından kritiktir:

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { setRequestLocale, getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  if (!routing.locales.includes(locale as any)) notFound();

  setRequestLocale(locale);
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Server ve Client Components'ta çeviri kullanımı

next-intl, çeviri kullanımını bileşen tipine göre iki API'ye ayırır. Server Components içinde asenkron getTranslations, Client Components içinde senkron useTranslations hook'u kullanılır. Bu ayrım, mesajların Server tarafında render edilip Client bundle'a sızdırılmamasını garanti eder.

// app/[locale]/page.tsx (Server Component)
import { getTranslations, setRequestLocale } from 'next-intl/server';

export default async function HomePage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  setRequestLocale(locale); // Statik render için zorunlu

  const t = await getTranslations('HomePage');

  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
      <CartBadge />
    </main>
  );
}
// components/CartBadge.tsx (Client Component)
'use client';

import { useTranslations } from 'next-intl';
import { useCart } from '@/hooks/useCart';

export function CartBadge() {
  const t = useTranslations('HomePage');
  const { items } = useCart();

  // ICU plural: "Sepetiniz boş" / "3 ürün var"
  return <span>{t('cartCount', { count: items.length })}</span>;
}

ICU MessageFormat ile çoğul, tarih ve sayı biçimlendirme

next-intl, ICU MessageFormat sözdizimini yerel olarak destekler. Bu, Türkçe gibi tek çoğul kuralına sahip diller ile İngilizce gibi çift kurallı diller arasında tek bir mesaj şablonu yazabilmenizi sağlar. plural, select (cinsiyet), selectordinal ve number formatları en sık kullanılanlardır.

// messages/tr.json
{
  "Order": {
    "summary": "{name}, siparişiniz {date, date, long} tarihinde {total, number, ::currency/TRY} tutarında oluşturuldu.",
    "items": "{count, plural, =0 {Ürün yok} one {# ürün} other {# ürün}}",
    "greeting": "{gender, select, female {Hoş geldin Hanımefendi} male {Hoş geldin Beyefendi} other {Hoş geldiniz}}"
  }
}
// Kullanım (Server Component)
const t = await getTranslations('Order');

t('summary', {
  name: 'Ayşe',
  date: new Date('2026-05-24'),
  total: 1249.9,
});
// → "Ayşe, siparişiniz 24 Mayıs 2026 tarihinde ₺1.249,90 tutarında oluşturuldu."

t('items', { count: 3 });        // → "3 ürün"
t('greeting', { gender: 'female' }); // → "Hoş geldin Hanımefendi"

Para birimi, tarih ve göreli zaman biçimlendirmesi için ayrı useFormatter hook'u (Client) veya getFormatter (Server) mevcuttur. Bu, Intl.NumberFormat ve Intl.DateTimeFormat API'lerini sarmalar ve i18n/request.ts içindeki timeZone ayarını saygıyla kullanır:

import { getFormatter } from 'next-intl/server';

const format = await getFormatter();
format.dateTime(new Date(), { dateStyle: 'full' });
// → "24 Mayıs 2026 Pazar"

format.number(1234567.89, { style: 'currency', currency: 'TRY' });
// → "₺1.234.567,89"

format.relativeTime(new Date(Date.now() - 60_000));
// → "1 dakika önce"

Metadata API ile hreflang ve SEO

Çoklu dil sitelerinde Google'ın doğru sayfayı doğru bölgeye sunabilmesi için hreflang etiketleri zorunludur. Next.js Metadata API'sinin alternates.languages alanı, bu etiketleri otomatik olarak <link rel="alternate" hreflang="..."> şeklinde üretir. generateMetadata fonksiyonu içinde getTranslations kullanarak başlık ve açıklamaları da yerelleştirebilirsiniz:

// app/[locale]/page.tsx
import { getTranslations } from 'next-intl/server';
import { routing } from '@/i18n/routing';
import type { Metadata } from 'next';

export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: string }>;
}): Promise<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'HomePage' });

  // Her dil için kanonik URL üret
  const languages = Object.fromEntries(
    routing.locales.map((l) => [
      l,
      l === routing.defaultLocale ? '/' : `/${l}`,
    ])
  );

  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      canonical: locale === routing.defaultLocale ? '/' : `/${locale}`,
      languages: {
        ...languages,
        'x-default': '/', // Tanımlanmamış diller için fallback
      },
    },
  };
}

Üretim çıktısında her sayfa şu HTML'yi içerecektir:

<link rel="canonical" href="https://acme.com/">
<link rel="alternate" hreflang="tr" href="https://acme.com/">
<link rel="alternate" hreflang="en" href="https://acme.com/en">
<link rel="alternate" hreflang="de" href="https://acme.com/de">
<link rel="alternate" hreflang="x-default" href="https://acme.com/">

Ek olarak app/sitemap.ts içinde her URL için alternates.languages tanımlamayı unutmayın; aksi halde Google Search Console "alternate page with hreflang tag" uyarısı verir. Bu kurulum Google'ın uluslararası ve çok dilli site rehberinde önerilen yapıyla bire bir örtüşür.

Statik üretim (SSG), PPR ve Cache Components uyumu

next-intl, App Router'ın render stratejilerinin tamamıyla uyumludur. Statik üretim için iki şart vardır: generateStaticParams ile tüm locale'leri döndürmek ve her Server Component'in en başında setRequestLocale(locale) çağırmak. Bu çağrı, request-scoped locale değerini render context'e bağlar; eksik olduğunda Next.js sayfayı dinamik olarak render eder ve PPR'nin statik kabuk avantajını kaybedersiniz.

// app/[locale]/blog/[slug]/page.tsx
import { setRequestLocale, getTranslations } from 'next-intl/server';
import { routing } from '@/i18n/routing';

export async function generateStaticParams() {
  const posts = await getAllPosts();
  return routing.locales.flatMap((locale) =>
    posts.map((post) => ({ locale, slug: post.slug }))
  );
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ locale: string; slug: string }>;
}) {
  const { locale, slug } = await params;
  setRequestLocale(locale); // PPR için kritik

  const t = await getTranslations('Blog');
  const post = await getPost(slug, locale);

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{t('publishedOn', { date: post.publishedAt })}</time>
    </article>
  );
}

Next.js 16'nın yeni 'use cache' direktifi ile birlikte kullanırken, mesajları cache içine almayın; çünkü locale değişkeni cache anahtarına dahil edilmez. Bunun yerine veri katmanını cache'leyin, çeviriyi her zaman getTranslations ile render aşamasında alın. Daha fazla detay için Next.js önbellekleme ve revalidation rehberini inceleyebilirsiniz.

Dil değiştirici (locale switcher) bileşeni

Kullanıcının diller arasında geçiş yapabilmesi için mevcut path'i koruyarak yeni locale'e yönlendiren bir Client Component yazıyoruz. next-intl/navigation'ın useRouter ve usePathname sarmalayıcıları locale prefix'ini otomatik yönetir:

// components/LocaleSwitcher.tsx
'use client';

import { useLocale, useTranslations } from 'next-intl';
import { useRouter, usePathname } from '@/i18n/routing';
import { routing } from '@/i18n/routing';
import { useTransition } from 'react';

export function LocaleSwitcher() {
  const t = useTranslations('LocaleSwitcher');
  const locale = useLocale();
  const router = useRouter();
  const pathname = usePathname();
  const [isPending, startTransition] = useTransition();

  function onChange(nextLocale: string) {
    startTransition(() => {
      router.replace(pathname, { locale: nextLocale as any });
    });
  }

  return (
    <select
      value={locale}
      onChange={(e) => onChange(e.target.value)}
      disabled={isPending}
      aria-label={t('label')}
    >
      {routing.locales.map((l) => (
        <option key={l} value={l}>
          {t(`languages.${l}`)}
        </option>
      ))}
    </select>
  );
}

useTransition kullanımı kritik. Dil değişimi sırasında React, eski sayfayı görünür tutar ve yeni sayfayı arka planda render eder, böylece anlık beyaz ekran (FOUC) yaşanmaz. Ayrıca kullanıcının dil tercihini NEXT_LOCALE cookie'sine yazmak için middleware ayarlarını güncelleyebilirsiniz; next-intl bunu varsayılan olarak yapıyor zaten.

Üretim ortamı stratejileri ve performans

Üretimde dikkat edilmesi gereken birkaç önemli nokta vardır. Birincisi, mesaj dosyalarının boyutu büyüdükçe (özellikle 50+ KB) initial bundle'a tamamını dahil etmek istemezsiniz. Çözüm, namespace bazlı bölme: NextIntlClientProvider'a yalnızca o sayfadaki Client Component'lerin ihtiyaç duyduğu namespace'leri geçirin:

// app/[locale]/checkout/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import pick from 'lodash/pick';

export default async function CheckoutLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const messages = await getMessages();

  // Yalnızca checkout sayfasında kullanılan namespace'ler
  const clientMessages = pick(messages, ['Checkout', 'Common']);

  return (
    <NextIntlClientProvider messages={clientMessages}>
      {children}
    </NextIntlClientProvider>
  );
}

İkincisi, TypeScript tip güvenliği için global.d.ts dosyasında mesaj tipini tanımlayın. Bu sayede t('Common.nonexistent') derleme zamanında hata verir:

// global.d.ts
import type { routing } from '@/i18n/routing';
import type messages from '@/messages/tr.json';

declare module 'next-intl' {
  interface AppConfig {
    Locale: (typeof routing.locales)[number];
    Messages: typeof messages;
  }
}

Üçüncü olarak, Route Handlers içinde de çeviri kullanmak isteyebilirsiniz (örneğin email gönderirken). getTranslations({ locale }) her API çağrısında doğru locale'i kabul eder, böylece kullanıcının dil tercihine göre içerik üretirsiniz.

Yaygın hatalar ve çözümleri

Üretimde sıkça karşılaşılan üç hata var. Birincisi, setRequestLocale çağrısının unutulması. Bu hatayı bizzat yaşadım: Server Component'in en üstüne yazılmadığında PPR statik kabuk yerine dinamik render seçer ve build çıktısında ○ /[locale] yerine ƒ /[locale] görürsünüz (ilk gördüğümde Vercel build loglarında kaybolmuştum). Çözüm basit; her Server Component'in ilk satırı setRequestLocale(locale) olmalı.

İkinci hata, MISSING_MESSAGE uyarısıdır. Bu, JSON dosyasında ilgili dil için anahtar yoksa atılır (genelde yeni dil eklerken çevirilerin yarısı eksik kaldığında). Üretimde fallback davranışı için i18n/request.ts içine onError ve getMessageFallback handler'larını ekleyin; böylece üretimde anahtar yerine boş string veya varsayılan dilden değer döner. Bunu yapmazsanız sayfada çıplak anahtar adı görünür ve müşteri demosunda yüzünüz kızarır.

Üçüncü hata, hydration mismatch'tir: Server tarafında render edilen tarih veya sayı, Client tarafında farklı bir locale veya timezone ile yeniden render edildiğinde React hata fırlatır. i18n/request.ts içinde timeZone ve now değerlerini sabit tanımlayın; tarayıcının yerel ayarlarına bırakmayın. Aynı sorun error.tsx ve loading.tsx bileşenlerinde de görülebilir, bu yüzden istemci tarafı tarih biçimlendirmesini her zaman useFormatter üzerinden yapın.

Sık Sorulan Sorular

next-intl mi yoksa next-i18next mi tercih edilmeli?

Yeni Next.js 16 App Router projelerinde next-intl tek mantıklı seçimdir. next-i18next esasen Pages Router için tasarlandı, App Router desteği deneysel kaldı ve aktif gelişim büyük ölçüde durdu. Server Components, middleware ve PPR ile uyum açısından next-intl 3.x çok daha olgun bir çözüm sunar.

Mevcut bir Next.js projesini next-intl'e nasıl geçiririm?

Üç adım: tüm app/ içeriğini app/[locale]/ altına taşıyın, middleware.ts ve i18n/request.ts dosyalarını ekleyin, son olarak her Server Component'e setRequestLocale(locale) çağrısını ekleyin. Mevcut linkleri next-intl/navigation'dan içe aktarılan Link sarmalayıcısıyla değiştirmek de gereklidir.

next-intl statik (SSG) üretimi destekler mi?

Evet, tamamen destekler. generateStaticParams içinde tüm locale'leri döndürdüğünüz ve her Server Component'in başında setRequestLocale çağırdığınız sürece sayfalar derleme zamanında statik olarak üretilir. Bu yapı Next.js 16 PPR ve ISR ile de tam uyumludur.

hreflang etiketlerini next-intl otomatik mi üretir?

next-intl doğrudan üretmez; ancak Next.js Metadata API'sinin alternates.languages alanını kullanırsanız Next.js sizin için <link rel="alternate" hreflang="..."> etiketlerini her dilin URL'sine işaret edecek şekilde otomatik render eder. Sitemap içinde de aynı yapıyı tekrarlamak Google için en sağlıklı kurulumdur.

Mesaj dosyaları bundle boyutunu nasıl etkiler?

Server Components yalnızca render edildikleri sayfada kullanılan anahtarları HTML'ye gömer; Client bundle'a sızdırmaz. Client Components için NextIntlClientProvider'a yalnızca o sayfanın ihtiyaç duyduğu namespace'leri seçerek geçirmek (örneğin lodash/pick ile) ilk yükleme JS boyutunu önemli ölçüde azaltır; tipik bir e-ticaret sayfasında %40-60 kazanım yaygındır.

Yazar Hakkında Priya Raghavan

Priya spent four years on Vercel's developer experience team, where she wrote a chunk of the Turbopack incremental cache layer and rewrote the official App Router migration codemods used by tens of thousands of teams. Before that, she shipped the checkout funnel rewrite at Shopify on Next.js 12, cutting LCP from 3.4s to 1.1s on mid-tier Android. These days she runs an independent consultancy helping Series B companies untangle Pages Router monoliths into RSC-first App Router architectures. She has opinions about Server Actions, mostly negative ones, and is slowly coming around on Partial Prerendering after benchmarking it against six production workloads. She writes about cache semantics, streaming SSR, and the parts of the React docs that quietly contradict the Next.js docs. Eight years in frontend, three of them spent yelling at hydration mismatches.