İçindekiler
Vue i18n, Composition API ile birlikte önemli ölçüde olgunlaştı. Hâlâ 2020'de yazılmış öğreticilerdeki Options API kalıplarını kullanıyorsanız, özellikle tür güvenliği, lazy loading ve CDN üzerinden sunulan mesajlar konularında çok şeyi masada bırakıyorsunuz demektir.
Bu yazı; bileşen düzeyindeki çevirilerden rota tabanlı dil geçişlerine, Nuxt 3 entegrasyonuna kadar ölçeklenebilir çok dilli Vue 3 uygulamaları oluşturmak için pratik bir rehberdir. Yalnızca demolarda işe yarayan değil, kurumsal ölçekte de geçerliliğini koruyan kalıpları ele alacağız.
Composition API ile vue-i18n Kurulumu
Composition API moduyla başlayın. Options API modu geriye dönük uyumluluk için hâlâ mevcuttur; ancak büyük kod tabanlarını yönetilebilir kılan composable kalıplarını sunmaz.
npm install vue-i18n@9
// src/i18n/index.ts
import { createI18n } from 'vue-i18n'
export type MessageSchema = typeof import('./locales/en.json')
export const i18n = createI18n<[MessageSchema], 'en' | 'fr' | 'de'>({
legacy: false, // Composition API modu
locale: 'en',
fallbackLocale: 'en',
messages: {
en: {} // lazy loading ile yüklenir
}
})
legacy: false ayarı kritik adımdır. Bu ayar useI18n()'i ve tam Composition API yüzeyini kullanıma açar.
useI18n(): Temel Composable
Çeviriye ihtiyaç duyan her bileşen useI18n() kullanır. Temel kalıp şu şekildedir:
// src/components/WelcomeHeader.vue
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<h1>{{ t('welcome.heading') }}</h1>
<p>{{ t('welcome.subheading', { plan: 'Pro' }) }}</p>
</template>
t() fonksiyonu reaktiftir. locale değiştiğinde tüm t() çağrıları otomatik olarak yeniden değerlendirilir. Bu sayede dil geçişi, elle yeniden render gerektirmeden sorunsuz çalışır.
Global ve Yerel Kapsam
Çoğu ekibin ölçeklendirme aşamasında hata yaptığı yer burasıdır. vue-i18n iki kapsamı destekler ve bunları dikkatsizce karıştırmak bakım sorunlarına yol açar.
Global kapsam — i18n örneği düzeyinde tanımlanan, her yerden erişilebilen mesajlar:
// src/i18n/locales/en.json
{
"nav.home": "Home",
"nav.pricing": "Pricing",
"common.save": "Save",
"common.cancel": "Cancel"
}
Yerel kapsam — belirli bir bileşene özgü, bileşenin içinde tanımlanan mesajlar:
// src/components/BillingForm.vue
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n({
messages: {
en: {
title: 'Billing Information',
cardNumber: 'Card number',
expiryDate: 'Expiry date',
errors: {
invalidCard: 'Please check your card number'
}
},
fr: {
title: 'Informations de facturation',
cardNumber: 'Numéro de carte',
expiryDate: "Date d'expiration",
errors: {
invalidCard: 'Veuillez vérifier votre numéro de carte'
}
}
}
})
</script>
Pratik kural: Uygulama genelinde görünen navigasyon, yaygın eylemler ve hata mesajları global kapsama girer. Özelliğe özgü arayüz metinleri ise bileşenle birlikte konumlandırılmış yerel kapsama girer.
Yerel kapsam mesajları global ad alanını kirletmez ve hangi çevirilerin hangi özelliğe ait olduğunu açıkça gösterir. Bir bileşeni sildiğinizde, onunla birlikte çevirilerini de silmiş olursunuz.
TypeScript Tür Güvenliği
Tür güvenliği olmadan i18n bir çalışma zamanı hatası sorununa dönüşür. Bir çeviri anahtarının eksik olduğunu, kullanıcı üretimde boş bir dizi görene kadar fark edemezsiniz.
vue-i18n tam TypeScript entegrasyonunu destekler. Kurulum için bir tür bildirimi gereklidir:
// src/i18n/index.ts
import { createI18n } from 'vue-i18n'
import enMessages from './locales/en.json'
type MessageSchema = typeof enMessages
declare module 'vue-i18n' {
export interface DefineLocaleMessage extends MessageSchema {}
}
export const i18n = createI18n<false, MessageSchema>({
legacy: false,
locale: 'en',
messages: {
en: enMessages
}
})
Artık t() tam olarak türlendirilmiştir. Var olmayan bir anahtar geçirdiğinizde TypeScript bunu derleme zamanında yakalar:
const { t } = useI18n()
t('nav.home') // OK
t('nav.homee') // TypeScript hatası: '"nav.homee"' türündeki bağımsız değişken atanamaz
t('welcome.heading') // OK — şemada mevcutsa
Ölçekte bu, i18n'in bir bakım yükü mü yoksa görünmez mi olacağı arasındaki farktır. IDE'niz anahtarları otomatik tamamlar. Yazım hataları üretimde değil, derleme zamanında başarısız olur.
Locale'leri Lazy Loading ile Yüklemek
Tüm locale'leri başlangıçta yüklemek küçük uygulamalar için uygundur. 20'den fazla dile ve binlerce anahtara sahip kurumsal uygulamalar için ise değildir. Çözüm lazy loading'dir: aktif locale'i talep üzerine yükleyin.
// src/i18n/loader.ts
import { i18n } from './index'
const loadedLocales = new Set<string>(['en']) // en başlangıçta yüklenir
export async function loadLocale(locale: string): Promise<void> {
if (loadedLocales.has(locale)) return
const messages = await import(`./locales/${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
loadedLocales.add(locale)
}
export async function setLocale(locale: string): Promise<void> {
await loadLocale(locale)
i18n.global.locale.value = locale as any
document.documentElement.setAttribute('lang', locale)
}
Bunu bir dil değiştirici bileşende kullanın:
// src/components/LocaleSwitcher.vue
<script setup lang="ts">
import { setLocale } from '@/i18n/loader'
const locales = [
{ code: 'en', label: 'English' },
{ code: 'fr', label: 'Français' },
{ code: 'de', label: 'Deutsch' },
]
async function handleChange(code: string) {
await setLocale(code)
}
</script>
<template>
<select @change="e => handleChange((e.target as HTMLSelectElement).value)">
<option v-for="l in locales" :key="l.code" :value="l.code">
{{ l.label }}
</option>
</select>
</template>
Her locale paketi ayrı bir chunk'tır. Kullanıcılar yalnızca kullandıkları dili indirir.
Rota Tabanlı Dil Geçişi
SEO açısından kritik uygulamalar için locale URL'de yer almalıdır — /en/pricing, /fr/tarifs. Bu yaklaşım her locale'e kendi indekslenebilir URL'sini verir ve arama motorlarının sitenizin dil yapısını anlamasını sağlar.
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { setLocale } from '@/i18n/loader'
const SUPPORTED_LOCALES = ['en', 'fr', 'de']
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/:locale(en|fr|de)',
component: () => import('@/layouts/LocaleLayout.vue'),
children: [
{ path: '', component: () => import('@/pages/Home.vue') },
{ path: 'pricing', component: () => import('@/pages/Pricing.vue') },
{ path: 'features', component: () => import('@/pages/Features.vue') },
]
},
{
path: '/:pathMatch(.*)*',
redirect: to => `/en/${to.params.pathMatch}`
}
]
})
router.beforeEach(async (to) => {
const locale = to.params.locale as string
if (SUPPORTED_LOCALES.includes(locale)) {
await setLocale(locale)
}
})
export default router
Locale düzen bileşeni SEO için <link rel="alternate" hreflang> etiketlerini yönetir:
// src/layouts/LocaleLayout.vue
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { useHead } from '@vueuse/head'
const route = useRoute()
const locale = route.params.locale as string
const LOCALES = ['en', 'fr', 'de']
useHead({
htmlAttrs: { lang: locale },
link: LOCALES.map(l => ({
rel: 'alternate',
hreflang: l,
href: `https://example.com/${l}${route.path.replace(`/${locale}`, '')}`
}))
})
</script>
<template>
<RouterView />
</template>
Çoğullama ve Biçimlendirme
Çoğullama, naif i18n uygulamalarının çöktüğü yerdir. İngilizcede iki çoğul form vardır. Arapçada altı tane. Rusçada ise sayının son hanesine bağlı olarak farklı formlar kullanılır. vue-i18n bunu doğru ele alır — eğer siz de doğru kullanırsanız.
Çoğullama:
// en.json
{
"items": "no items | one item | {count} items"
}
// ru.json — Rusça çoğul kuralları
{
"items": "нет элементов | {count} элемент | {count} элемента | {count} элементов"
}
const { t, n, d } = useI18n()
// Çoğullama
t('items', 0) // "no items"
t('items', 1) // "one item"
t('items', 42) // "42 items"
// Adlandırılmış formatlarla sayı biçimlendirme
n(1234567.89, 'currency') // en'de "$1,234,567.89", fr'de "1 234 567,89 €"
Adlandırılmış formatları i18n örneği düzeyinde tanımlayın:
export const i18n = createI18n({
legacy: false,
locale: 'en',
numberFormats: {
en: {
currency: { style: 'currency', currency: 'USD', notation: 'standard' },
decimal: { style: 'decimal', minimumFractionDigits: 2 }
},
fr: {
currency: { style: 'currency', currency: 'EUR', notation: 'standard' },
decimal: { style: 'decimal', minimumFractionDigits: 2 }
}
},
datetimeFormats: {
en: {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }
},
fr: {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }
}
}
})
Artık n() ve d(), bileşen başına mantık gerekmeksizin her locale için doğru biçimlendirme yapar.
CDN Üzerinden Sunulan Mesajlar ve Paketlenmiş JSON
JSON locale dosyalarını uygulamanıza paketlemenin gerçek bir ödünleşimi vardır: her çeviri güncellemesi yeniden dağıtım gerektirir. Çevirileri sık güncelleyen ekipler için — metinlerde A/B testleri yapan, hataları düzelten, yeni pazarlar için yerelleştiren — bu önemli bir kısıttır.
Alternatif, CDN üzerinden sunulan mesajlardır. Çeviri dosyalarınız bir CDN'de yer alır ve uygulamanız bunları çalışma zamanında getirir. Bu yaklaşım her Vue kurulumunda çalışır — Nuxt 3, Vite SPA veya özel sunucu render.
// src/i18n/cdn-loader.ts
const CDN_BASE = 'https://cdn.example.com/i18n'
export async function loadLocaleFromCDN(locale: string): Promise<Record<string, string>> {
const res = await fetch(`${CDN_BASE}/${locale}.json`, {
headers: { 'Cache-Control': 'max-age=3600' }
})
if (!res.ok) throw new Error(`Failed to load locale: ${locale}`)
return res.json()
}
Ödünleşim, locale yüklenirken yapılan bir ağ isteğidir. Düzgün önbellekleme yapılandırılmış bir CDN ile bu genellikle ilk yüklemeden sonra önbellekten gelir. Avantajı ise çeviri ekibinizin, kod dağıtım sürecine dokunmadan anında metin güncellemesi yapabilmesidir.
Better i18n'de CDN dağıtımı platforma entegre edilmiştir. Çeviriler bir inceleme iş akışından geçer, yayımlanır ve CDN aracılığıyla küresel olarak anında erişilebilir hale gelir — yeniden dağıtım yok, yazım hatası düzeltmek için PR yok. Özellikler sayfası dağıtım sürecinin nasıl işlediğini kapsar; bunu bir Vue projesi için değerlendiriyorsanız geliştiriciler için sayfa SDK entegrasyon örneklerine sahiptir.
Nuxt 3 Entegrasyonu
Nuxt 3, vue-i18n'i sarmalayan ve sunucu tarafı render, rota oluşturma ve SEO yardımcı programları ekleyen @nuxtjs/i18n aracılığıyla birinci sınıf i18n desteğine sahiptir.
npm install @nuxtjs/i18n
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'en', iso: 'en-US', file: 'en.json' },
{ code: 'fr', iso: 'fr-FR', file: 'fr.json' },
{ code: 'de', iso: 'de-DE', file: 'de.json' }
],
defaultLocale: 'en',
langDir: 'i18n/locales/',
lazy: true,
strategy: 'prefix_except_default',
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root'
}
}
})
strategy: 'prefix_except_default' ile İngilizce rotaların öneki yoktur (/pricing), diğer dillerin ise vardır (/fr/tarifs). Nuxt rota oluşturmayı otomatik olarak yönetir.
Bileşenlerde useI18n(), Nuxt olmayan bir uygulamayla aynı şekilde çalışır:
// pages/pricing.vue
<script setup lang="ts">
const { t } = useI18n()
useSeoMeta({
title: t('pricing.meta.title'),
description: t('pricing.meta.description')
})
</script>
SSR entegrasyonu, çevrilen içeriğin ilk HTML'de yer aldığı anlamına gelir — çevrilmemiş içeriğin anlık görünümü olmaz ve arama motorları yerelleştirilmiş sürümü görür.
Geliştirici İş Akışını Ölçeklendirmek
Çeviri yüzeyiniz büyüdükçe darboğaz teknik kurulumdan iş akışına kayar. Ölçekte geçerliliğini koruyan birkaç kalıp:
Çeviri anahtarlarını konumsal değil, anlamsal tutun. nav.pricing, link3'ten daha uzun ömürlüdür. Navigasyon yeniden tasarlandığında link3 anlamsız hale gelir. nav.pricing hâlâ doğrudur.
Eksik çevirileri derleme hatası olarak ele alın. Bir anahtarın varsayılan dilde mevcut olup hedef dilde eksik olduğu durumda uyarı vermek veya başarısız olmak için derlemenizi yapılandırın. Kullanıcılar görmeden yakalayın.
Çeviri çıkarımını otomatikleştirin. vue-i18n-extract veya benzer araçları CI sürecinin bir parçası olarak çalıştırmak, her yeni dizenin yayınlanmadan önce çeviri için işaretlenmesini sağlar.
Çeviri incelemesini kod incelemesinden ayırın. Çeviri kalitesi, kod kalitesinden farklı bir beceridir. Çevirmenlerin bir yazım hatasını düzeltmek için PR açması gerekiyorsa, gereksiz bir sürtünme yaratmışsınız demektir. Dil incelemesinin Git'te değil, dil içinde gerçekleştiği bir çeviri platformu kullanın.
Bu yazıda açıklanan teknik temel mühendislik tarafını ele alır. İş akışı tarafı — çevirilerin incelenmesi, çevirmenlerin erişiminin yönetilmesi, diller arasında terim bilgisinin tutarlı tutulması — özel araçların orantısız biçimde fayda sağladığı alandır.