Bu siteye İngilizce desteği eklerken işin çeviri kısmının en kolay kısım olduğunu öğrendim. Zor kısım üç karardı: URL'ler nasıl görünecek, dili kim seçecek ve veritabanındaki içerik ne olacak. Bir de kimsenin başına gelmesin dediğim bir hata var — sona sakladım.
once url karari
İki dilli sitede ilk karar çeviri kütüphanesi değil, URL tasarımı. Benim kriterlerim netti: mevcut Türkçe URL'ler (/blog, /hakkimda) tek karakter bile değişmeyecek — Google indeksi ve paylaşılmış linkler bozulmayacak. İngilizce de gizli bir çeviri katmanı değil, kendi adresi olan gerçek sayfalar olacak ki ayrıca indekslensin.
next-intl'in localePrefix: "as-needed" modu tam bunu yapıyor: varsayılan dil (tr) prefix'siz kökte kalıyor, İngilizce /en/... altına geliyor. Her sayfaya karşılıklı hreflang etiketleri eklenince Google iki sürümü akraba ama ayrı sayfalar olarak görüyor.
dil tespiti hazir geldigi gibi kullanilmaz
İstediğim davranış tek cümleydi: tarayıcısı Türkçe olan Türkçe görsün, diğer herkes İngilizce. next-intl'in hazır dil müzakeresi tam burada tökezliyor: Accept-Language başlığını yalnızca tanımlı locale'lerle (tr, en) eşleştiriyor; Almanca bir tarayıcı ikisiyle de eşleşmeyince sessizce defaultLocale'e, yani Türkçe'ye düşüyor. Berlin'den gelen ziyaretçiye Türkçe sayfa göstermek, "diğer herkes İngilizce" kuralının tam tersi.
Çözüm, hazır tespiti kapatıp (localeDetection: false) middleware'de üç satırlık kendi mantığımı yazmaktı:
Sıralama önemli: URL her zaman kazanır (link paylaşımı tutarlı kalsın), sonra kullanıcının bilinçli seçimini tutan cookie, en son tarayıcı dili. Nav'daki tr | en düğmesi de cookie'yi yazıp tam sayfa navigasyonu yapıyor — client router ile geçiş yapınca cookie yazımıyla istek arasında yarış oluşuyor ve middleware sizi eski dile geri postalıyor. Bunu da deneyerek öğrendim.
icerik cevirisi ui cevirisi degildir
Buton ve menü metinleri messages/tr.json ve en.json'da yaşıyor; klasik iş. Ama bu sitede metnin çoğu veritabanından geliyor: profil, hizmetler, projeler, yazılar. Onlar için JSON dosyası değil, şema gerekiyor: her çevrilebilir kolonun yanına bir *En kolonu ekledim ve API'ye ?lang=en parametresi tanımladım.
Kritik detay alan bazlı fallback: lang=en istendiğinde her alan için önce İngilizcesine bakılıyor, boşsa Türkçesi dönüyor. Böylece çeviriyi parça parça doldurabiliyorum; eksik alan sayfayı bozmuyor, sadece o alan Türkçe görünüyor. Yazı DTO'suna eklediğim contentLanguage alanı da İngilizce görünümde Türkçe kalan yazının başına dürüst bir not düşürüyor: "this post is available in Turkish only."
ic ice html tuzagi
Ve söz verdiğim hata. Kurulum bitti, her şey çalışıyor ama konsolda her tam sayfa yüklemede hydration uyarısı: React sunucudan gelen HTML ile uyuşamayıp tüm sayfayı istemcide yeniden render ediyor. Sayfa çalıştığı için görmezden gelmesi kolay — performansı sessizce yediği için de tam bir tuzak.
Sebep: dosya yapısında hem app/layout.tsx (eski kök) hem app/[locale]/layout.tsx vardı ve ikisi de <html> render ediyordu. Next.js'te layout'lar koşulsuz iç içe geçer — "üstteki sadece fallback" diye bir şey yoktur. Ortaya <html> içinde <html> çıkıyor; tarayıcı bunu parse ederken sessizce düzeltiyor, React ise düzeltilmiş DOM ile kendi beklentisini karşılaştırıp şaşırıyor.
Çözüm, kök app/layout.tsx'i tamamen silmek: app/[locale]/layout.tsx tek kök layout olur. Kök 404'ün ihtiyaç duyduğu yakalamayı da locale ağacının içine taşıyan küçük bir catch-all yeterli:
// app/[locale]/[...rest]/page.tsx
import { notFound } from "next/navigation";
// bilinmeyen her url, çevrilmiş 404'e düşer
export default function CatchAll(): never {
notFound();
}
ozet
İki dilli siteye giderken sırayla: URL tasarımına karar verin (varsayılan dil prefix'siz kalsın), dil tespitini kendi kuralınızla yazın (hazır müzakere "bilinmeyen dil → varsayılan" der, siz istemeyebilirsiniz), veritabanı içeriği için alan bazlı fallback kurun ve app/[locale] kullanıyorsanız üstte ikinci bir kök layout bırakmayın. Bu maddelerin her biri bu sitede test edilmiştir — istemeden de olsa.