

1/1/1970
3 Günde 20 Dile i18n Nasıl Ekledim
Selam! Az önce Foony’yi 20 farklı dile çevirdiğim devasa bir işi bitirdim. Kod tabanındaki neredeyse her dosyaya dokunmam gerekti, ama her şeyi sadece 3 günde hallettim.
Aşağıda bunu nasıl yaptığımı, değişikliğin arkasındaki net rakamları ve neden sektör standardını kullanmak yerine (yine) kendi çeviri kütüphanemi yazmayı seçtiğimi anlatacağım.
Neden i18next Değil?
Çeviri eklemeyi ilk düşündüğümde, sektör standardına baktım: i18next ve react-i18next.
Bunun yerine, AI ile bakımı kolay olacak şekilde optimize etmeye karar verdim. i18next güçlü, ama API çeşitliliği LLM’lerin halüsinasyon görmesine ya da tutarsız kod yazmasına yol açabiliyor. Kütüphaneyi basit bir t() ve interpolate() ile kısıtlayarak, 10’dan fazla paralel agent’in neredeyse sıfır insan müdahalesiyle %100 tip güvenli kod yazabilmesini sağladım.
Ayrıca, ileride kırıcı değişiklikler getirebilecek büyük bir ekosisteme bağlanma konusunda da temkinliydim. React Router v5 ve MUI v4 → v5 gibi can yakan göçlerden daha önce de yanmış biri olarak, JavaScript dünyasında geriye dönük uyumluluğun hızla bozulmasının ne kadar yaygın olduğunu biliyorum. Çoğulluk (pluralization) gibi özellikleri daha sonra eklemenin maliyeti, şu an 139k satır kodu elle taşımaktan daha düşük.
İstediğim şey aşırı basit, inanılmaz hafif ve tam olarak ekibimin ihtiyaçlarına göre biçilmiş bir şeydi.
O yüzden kendi kütüphanemi yazdım.
Yüksek doğrulukta, otonom AI refaktoring için özel olarak tasarlanmış, 3 KB’lık kısıtlı bir alt küme inşa ettim. Bu sayede tek başıma çalışan bir mühendis olarak, normalde 5 kişilik bir ekibin 3 haftada yapacağı işi 3 günde çıkarabildim.
Özel Uygulama
Gzip’lenmiş hâli yaklaşık 3 KB olan minimal bir i18n kütüphanesi tasarladım. İki ana fonksiyon sunuyor: React dışı bağlamlar için getTranslation() ve bileşenler için useTranslation() hook’u.
Bunlar, basit string değiştirme için t() ve bir çeviri string’inin içine React bileşenleri (mesela bir link ya da ikon) enjekte etmem gerektiğinde interpolate() döndürüyor. Her iki fonksiyon da değişken değiştirmeyi destekliyor, örneğin "Hello {{thing}}", {thing: 'World'} gibi.
İşte çekirdek t() fonksiyonu:
export function t(key: TranslationKeys, values?: Record<string, string | number>, locale?: SupportedLocale): string {
let namespace: string = '';
let translationKey: string = key;
// Anahtar '/' içeriyor mu diye kontrol et - bu bir namespace olduğunu gösterir
const slashIndex = key.indexOf('/');
if (slashIndex !== -1) {
const parts = key.split('/');
namespace = parts.slice(0, -1).join('/');
translationKey = parts[parts.length - 1];
}
const targetLocale = locale ?? currentLocale;
const text = getTranslationValue(targetLocale, namespace, translationKey);
if (values) {
return interpolateString(text, values);
}
return text;
}
Ve React hook’u:
export function useTranslation() {
const [language] = useLanguage();
return useMemo(() => ({
t: (key: TranslationKeys, values?: Record<string, string | number>) =>
t(key, values, language),
interpolate: (key: TranslationKeys, components: Record<string, ReactNode>) =>
interpolate(key, components, language),
}), [language, version]);
}
Tüm kütüphanenin çekirdeği yaklaşık 580 satır kod. Şunları hallediyor:
- Tüm 20 dili her kullanıcıya göndermemek için çeviri dosyalarını lazy-load etmek.
- Çevirileri "namespace" ile parçalara ayırmak (örneğin
common,misc,games/{gameId}). - Her şeyin doğru bağlandığından emin olmak için ham anahtarları gösteren bir "debug" locale’i.
Sistemin bakımının kolay kalması için shared/src/i18n/README.md içine kapsamlı bir dokümantasyon da ekledim. Dosya yapısından, hem client hem server tarafında kullanım örneklerine kadar her şey burada. Standart bir kütüphane kullanmadığım için, bu referans yeni ekip üyelerini (veya gelecekteki beni ya da LLM’leri) içeri almak açısından kritik.
Rakamlarla
Bu güncellemenin ölçeğini daha net görmek için kod tabanında nelerin değiştiğine bakalım:
- 20 desteklenen dil (artı geliştirme için bir debug locale’i).
- 360 locale dosyası oluşturuldu.
- 139.031 satır çeviri kodu.
- İstemci tarafında eklenen 3.938 adet
t()çağrısı. - 728 kaynak dosya değişti.
- Gerçeğin kaynağı olan 18 İngilizce kaynak dosya (16 oyun + common + misc).
Agent’lerle Orkestrasyon
Bunu tamamen elle yapmak, aylarca sürecek, beyin yakan mekanik bir iş olurdu. Bunun yerine, yükün büyük kısmını taşımaları için bir düzineden fazla Cursor agent’ini aynı anda orkestre ettim.
Kod tabanını klasörlere göre "bölümlere" ayırarak başladım. Foony’deki her oyunun kendi klasörü ve kendi çeviri namespace’i var. Böylece başlangıç yüklemesi küçük kalıyor, çünkü sadece oynadığın oyunun çevirilerini yüklüyorsun.
Birden fazla Cursor agent’ini aynı anda çalıştırdım. Her birine "Satranç oyununu çeviri kullanacak hâle getir" gibi spesifik bir bölüm atadım. Agent de dosya dosya gezip kullanıcıya görünen string’leri buldu ve bunları t('games/chess/some.key') ile değiştirdi.
Sonra agent, ilgili anahtarı uygun İngilizce locale dosyasına ekledi ve string’in "ne" olduğunu ve "nerede" kullanıldığını anlatan bir JSDoc yorumuyla birlikte yazdı. Bu bağlam, diğer diller için çeviri üretirken önemli, çünkü LLM’in "Save" kelimesinin "Oyun Ayarlarını Kaydet" mi yoksa "Çiz & Tahmin çizimini kaydet" mi olduğunu anlamasına yardım ediyor.
Kalite Kontrol
Üretilen tüm kodu hızlıca gözden geçirdim. Agent’ler şaşırtıcı derecede iyiydi, ama ara sıra useTranslation hook’unu erken bir return ifadesinden sonra yazmak gibi hatalar yaptılar.
Sıkı tiplenmiş çeviriler inanılmaz yardımcı oldu. Bu sayede her locale için tüm çevirilerin doğru anahtarlara sahip olması (ve yanlış anahtar içermemesi) garanti altına alındı. Ayrıca t() ve interpolate() çağrılarının gerçekten var olan çeviri string’lerini kullanmasını sağladı.
Tip sistemi, olası tüm çeviri anahtarlarını İngilizce kaynak dosyalardan çıkarıyor:
/**
* İç içe geçmiş bir nesne tipinden mümkün olan tüm path’leri çıkarır ve nokta gösterimli anahtarlar üretir.
* Örnek: {a: string, b: {c: string, d: {e: string}}} → 'a' | 'b.c' | 'b.d.e'
*/
type ExtractPaths<T, Prefix extends string = ''> = T extends string
? Prefix extends '' ? never : Prefix
: T extends object
? {
[K in keyof T]: K extends string | number
? T[K] extends string
? Prefix extends '' ? `${K}` : `${Prefix}.${K}`
: ExtractPaths<T[K], Prefix extends '' ? `${K}` : `${Prefix}.${K}`>
: never
}[keyof T]
: never;
export type TranslationKeys =
| ExtractPaths<typeof import('./locales/en/index').default>
| `misc/${ExtractPaths<typeof import('./locales/en/misc').default>}`
| `games/chess/${ExtractPaths<typeof import('./locales/en/games/chess').default>}`
| `games/pool/${ExtractPaths<typeof import('./locales/en/games/pool').default>}`
// ... ve tüm oyunlar için böyle devam eder
Bu, kusursuz TypeScript otomatik tamamlama sağlıyor ve çeviri anahtarındaki herhangi bir typo, derleme zamanında yakalanıyor. Agent’ler t('games/ches/name') gibi hatalar yapamıyor, çünkü TypeScript bunu anında işaretliyor.
Yerelleştirme
İngilizce dönüştürme işi bittikten sonra, kalan locale görevlerini parçalara böldüm. Her agent’e, tek bir İngilizce locale dosyasını belirli bir dile çevirmekten sorumlu olacak şekilde görev verdim.
Örneğin agent’lere şöyle bir prompt verdim:
Lütfen ar/games/dinomight.ts dosyasında en/games/dinomight.ts içindeki tüm çevirilerin olduğundan emin ol.
`export const account: DinomightTranslations = {` satırını kullan.
Hiç tip hatası kalmayana kadar kendi çeviri dosyan üzerinde yinele (başka dosyalar için hata görürsen önemseme--diğer dosyalardan diğer agent’ler sorumlu, paralel çalışıyorsunuz).
Çevirilerin, en içindeki jsdoc bağlamına göre mükemmel ve doğru olmalı.
Bunu kesinlikle elle yapmalısın, "yardımcı" script’ler yazmadan ve kestirme kullanmadan.
Cursor’a bu dosyaların her birini bir LLM’e yediren ve çıktıyı üreten bir script yazdırmayı da düşündüm, ama LLM maliyetinden biraz tasarruf etmek istedim. Sadece eksik çevirileri güncelleyecek bir script kullanmak daha iyi bir yaklaşımdı, muhtemelen gelecekte de benzer bir çözüm kullanırım. Hangi string’lerin güncellenmeye / çevrilmeye ihtiyacı olduğunu takip etmek istiyorum ama işleri de basit tutmak istiyorum. Belki çeviri işini bir veritabanına falan taşırım.
Geliştirme ortamında kullanılmak üzere bir de "debug" locale’i ekledim. Böylece tüm değiştirilmiş string’leri görüp her şeyin çalıştığını doğrulayabiliyorum (bir de bence havalı duruyor). Debug locale’i kullandığında t() anahtarı köşeli parantezlere sarılı olarak döndürüyor:
if (targetLocale === 'debug') {
return `⟦${key}⟧`;
}
Yani "Welcome to Foony!" görmek yerine ⟦welcome⟧ görüyorsun, bu da eksik çevirileri fark etmeyi kolaylaştırıyor.
Son olarak, başka bir agent /{locale}/** routing’ini implemente etti, böylece /ja/games/chess gibi yollar doğru dile (bu örnekte Japonca) yönleniyor.
Blog’u Çevirmek
UI string’lerini çevirmek bir şeydi, peki ya blog yazıları? Tüm blog yazılarını çevirmek için daha da fazla agent yönetmekle uğraşmak istemedim.
Bunu, her şeyi otomatikleştiren bir script (scripts/src/generateBlogTranslations.ts) yazması için bir agent’e görev vererek çözdüm.
Nasıl çalışıyor:
client/src/posts/enklasörünü tarayıp İngilizce MDX dosyalarını buluyor.- Diğer locale klasörlerinde (örneğin
posts/ja,posts/es) eksik çevirileri kontrol ediyor. - Eğer bir çeviri eksikse, İngilizce içeriği okuyor ve Markdown formatını koruyarak çevirmesi için Gemini 3 Pro Preview’a, özel bir prompt ile gönderiyor.
- Yeni dosyayı doğru konuma kaydediyor.
Frontend tarafında, bu MDX dosyalarının hepsini dinamik olarak içeri almak için import.meta.glob kullanıyorum. PostPage bileşenim kullanıcının o anki locale’ine bakıyor ve doğru MDX dosyasını lazy-load ediyor. Eğer çeviri eksikse (script’i henüz çalıştırmadıysam), zarif bir şekilde İngilizceye geri düşüyor.
Sonuç
Bu noktada, 20 locale’in tamamına çevrilmiş, tam çalışan bir sitem vardı!
Oldukça çılgın 3 gündü, ama sonuç olarak dünyanın dört bir yanındaki kullanıcılara (büyük ölçüde) yerel hissettiren, tamamen yerelleştirilmiş bir site ortaya çıktı. Özel, hafif bir kütüphane kurup, sıkıcı refaktoring işini AI agent’lere devrederek, bir yıl önce imkânsız görünen bir şeyi başardım: Tek bir mühendisle, karmaşık bir siteye 3 günde tam i18n. Programlamanın geleceği kodu hızlı yazmak değil, AI agent’leri orkestre etmek ve ürettikleri çıktıyı doğrulayabilecek derin alan bilgisini taşımak.