08 / 09
Çoklu Dil (i18n)
next-intl ile çeviri, dil yönlendirmesi ve tarih/para birimi formatı.
Çoklu dil desteği üç ayrı iş içerir: URL'den dili belirlemek (yönlendirme), metinleri çevirmek (yerelleştirme) ve tarih, sayı, para birimini kullanıcının diline göre göstermek (biçimlendirme).1 Bu rehber üçünü next-intl ile kuruyor. next-intl Next.js'e özel bir kütüphane. Vite ve düz React kullanıyorsanız rehberin sonunda kısa bir alternatif var.
Önce kavramlar
Locale, dil ve biçim tercihlerinin kimliğidir. Genellikle kullanıcının dilini ve isteğe bağlı olarak bölgesini içerir: en-US ABD İngilizcesi, nl-NL Hollanda'da Felemenkçe, nl bölgesiz Felemenkçe.1
| İş | Soru | next-intl'de |
|---|---|---|
| Yönlendirme | Hangi dil? /tr/urunler mi /en/products mi? | defineRouting, proxy |
| Yerelleştirme | Metin ne? | messages/*.json, useTranslations |
| Biçimlendirme | Sayı, tarih, para nasıl yazılır? | useFormatter |
next-intl'in biçimlendirme katmanı tarayıcının yerleşik Intl API'sinin üzerine kurulu: Intl.DateTimeFormat, NumberFormat, PluralRules, RelativeTimeFormat ve ListFormat dile duyarlı biçimlendirmeyi sağlıyor.2
Kurulum
Kurulum Next.js 16 ve next-intl 4 ile denendi.
pnpm add next-intlmessages/
├── tr.json
└── en.json
src/
├── i18n/
│ ├── routing.ts
│ ├── navigation.ts
│ └── request.ts
├── app/
│ └── [locale]/
│ ├── layout.tsx
│ └── page.tsx
└── proxy.ts
next.config.ts// next.config.ts
import type { NextConfig } from 'next'
import createNextIntlPlugin from 'next-intl/plugin'
const nextConfig: NextConfig = {}
const withNextIntl = createNextIntlPlugin()
export default withNextIntl(nextConfig)// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing'
export const routing = defineRouting({
locales: ['tr', 'en'],
defaultLocale: 'tr',
})locales desteklenen dillerin tamamı, defaultLocale ise hiçbir dil eşleşmediğinde kullanılacak yedek.3
// src/proxy.ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'
export default createMiddleware(routing)
export const config = {
// /api, /trpc, /_next, /_vercel ve nokta içeren yolları (favicon.ico gibi) atla
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
}// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing)// src/i18n/request.ts
import * as rootParams from 'next/root-params'
import { notFound } from 'next/navigation'
import { getRequestConfig } from 'next-intl/server'
import { hasLocale } from 'next-intl'
import { routing } from './routing'
export default getRequestConfig(async ({ locale }) => {
if (!locale) {
const paramValue = await rootParams.locale()
if (hasLocale(routing.locales, paramValue)) {
locale = paramValue
} else {
notFound()
}
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
}
})// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl'
import { getLocale } from 'next-intl/server'
import { routing } from '@/i18n/routing'
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }))
}
export default async function LocaleLayout({ children }: { children: React.ReactNode }) {
const locale = await getLocale()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider>{children}</NextIntlClientProvider>
</body>
</html>
)
}Mevcut layout.tsx ve page.tsx dosyalarını app/[locale]/ altına taşıyın.4 generateStaticParams her dil için sayfayı derleme zamanında statik üretir.4
Uyarı
Sürüme dikkat. proxy.ts dosyası middleware.ts'in yeni adı, Next.js 16'dan itibaren bu ad kullanılıyor. next/root-params Next.js 16.3 ve sonrasında varsayılan olarak açık, önceki sürümlerde experimental.rootParams ayarı gerekir.4 Next.js 15 için hazırlanmış eğitimlerde middleware.ts ve setRequestLocale kullanımını göreceksiniz, kavramlar aynı.5
Bir bileşende çeviri kullanmak:
// messages/tr.json
{
"HomePage": {
"title": "Mağazaya hoş geldiniz"
}
}// src/app/[locale]/page.tsx
import { useTranslations } from 'next-intl'
export default function HomePage() {
const t = useTranslations('HomePage')
return <h1>{t('title')}</h1>
}useTranslations('HomePage') çağrıyı bir isim alanına bağlar, t('title') HomePage.title anahtarına çözülür. Async Server Component'te useTranslations yerine getTranslations kullanın.67
Dil yönlendirmesi
Yönlendirmeyi URL'nin alt yoluyla (/fr/products) ya da alan adıyla (my-site.fr/products) yapabilirsiniz.1 Kullanıcının tarayıcı dili Accept-Language başlığıyla gelir ve dil seçiminin dayanağı olarak önerilir.1
next-intl bu işi proxy'de yapıyor. Denemede Accept-Language: en-US ile gelen istek /'ten /en'e, tr-TR ile gelen istek /tr'ye yönlendirildi. Desteklenmeyen bir yol (/de) ise /en/de'ye yönlendirilip 404'e düşer.
| Seçenek | Anlamı | Varsayılan |
|---|---|---|
localePrefix: 'always' | Her yolda dil öneki: /en/about | Evet3 |
localePrefix: 'as-needed' | Varsayılan dilde önek yok (/about), diğerlerinde var (/de/about)3 | |
localePrefix: 'never' | URL'de dil öneki yok. Alan adı veya çerez tabanlı yönlendirmede işe yarar3 | |
localeDetection | Accept-Language başlığı ve çerezden dil algılama | Açık. false yaparsanız dil yalnızca URL'den gelir3 |
localeCookie | En son algılanan dili NEXT_LOCALE oturum çerezinde saklar | Açık3 |
pathnames | Yolları da çevirir: '/about': { de: '/über-uns' }3 | Yok |
domains | Her alan adını kendi dillerine bağlar3 | Yok |
Doküman, SEO önemliyse yolların da çevrilmesini (pathnames) öneriyor.3
Link ve dil değiştirici
Link, redirect, useRouter gibi bileşenleri next/link yerine @/i18n/navigation'dan alın. Dil önekini sizin yerinize eklerler ve yerelleştirilmiş yolları (örneğin /de/über-uns) yönetirler.4 Dil değiştirmek için locale prop'u yeterli:
import { Link } from '@/i18n/navigation'
<Link href="/" locale="en">EN</Link>Kütüphanesiz yol
Next.js dokümanı kütüphanesiz bir yol da gösteriyor: proxy'de @formatjs/intl-localematcher ve negotiator ile dili seçip /en-US/products gibi bir yola yönlendirmek, tüm özel dosyaları app/[lang] altına almak ve her dil için bir sözlük (dictionaries/en.json) yüklemek.1 Sözlükler Server Component'te yüklendiği için çeviri dosyaları istemci paketini büyütmez.1 Bu yol az bağımlılık sunar ama çoğul, biçimlendirme ve yerelleştirilmiş link işlerini kendiniz yazarsınız. Doküman başka kütüphaneleri de listeliyor: next-international, lingui, tolgee, paraglide-next.1
Çeviri dosyaları ve mesaj sözdizimi
Mesajlar dil başına bir JSON dosyasıdır ve iç içe olabilir. Anahtarlarda nokta kullanılamaz, çünkü nokta iç içe geçmeyi ifade eder.6 Metinleri ICU sözdizimiyle yazarsınız:
// messages/tr.json
{
"HomePage": {
"title": "Mağazaya hoş geldiniz",
"cart": "{count, plural, =0 {Sepetiniz boş} =1 {Sepetinizde 1 ürün var} other {Sepetinizde # ürün var}}",
"price": "Fiyat: {price, number, ::currency/TRY}",
"date": "Güncelleme tarihi: {date, date, long}"
}
}const t = useTranslations('HomePage')
t('cart', { count: 2 }) // Sepetinizde 2 ürün var
t('price', { price: 1234.5 }) // Fiyat: ₺1.234,50
t('date', { date }) // Güncelleme tarihi: 16 Ekim 2026 (sunucunun saat dilimine göre)Argüman:
"Merhaba {name}!"vet('message', { name: 'Ayşe' }). Argüman adları harf, rakam ve alt çizgi içerir. Süslü parantezi metin olarak yazmak için tek tırnak kullanın.6Çoğul:
{count, plural, =0 {...} =1 {...} other {...}}.#sayıyı biçimlendirir. Kategorilerzero,one,two,few,many,other. Tam eşleşme (=0) önceliklidir.6 DenemedeIntl.PluralRulesTürkçe, İngilizce ve Almanca için 0, 1 ve 2'ye sırasıylaother,one,otherdöndürdü. Lehçe veya Arapça gibi dillerdefewvemanygibi ek kategoriler devreye girer, bu yüzden çoğul kuralını dil başına çevirmene bırakın.Select:
{gender, select, female {...} male {...} other {...}}.otherdalı zorunlu.6Zengin metin: Mesajın içindeki etiketleri bileşene bağlar:
t.rich('message', { guidelines: (chunks) => <a href="/rules">{chunks}</a> }).t.rawham içeriği döndürür,dangerouslySetInnerHTMLile kullanmadan önce temizleyin.6
Tarih, saat, para birimi
Biçimlendirmeyi mesajın içinde ICU ile ya da kodda useFormatter ile yapabilirsiniz:
import { useFormatter } from 'next-intl'
const format = useFormatter()
const updated = new Date('2026-10-15T21:00:00Z')
format.number(1234.5, { style: 'currency', currency: 'TRY' })
format.dateTime(updated, { dateStyle: 'long', timeZone: 'Europe/Istanbul' })
format.relativeTime(updated, now) // en: "2 hours ago"format.number ve format.dateTime MDN'deki Intl.NumberFormat ve Intl.DateTimeFormat seçeneklerini kabul eder.89 Aynı değer dile göre şöyle yazılır:
| Locale | Para (1234,5) | Tarih (15 Ekim 2026, 21:00 UTC, İstanbul) |
|---|---|---|
tr | ₺1.234,50 | 16 Ekim 2026 |
en | $1,234.50 | October 16, 2026 |
de | 1.234,50 € | 16. Oktober 2026 |
Tabloda para birimi koduna (TRY, USD, EUR) ürünün kendisi karar verir, locale yalnızca gösterimi belirler. Tarihte dikkat edilecek nokta saat dilimi: aynı an, UTC'de 15 Ekim, İstanbul'da 16 Ekim 00:00. Saat dilimini açıkça verin ve tarihleri 2020-11-20T10:36:01.516Z gibi tam ISO 8601 metni olarak saklayın.9
relativeTime değerleri yuvarlar, yani 126 dakika "2 saat önce" olarak görünür. Birimi zorlamak için { now, unit: 'day' } verin. useNow hook'u referans zamanı verir ve updateInterval ile sürekli güncellenebilir.9
Global biçimler tanımlayıp ada göre kullanabilirsiniz: format.dateTime(date, 'short'), format.number(499.9, 'price', { currency: 'USD' }).98
SEO: hreflang
Aynı içeriğin dil sürümlerini arama motoruna hreflang ile bildirirsiniz. Google bunu HTML <link> etiketleri, HTTP Link başlığı veya XML site haritasıyla kabul eder ve üçü eşdeğerdir, birini seçin.10
Karşılıklı olun: Her sürüm kendisini ve diğer tüm sürümleri listelemeli. İki sayfa birbirini göstermezse etiketler yok sayılır.10
Kodlar: ISO 639-1 dil kodu ve isteğe bağlı ISO 3166-1 Alpha 2 bölge kodu (
tr,en-GB). Tek başına ülke kodu yazılamaz.10x-default: Hiçbir dille eşleşmeyen kullanıcılar için yedek sayfa, örneğin dil seçim sayfası.10Tam URL: Alternatif adresler protokolle birlikte yazılmalı.10
Google dili
langveyahreflang'dan tespit etmez,hreflangyalnızca sürümleri gruplar.10
next-intl'de alternateLinks varsayılan olarak açık. Proxy, arama motorlarına link başlığını x-default dahil kendisi ekler.3 Denemede /tr yanıtında şu başlık geldi:
link: <http://localhost:3100/tr>; rel="alternate"; hreflang="tr",
<http://localhost:3100/en>; rel="alternate"; hreflang="en",
<http://localhost:3100/>; rel="alternate"; hreflang="x-default"Kendi bağlantılarınızı eklemek isterseniz alternateLinks: false yapın.3 Sayfa başlığı ve açıklaması için de çeviri kullanın:
import { getTranslations } from 'next-intl/server'
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const t = await getTranslations({ locale, namespace: 'Metadata' })
return { title: t('title') }
}Çeviri iş akışı
Çeviri anahtarlarını elle JSON'a yazmak küçük projede yeter, büyüyünce yönetim sorunu olur. Bir videoda i18nexus adlı bulut platformu kullanılıyor: yeni bir çeviri eklenince platform metinleri OpenAI ile diğer dillere çeviriyor ve CLI çeviri JSON dosyalarını projeye indiriyor.5 Bu ticari bir hizmet, tek seçenek değil. Doküman tolgee, lingui gibi başka araçları da listeliyor.1 Hangisini seçerseniz seçin makine çevirisini bir insanın gözden geçirmesi iyi bir kural.
Next.js dışında: react-i18next
Vite ve düz React projesinde next-intl kullanılamaz. Yaygın bir alternatif react-i18next: useTranslation hook'u t ve i18n döndürür ve initReactI18next ile bağlanır.11
// src/i18n.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
i18n.use(initReactI18next).init({
lng: 'tr',
fallbackLng: 'en',
resources: {
en: { translation: { title: 'Welcome', cart_one: '{{count}} item', cart_other: '{{count}} items' } },
tr: { translation: { title: 'Hoş geldiniz', cart_one: '{{count}} ürün', cart_other: '{{count}} ürün' } },
},
interpolation: { escapeValue: false },
})
export default i18nimport { useTranslation } from 'react-i18next'
const { t } = useTranslation()
t('title') // Hoş geldiniz
t('cart', { count: 2 }) // 2 ürünBurada çoğul sözdizimi ICU değil, i18next'in _one ve _other anahtar son ekleri. Bu kurulum sunucu tarafında renderToString ile denendi. Yönlendirme, URL'de dil ve hreflang işlerini ise bu durumda kendiniz yapmanız gerekir.
Kendin dene
Bir e-ticaret sayfanıza
trveendillerini ekleyin./adresinin tarayıcı diline göre yönlendirdiğini doğrulayın.Sepet metnini
pluralile yazın ve 0, 1, 2 için çıktıyı kontrol edin.Fiyat ve tarihi
useFormatterile gösterin. Dil değiştirdiğinizde ayraçların (nokta, virgül) ve ay adlarının değiştiğini görün.localePrefix: 'as-needed'yapın ve varsayılan dilde önekin kalktığını görün.curl -Iilelinkbaşlığına bakın.Next.js 15: i18n with next-intl videosunu izleyin ve kendi sürümünüzle farkları not edin.5
Kaynaklar
Locale kavramı, alt yol veya alan adıyla yönlendirme, `Accept-Language`, sözlükler, `generateStaticParams` ve kütüphane listesi.
Dile duyarlı biçimlendirme API'leri: `DateTimeFormat`, `NumberFormat`, `PluralRules`, `RelativeTimeFormat`, `ListFormat`.
`locales`, `defaultLocale`, `localePrefix`, `localeDetection`, `localeCookie`, `alternateLinks`, `pathnames` ve `domains`.
`routing.ts`, `proxy.ts`, `navigation.ts`, `request.ts`, `[locale]` segmenti ve Next.js sürüm notları.
`[locale]` segmenti, yapılandırma dosyaları, statik üretim, dil değiştirici ve i18nexus ile yapay zeka destekli çeviri. Ağustos 2025.
JSON yapısı, isim alanları, ICU argüman, plural, select ve rich text.
Kurulum, `createNextIntlPlugin`, `useTranslations` ve `getTranslations`.
`format.number`, para birimi, global biçimler ve ICU sayı biçimleri.
`format.dateTime`, `relativeTime`, `useNow` ve ISO 8601 önerisi.
`hreflang`, karşılıklı bağlantı, `x-default` ve dil kodları.
`useTranslation` ve `initReactI18next` ile React bağlantısı.
Son güncelleme tarihi: 15 Ekim 2026
