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.