Yazılar
22 dilli bir Next.js platformu geliştirmek: GAGA'dan çıkarılan dersler
Locale yönlendirmesi, Latin ve Kiril Sırpçası, Intl biçimlendirmesi, yazı tipleri, çeviri CI'ı ve güncel döviz kurları: 22 dilli bir Next.js projesinden dersler.
sigmacode.io mühendislik ekibi10 dk okuma
Bu sayfada (11)
- Her locale'e kendi URL'sini verin
- İki alfabe, tek dil
- Sayıları, para birimlerini ve tarihleri Intl ile biçimlendirin
- Yazı tipleri ve glif kapsamı
- 22 dile dayanan bir çeviri iş akışı
- Bayat değerler olmadan gerçek zamanlı veri
- PDF, Excel, CSV ve XML için tek doğruluk kaynağı
- Çok sayıda locale'de SEO
- Erişilebilirlik
- Test: rotalar × locale'ler
- Neyi planlamalı
GAGA Menjačnica, Sırbistan'ın Novi Sad kentinde birkaç şubesi olan bir döviz bürosu ve kıymetli maden satıcısıdır. Ekibimiz platformlarını menjacnicegaga.rs adresinde, Vercel üzerinde Next.js ve React ile geliştirdi. Platform; Latin ve Kiril alfabesiyle Sırpça dahil 22 dilde — Almanca, Çince, Rusça, Türkçe, Ukraynaca, Yunanca ve Bulgarca — çalışıyor. National Bank of Serbia referans kurlarının yanında canlı alış ve satış kurlarını gösteriyor ve bir döviz çevirici, beş formatta indirilebilir kur listeleri, şube bulucu ile yatırım altını ve gümüşü hakkında kılavuzlar içeriyor.
Yirmi iki dil, uluslararasılaştırmanın bir özellik olmaktan çıktığı noktanın ötesidir. Bu ölçekte mimariyi şekillendirir. Bu makale, böyle bir platform geliştirirken önemli olduğunu düşündüğümüz konuları ele alıyor. Bir tane planlayan CTO'lar, ürün sahipleri ve frontend mühendisleri için yazıldı. Projenin kendisi için GAGA vaka çalışmasına bakın.
Her locale'e kendi URL'sini verin#
En önemli karar en başta gelir: her sayfanın her dil sürümü kendi kararlı ve taranabilir URL'sine ihtiyaç duyar. Dili bir cookie ile veya Accept-Language koklayarak değiştirmeyin. Arama motorları bunu indeksleyemez, kullanıcılar paylaşamaz ve CDN'ler düzgün önbelleğe alamaz.
Yolda bir locale öneki öneriyoruz: /sr/..., /de/... ve /zh/... gibi. Next.js App Router ile bu, uygulamanın kökünde bir [locale] segmenti demektir. İstek proxy'si (proxy.ts, eski adıyla middleware) ilk ziyareti tarayıcının diline göre mantıklı bir varsayılana yönlendirebilir. Bundan sonra URL tek doğruluk kaynağıdır ve bir dil değiştirici, diğer locale'in ana sayfasına değil, aynı sayfanın diğer locale'deki sürümüne bağlanmalıdır.
Locale tanımlayıcılarınızı erken planlayın ve her yerde BCP 47 etiketleri kullanın. Yalnızca Sırpça iki tane gerektirir: sr-Latn ve sr-Cyrl. Bunları URL segmentlerine, hreflang değerlerine ve Intl locale'lerine tek bir yerde eşlemek, ileride çok dertten kurtarır.
hreflang, x-default ve sitemap'ler#
Her sayfa, kendisi dahil tüm alternatiflerini listelemeli ve desteklemediğiniz dillerdeki kullanıcılar için bir x-default eklemelidir. App Router'da metadata API bunu sizin için yapar:
// app/[locale]/rates/page.tsx
import type { Metadata } from "next";
const BASE = "https://example.com";
const LOCALES = ["sr-Latn", "sr-Cyrl", "en", "de", "zh", "ru"] as const;
const segment = (l: string) => l.toLowerCase(); // "sr-Latn" -> "sr-latn"
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
const { locale } = await params;
const path = "/rates";
const languages: Record<string, string> = Object.fromEntries(
LOCALES.map((l) => [l, `${BASE}/${segment(l)}${path}`])
);
languages["x-default"] = `${BASE}/en${path}`;
return {
alternates: { canonical: `${BASE}/${locale}${path}`, languages },
};
}
Sitemap'inizi aynı locale listesinden ve aynı rota kaydından, her girdi için alternatiflerle birlikte oluşturun. hreflang etiketleri ve sitemap girdileri ayrı kod yollarından gelmeye başladığında er ya da geç çelişirler ve arama motorları sessizce ikisine de güvenmeyi bırakır.
İki alfabe, tek dil#
Sırpça hem Latin hem Kiril alfabesiyle yazılır ve birçok okurun güçlü bir tercihi vardır. Bunları görüntü değiştiricili tek bir locale olarak değil, iki tam locale olarak ele alın. Her biri kendi URL'sini, kendi hreflang değerini ve kendi lang özniteliğini alır.
Her şeyi tek alfabede yazıp diğerini otomatik olarak çevriyazmak cazip görünür. Düz metin için iyi test edilmiş bir transliterasyon adımı makul bir başlangıç noktası olabilir. Ancak bunu her şeye körü körüne uygulamayın:
- Özel isimler ve markalar. Şirket adları, ürün adları ve yabancı kelimeler Kiril metin içinde bile genellikle Latin biçimini korur. Mekanik bir dönüştürücü "yardımseverlikle" onları kimsenin yazmadığı bir şeye çevirir.
- İkiz harf belirsizliği. Latin
nj,ljvedžgenellikle tek Kiril harfine karşılık gelir, ancak her zaman değil. Bileşik kelimeler ve yabancı alıntılar kuralı bozar. Kiril'den Latin'e geçiş deterministiktir. Latin'den Kiril'e geçiş değildir. - URL'ler, kodlar ve tanımlayıcılar.
EURgibi para birimi kodları, e-posta adresleri, slug'lar ve interpolasyon yer tutucularının içindeki her şey asla çevriyazılmamalıdır. - Arama ve sıralama. Kullanıcılar Kiril bir sayfadaki arama kutusuna Latin yazabilir. Karşılaştırmadan önce iki tarafı da normalleştirin.
Önerimiz şu: Kiril kaynağı saklayın (deterministik yön) veya iki ayrı katalog tutun ve anadili konuşan birinin sahiplendiği küçük bir istisna listesi bulundurun. Makinenin ürettiği her şey yayınlanmadan önce gözden geçirilmelidir.
Sayıları, para birimlerini ve tarihleri Intl ile biçimlendirin#
Bir döviz kuru sitesinde sayılar ürünün kendisidir. Sırpça ondalık ayırıcı olarak virgül kullanır, Almanca binlikleri nokta ile gruplar ve Çince kullanıcılar yine farklı kurallar bekler. Elle biçimlendirmeyin. Yerleşik Intl API'lerini kullanın ve tam locale etiketini geçirin:
const rateFormatter = (locale: string) =>
new Intl.NumberFormat(locale, {
minimumFractionDigits: 4,
maximumFractionDigits: 4,
});
const moneyFormatter = (locale: string, currency: string) =>
new Intl.NumberFormat(locale, { style: "currency", currency });
const asOf = (locale: string, date: Date) =>
new Intl.DateTimeFormat(locale, {
dateStyle: "long",
timeStyle: "short",
timeZone: "Europe/Belgrade",
}).format(date);
rateFormatter("sr-Latn").format(117.1234); // "117,1234"
rateFormatter("de").format(117.1234); // "117,1234"
moneyFormatter("en", "EUR").format(1250); // "€1,250.00"
asOf("sr-Cyrl", new Date()); // e.g. "18. септембар 2026. 10:30" (exact output depends on the ICU version)
Planlanması gereken birkaç şey var. Saat dilimini açıkça ayarlayın; aksi hâlde sunucu tarafında render edilen sayfalar genellikle UTC olan sunucunun dilimini kullanır. Formatter'ları bir kez oluşturup yeniden kullanın, çünkü büyük bir tabloda tekrar tekrar oluşturmak birikir. Ve ondalık basamak sayısını hem veri katmanında hem UI'da tutun ki export'lar ve ekran birbiriyle uyuşsun.
Yazı tipleri ve glif kapsamı#
Yirmi iki dil; Latin, Latin Extended (Sırpça, Hırvatça, Türkçe), Kiril (Sırpça, Rusça, Ukraynaca, Bulgarca), Yunanca ve CJK karakterleri demektir. Marka yazı tiplerinin çok azı bunların hepsini kapsar ve kapsayanlar da büyüktür.
Önemli olanlar:
- Bir yazı tipi seçmeden önce alfabe başına kapsamı kontrol edin. "Lorem ipsum" değil, gerçek dizgileri test edin. Sırpça Kiril'in kendi harfleri ve yerel italik biçimleri vardır, Ukraynaca'nın Rusça'da olmayan harfleri vardır ve Bulgarca kendi glif şekillerini tercih eder.
- Alfabeye göre alt kümeleyin ve locale'e göre yükleyin.
next/font,latin,latin-ext,cyrillicvegreekgibi alt kümeleri destekler. Bir Almanca sayfa Kiril glifleri indirmemelidir. - Her sayfada tam bir CJK yazı tipini self-host etmeyin. Çince yazı tipleri birkaç megabayt olabilir. CJK için
"PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serifgibi bir sistem yazı tipi yığını çoğu zaman doğru ödünleşimdir. - Açık bir fallback yığını tasarlayın ve metrik uyumlu fallback'ler ayarlayın ki web fontu yüklendiğinde metin kaymasın.
22 dile dayanan bir çeviri iş akışı#
İki veya üç dilde bir elektronik tablo ve biraz disiplin yeterlidir. 22'de bir pipeline'a ihtiyacınız var.
Kararlı anahtarlı mesaj katalogları. Locale başına bir JSON dosyası kullanın; anahtarları rates.table.buy gibi anlama göre adlandırın, asla İngilizce metne göre değil. Çoğullar ve interpolasyon için ICU MessageFormat kullanın. Çoğul kuralları çok farklıdır: Rusça, Ukraynaca ve Sırpça'nın birkaç biçimi vardır, Çince'nin ise hiç yoktur.
CI'da anahtar eşitliği. Bir locale'de eksik bir anahtar, çok dilli bir sitenin en yaygın hatasıdır ve otomatik olarak yakalanması en kolay olandır:
// scripts/check-i18n.ts — run in CI, fail the build on drift
import { readdirSync, readFileSync } from "node:fs";
const dir = "messages";
const flatten = (o: Record<string, unknown>, p = ""): string[] =>
Object.entries(o).flatMap(([k, v]) =>
v && typeof v === "object" ? flatten(v as Record<string, unknown>, `${p}${k}.`) : [`${p}${k}`]
);
const load = (f: string) => new Set(flatten(JSON.parse(readFileSync(`${dir}/${f}`, "utf8"))));
const source = load("en.json");
let failed = false;
for (const file of readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "en.json")) {
const keys = load(file);
const missing = [...source].filter((k) => !keys.has(k));
const extra = [...keys].filter((k) => !source.has(k));
if (missing.length || extra.length) {
failed = true;
console.error(`${file}: missing ${missing.length}, extra ${extra.length}`, { missing, extra });
}
}
process.exit(failed ? 1 : 0);
Aynı script'i, interpolasyon yer tutucularının locale'ler arasında eşleştiğini kontrol edecek şekilde genişletin. Çevrilmiş bir yer tutucu adı derleme zamanında değil, çalışma zamanında patlar.
İnsan incelemeli, yapay zekâ destekli çeviri. Makine ve LLM çevirisi artık iyi ilk taslaklar üretiyor ve 22 dilde bu ekonomiyi değiştiriyor. Ancak bir döviz bürosu para ve güvenle uğraşır. Kur etiketleri, yasal uyarılar ve yatırım kılavuzları yayınlanmadan önce akıcı konuşan bir insan tarafından gözden geçirilmelidir. Çevirmenlere bağlam verin: ekran görüntüleri, karakter sınırları ve "buy rate", "sell rate" ve "reference rate" gibi sabit terimlerin sözlüğü.
Bayat değerler olmadan gerçek zamanlı veri#
Döviz kurları gün içinde değişir. Tuzak, onları static-first bir Next.js sitesinin geri kalanı kadar agresif önbelleğe almaktır.
Önerdiğimiz genel stratejiler:
- Kabuğu veriden ayırın. Sayfa düzeni, çeviriler ve kılavuzlar statik olabilir veya nadiren revalidate edilebilir. Kur tablosunun kendi kısa revalidation penceresi olmalı ya da istemcide çekilmeli veya stream edilmelidir.
- Revalidation'ı kısa ve bilinçli tutun. Kurların gerçekte ne sıklıkla değiştiğine uyan bir pencere seçin ve bunu belgeleyin. Mümkünse yalnızca bir zamanlayıcıya güvenmek yerine, yeni kurlar yayınlandığında on-demand revalidation kullanın.
- Edge'de dikkatli önbelleğe alın.
stale-while-revalidateile kısas-maxagesayfaları hızlı tutar, ancak bayat pencerenin işletmenin kabul edebileceği bir süre olduğundan emin olun. - Her zaman bir "itibarıyla" zaman damgası gösterin, şubenin saat diliminde locale'e göre biçimlendirilmiş olarak. Bu, her önbellekleme sisteminin doğurduğu sorunun — "bu ne kadar taze?" — dürüst yanıtıdır.
PDF, Excel, CSV ve XML için tek doğruluk kaynağı#
GAGA, kur listesini PDF, JPG, Excel, CSV ve XML olarak indirilebilir şekilde yayınlar. Beş format, sayıların birbirinden ayrışması için beş fırsat demektir.
Kural şu: her export'u, ekrandaki tabloyu da render eden aynı normalleştirilmiş veri yapısından üretin. Yuvarlama, sıralama ve para birimi metadata'sını bu yapıya koyun, her bir exportçuya değil. O zaman her format ince bir serializer'a dönüşür. Planlanacak şeyler:
- CSV belirtilmiş bir ayırıcı ve kodlama ister. Birçok Avrupa locale'indeki Excel noktalı virgül bekler ve UTF-8'i BOM ile daha iyi işler.
- Excel önceden biçimlendirilmiş dizgiler değil, sayı formatlarına sahip gerçek sayısal hücreler almalı ki kullanıcılar bunlarla hesap yapabilsin.
- XML kararlı ve belgelenmiş bir şema ister, çünkü biri ona karşı entegrasyon yapacaktır.
- PDF ve görseller istenen alfabeyi kapsayan yazı tiplerini gömmek zorundadır; bu da sizi glif kapsamına geri götürür.
"İtibarıyla" zaman damgasını her export'a da koyun.
Çok sayıda locale'de SEO#
hreflang ve sitemap'lerin ötesinde:
- Başlıkları, açıklamaları ve Open Graph metadata'sını locale başına çevirin. Bir Yunanca sayfada İngilizce metadata bırakmayın.
- Slug'ları yalnızca kararlı tutabiliyorsanız yerelleştirin. Bir locale'de değişen bir slug, yönlendirmeler ve bozuk bir hreflang kümesi demektir.
- İnce kopyalardan kaçının. Bir locale'de yalnızca kısmi içerik varsa, henüz indekslenmesi gerekip gerekmediğini düşünün.
- Canonical URL'ler sayfanın kendisine işaret etmeli, asla başka bir dil sürümüne değil.
Erişilebilirlik#
Ekran okuyucuların doğru sesi ve telaffuzu seçmesi için html elementinde her locale için tam etiketi (sr-Latn, sr-Cyrl) kullanarak lang ayarlayın. Bir sayfa içinde başka bir dilde bir ifade geçtiğinde, onu kendi lang özniteliğiyle işaretleyin.
Bugün sağdan sola bir dili desteklemeseniz bile planlayın. margin-left yerine margin-inline-start gibi CSS mantıksal özelliklerini kullanın ve dir'i locale'den türetin. Düzen soldan sağa varsaymadığında, ileride Arapça eklemek çok daha ucuzdur.
Test: rotalar × locale'ler#
22 locale ile yalnızca birinde görünen bir hatayı kaçırmak kolaydır. Herkese açık her rotayı ve her locale'i dolaşan ve temel noktaları kontrol eden otomatik bir smoke test öneriyoruz:
- sayfa 200 döndürüyor ve çalışma zamanı hatası olmadan render oluyor;
htmldoğrulangözniteliğine sahip;- hreflang alternatifleri eksiksiz ve var olan URL'lere işaret ediyor;
rates.table.buygibi ham mesaj anahtarları veya boş dizgiler görünmüyor;- çevirici ve kur tablosu sayıları beklenen formatta render ediyor.
En uzun diller için görsel snapshot'lar ekleyin. Almanca ve Yunanca etiketler, İngilizce'de sorunsuz görünen düzenleri sık sık bozar.
Neyi planlamalı#
Çok dilli bir Next.js platformuna başlıyorsanız, ilk gün verilmesi gereken kararlar bunlar: locale önekli bir URL stratejisi, yönlendirme, metadata ve sitemap'leri besleyen tek bir locale kaydı, alfabeler ve transliterasyon için net bir politika, tüm biçimlendirme için Intl, alfabe başına bir yazı tipi planı, çeviri eşitliği için CI kontrolleri, canlı veri için açık bir tazelik politikası ve her export için tek bir doğruluk kaynağı.
Bunların hiçbiri egzotik değil, ancak baştan tasarlamak sonradan eklemeye çalışmaktan çok daha ucuzdur. Çalışmalarımız 20+ yıllık deneyime sahip bir tech lead tarafından yönetiliyor ve Web & Platforms kapsamında tam da bu tür bir temel işine odaklanıyoruz. Benzer bir şey planlıyorsanız iletişime geçin.