Tüm yazılar
07 · Arayüz · 2026.10 · 6 dk

Dil adreste yaşar: Türkçe kökte, İngilizce /en altında

Siteyi iki dilli yaparken dili localStorage'a ya da tarayıcı ayarına bırakmadım, adrese yazdım. Türkçe kökte, İngilizce /en altında duruyor; router dili URL'den okuyor, LocLink de bağlantıları okuduğun dilde tutuyor. Bu notta kurgunun nasıl çalıştığını anlatıyorum.

01Neden adres?

İlk sürümde dil tipli küçük bir depoda tutuluyordu ve tercih localStorage'a dvx-lang anahtarıyla yazılıyordu. Ziyaretçi için çalışıyordu ama üç yerde tıkandı. Birincisi arama motorları: tek bir adres iki dilde içerik veriyorsa hangisinin indeksleneceği belirsiz kalıyor. İkincisi paylaşım: İngilizce okuyan birine attığım bağlantı, onun tarayıcısında Türkçe açılabiliyordu. Üçüncüsü prerender: build her rotayı statik HTML'e çeviriyor ve sunucuda localStorage diye bir şey yok. Sunucunun hangi dilde render edeceğini bilmesi için dilin isteğin kendisinde olması gerekiyordu.

Tarayıcı dilini okuyup yönlendirmeyi de düşündüm, ama bu aynı sorunu başka bir kapıdan geri getiriyor: aynı URL farklı insanlara farklı içerik gösteriyor. Ben her sayfanın her dilde kendi adresi olsun istedim. Türkçe varsayılan olduğu için kökte kaldı (/work), İngilizce /en önekini aldı (/en/work).

02Rotaları kopyalamak

Router tarafında iki ayrı rota listesi yazmadım. Türkçe rotalar tek kaynak, İngilizce olanlar bunların map ile üretilmiş kopyası. Yolun başına /en ekleniyor, isim en- önekini alıyor, meta alanına lang: 'en' yazılıyor. Ana sayfa /en oluyor, 404 için kullanılan catch-all rota da /en/:pathMatch(.*)* oluyor.

src/app/router.ts
const english = base.map((r) => ({
  ...r,
  path: r.path === '/' ? '/en' : r.path === '/:pathMatch(.*)*' ? '/en/:pathMatch(.*)*' : '/en' + r.path,
  name: 'en-' + String(r.name),
  meta: { ...r.meta, lang: 'en' as const }
})) as RouteRecordRaw[]

export const routes: RouteRecordRaw[] = [...english.slice(0, -1), ...base.slice(0, -1), english[english.length - 1], base[base.length - 1]]

// makeRouter(server): runs on the server render and in the browser
router.beforeEach((to) => {
  setLocale(localeFromPath(to.path))
  return true
})

Sıralama burada önemli. İki catch-all rota listenin sonuna gidiyor ve İngilizce olan önce geliyor; aksi halde /en altındaki bilinmeyen bir adres Türkçe 404'e düşerdi. Olmayan proje ve yazı slug'ları için beforeEnter içinde notFound adında bir yardımcı var: ziyaretçinin yazdığı adresi koruyup doğru dildeki 404 görünümünü render ediyor.

03Dili URL belirler

Asıl karar tek bir beforeEach içinde. Router her geçişte to.path değerine bakıyor, localeFromPath ile dili çıkarıyor ve setLocale çağırıyor. Bu kanca bilerek sunucu kontrolünden önce kuruluyor, yani hem build sırasındaki sunucu render'ında hem tarayıcıda çalışıyor. Sunucu memory history, tarayıcı web history kullanıyor; dil kararı ikisinde de aynı satırdan geliyor.

Bunun hydration için de anlamı var. Dil deposu her zaman Türkçe başlıyor, ama ilk render'dan önce router devreye girip adresteki dili yazıyor. main.ts içinde mount işlemi router.isReady() sonrasına bağlı. Böylece /en/work hem sunucuda hem istemcide İngilizce render ediliyor ve işaretleme birebir uyuşuyor. main.ts'teki yorum da bunu söylüyor: dil için mount sonrasında yapılacak bir şey kalmadı.

04Yol yardımcıları

Bütün dönüşümler src/i18n/paths.ts içindeki üç küçük fonksiyonda. localeFromPath, yol tam olarak /en ise ya da /en/ ile başlıyorsa en, değilse tr döndürüyor. basePath öneki kesip dil-nötr yolu veriyor, localize ise bu yolu hedef dile çeviriyor.

src/i18n/paths.ts
export const localeFromPath = (path: string): Locale => (path === '/en' || path.startsWith('/en/') ? 'en' : 'tr')

/** '/en/work' → '/work', '/en' → '/' */
export const basePath = (path: string) => (localeFromPath(path) === 'en' ? path.slice(3) || '/' : path)

/** '/work' + en → '/en/work', '/' + en → '/en' */
export const localize = (path: string, locale: Locale) => {
  const base = basePath(path)
  return locale === 'en' ? (base === '/' ? '/en' : '/en' + base) : base
}

Kontrolün yalnızca tam eşleşmeye ve /en/ önekine bakması küçük ama bilinçli bir ayrıntı. Düz bir startsWith('/en') yazsaydım, ileride /entries gibi bir sayfa eklediğimde o sayfa sessizce İngilizce sayılırdı.

05LocLink ve dil düğmesi

Bileşenlerin her yerinde RouterLink ile /work yazmak, İngilizce sayfadaki okuru her tıklamada Türkçeye geri atardı. Bunun yerine LocLink adında küçük bir bileşen yazdım ve createSiteApp içinde global olarak kaydettim. Aynı to değerini alıyor: / ile başlayan bir string ise localize'dan geçiriyor, path alanı olan bir nesne ise yalnızca o alanı çeviriyor, sadece hash ya da query içeren hedefleri olduğu gibi bırakıyor. Sonra sıradan bir RouterLink render ediyor. Header, footer, menü ve kartlar hep bunu kullanıyor; bağlantıları izleyen bir tarayıcı hiçbir noktada dil değiştirmiyor.

Dil düğmesi aynı fikrin tersini yapıyor. SiteHeader'daki TR ve EN düğmeleri bir değişkeni değiştirmiyor; mevcut sayfanın diğer dildeki adresine router.push ile gidiyor, query ve hash'i de yanında taşıyarak. Hangi dilin etkin olduğunu aria-pressed söylüyor. Dil değişince <html lang> değeri App.vue'deki useHead üzerinden güncelleniyor, her sayfa da useSeo ile tr, en ve x-default için hreflang bağlantılarını yazıyor.

Dil bir tercih olmaktan çıkıp adresin parçası olunca sunucu, tarayıcı ve arama motoru aynı şeyi görüyor.

06Ne kazandım, neyi bıraktım

Prerender script'i artık her yolu iki kez render ediyor: Türkçesi dist/<rota>.html, İngilizcesi dist/en/<rota>.html olarak. Sitemap iki dili birlikte listeliyor. Bir bağlantıyı paylaştığımda karşı taraf benim gördüğüm dili görüyor ve bunun için hiçbir şeyin hatırlanmasına gerek yok.

Bıraktığım şey otomatik dil tahmini. İngilizce bir tarayıcıyla gelen biri ilk ziyarette Türkçe kökü görüyor ve EN düğmesine basması gerekiyor. setLocale tercihi hâlâ localStorage'a yazıyor, ama dili artık o değer değil adres belirliyor. Bunu bilerek kabul ettim; bir portfolyo için öngörülebilir adresler, ilk ziyarette kazanılacak bir tıktan daha değerli geldi.

İlgili yazılar
  1. 08XSLT ile süslenen sitemap.xml'den gerçek bir HTML site haritasınaArama ve GEO2026.105 dk
  2. 09İmleci kovalamayı bıraktım: CSS cursor ile yerel SVG imleçTasarım ve etkileşim2026.106 dk