ViraStack
  • Academy
  • About
  • Support
HomeSourcesContribute
ViraStack

A DX-first toolkit for frontend developers: starters, agent rules, form hooks, and a guide.

Products

  • ViraStack Start
  • ViraStack AI
  • ViraStack Mask
  • ViraStack Password
  • ViraStack Guide

Explore

  • Academy
  • About
  • Brand
  • Support

© 2026 ViraStack.

Open source · MIT

Mimari ve Ölçeklenebilirlik

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

İşSorunext-intl'de
YönlendirmeHangi dil? /tr/urunler mi /en/products mi?defineRouting, proxy
YerelleştirmeMetin ne?messages/*.json, useTranslations
BiçimlendirmeSayı, 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-intl
messages/
├── 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

Warning

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çenekAnlamıVarsayılan
localePrefix: 'always'Her yolda dil öneki: /en/aboutEvet3
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
localeDetectionAccept-Language başlığı ve çerezden dil algılamaAçık. false yaparsanız dil yalnızca URL'den gelir3
localeCookieEn son algılanan dili NEXT_LOCALE oturum çerezinde saklarAçık3
pathnamesYolları da çevirir: '/about': { de: '/über-uns' }3Yok
domainsHer alan adını kendi dillerine bağlar3Yok

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}!" ve t('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. Kategoriler zero, one, two, few, many, other. Tam eşleşme (=0) önceliklidir.6 Denemede Intl.PluralRules Türkçe, İngilizce ve Almanca için 0, 1 ve 2'ye sırasıyla other, one, other döndürdü. Lehçe veya Arapça gibi dillerde few ve many gibi ek kategoriler devreye girer, bu yüzden çoğul kuralını dil başına çevirmene bırakın.

  • Select: {gender, select, female {...} male {...} other {...}}. other dalı zorunlu.6

  • Zengin metin: Mesajın içindeki etiketleri bileşene bağlar: t.rich('message', { guidelines: (chunks) => <a href="/rules">{chunks}</a> }). t.raw ham içeriği döndürür, dangerouslySetInnerHTML ile 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:

LocalePara (1234,5)Tarih (15 Ekim 2026, 21:00 UTC, İstanbul)
tr₺1.234,5016 Ekim 2026
en$1,234.50October 16, 2026
de1.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.10

  • x-default: Hiçbir dille eşleşmeyen kullanıcılar için yedek sayfa, örneğin dil seçim sayfası.10

  • Tam URL: Alternatif adresler protokolle birlikte yazılmalı.10

  • Google dili lang veya hreflang'dan tespit etmez, hreflang yalnı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 i18n
import { useTranslation } from 'react-i18next'

const { t } = useTranslation()
t('title')              // Hoş geldiniz
t('cart', { count: 2 }) // 2 ürün

Burada ç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

  1. Bir e-ticaret sayfanıza tr ve en dillerini ekleyin. / adresinin tarayıcı diline göre yönlendirdiğini doğrulayın.

  2. Sepet metnini plural ile yazın ve 0, 1, 2 için çıktıyı kontrol edin.

  3. Fiyat ve tarihi useFormatter ile 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.

  4. localePrefix: 'as-needed' yapın ve varsayılan dilde önekin kalktığını görün. curl -I ile link başlığına bakın.

  5. Next.js 15: i18n with next-intl videosunu izleyin ve kendi sürümünüzle farkları not edin.5

Kaynaklar

  • 1DocsNext.js: Internationalization

    Locale kavramı, alt yol veya alan adıyla yönlendirme, `Accept-Language`, sözlükler, `generateStaticParams` ve kütüphane listesi.

  • 2DocsMDN: Intl

    Dile duyarlı biçimlendirme API'leri: `DateTimeFormat`, `NumberFormat`, `PluralRules`, `RelativeTimeFormat`, `ListFormat`.

  • 3Docsnext-intl: Routing configuration

    `locales`, `defaultLocale`, `localePrefix`, `localeDetection`, `localeCookie`, `alternateLinks`, `pathnames` ve `domains`.

  • 4Docsnext-intl: App Router with i18n routing

    `routing.ts`, `proxy.ts`, `navigation.ts`, `request.ts`, `[locale]` segmenti ve Next.js sürüm notları.

  • 5VideoNext.js 15: i18n with next-intl (Full Walkthrough) + AI Translation

    `[locale]` segmenti, yapılandırma dosyaları, statik üretim, dil değiştirici ve i18nexus ile yapay zeka destekli çeviri. Ağustos 2025.

  • 6Docsnext-intl: Messages

    JSON yapısı, isim alanları, ICU argüman, plural, select ve rich text.

  • 7Docsnext-intl: App Router getting started

    Kurulum, `createNextIntlPlugin`, `useTranslations` ve `getTranslations`.

  • 8Docsnext-intl: Numbers

    `format.number`, para birimi, global biçimler ve ICU sayı biçimleri.

  • 9Docsnext-intl: Dates and times

    `format.dateTime`, `relativeTime`, `useNow` ve ISO 8601 önerisi.

  • 10DocsGoogle Search Central: Localized versions of your pages

    `hreflang`, karşılıklı bağlantı, `x-default` ve dil kodları.

  • 11Docsreact-i18next: useTranslation hook

    `useTranslation` ve `initReactI18next` ile React bağlantısı.

Edit this page

Last updated: October 15, 2026

Previous step07. Kod Kalitesi ve Ekip Standartları
Next step09. Çok Dilli Sayfa ve CI HattıProject