İçeriğe atla
md ~/yazilar/turkce-i-sorunu-buyuk-kucuk-harf
Atatürk'ü Saygı ve Özlemle Anıyoruz

10 Kasım · 88. yıl dönümü

Atatürk'ü Saygı ve Özlemle Anıyoruz

Cumhuriyetimizin kurucusu Mustafa Kemal Atatürk, 10 Kasım 1938'de saat 09.05'te İstanbul'da, Dolmabahçe Sarayı'nda hayatını kaybetti. Her yıl bu saatte bütün ülkede saygı duruşunda bulunuyoruz.

Bize emanet ettiği Cumhuriyeti yaşatmak hepimizin görevi.

Benim naçiz vücudum elbet bir gün toprak olacaktır, fakat Türkiye Cumhuriyeti ilelebet payidar kalacaktır.Mustafa Kemal Atatürk
Tüm yazılar

· 5 dk okuma

Türkçe İ Sorunu: Büyük-Küçük Harf Çevirisi Beş Projede Nasıl Bozuldu

Türkçe İ Sorunu: Büyük-Küçük Harf Çevirisi Beş Projede Nasıl Bozuldu

Türkçe yazılım geliştiren herkesin er geç karşılaştığı bir hata var: büyük ve küçük harf çevirisi. İngilizcede I ile i bir çifttir. Türkçede iki ayrı çift vardır: I–ı ve İ–i. Programlama dillerinin çoğu metni varsayılan olarak İngilizce kurala göre çevirir, bazıları ise çalıştığı bilgisayarın diline göre. İkisi de yanlış yerde kullanılınca hata verir. Bu yazıda bu Türkçe İ sorununu beş farklı projemde beş farklı biçimde nasıl yaşadığımı ve her birinde nasıl çözdüğümü anlatıyorum: Java, JavaScript, Python ve C#.

Sorun tek cümlede

Dönüşümİngilizce kuralTürkçe kural
I küçük harfeiı
i büyük harfeIİ
İ küçük harfei + birleşik nokta (iki karakter)i

Bir metni küçük harfe çevirip başka bir metinle karşılaştıran her kod bu tabloya takılabilir: arama, kategori eşleştirme, e-posta adresi, dosya uzantısı, protokol adı. Hatanın sinsi tarafı, İngilizce test verisiyle hiç görünmemesi.

1. Java: "INBOX" klasörü "ınbox" oldu

E-posta istemcimde klasörlere simge veriyorum: gelen kutusu, gönderilenler, çöp. IMAP sunucusu gelen kutusunun adını INBOX olarak gönderir. Kod klasör adını küçük harfe çevirip içinde "inbox", "sent", "trash" gibi kelimeler arıyordu.

Java'da toLowerCase() parametresiz çağrılınca bilgisayarın varsayılan dilini kullanır. Türkçe Windows'ta bu Türkçedir ve "INBOX".toLowerCase() sonucu "ınbox" olur. Noktasız ı yüzünden hiçbir eşleşme tutmadı, gelen kutusu simgesiz kaldı. Aynı sorun e-posta adresinden sunucu ayarını bulan kodda da vardı: adresi büyük harfle yazan biri için GMAIL.COM → gmaıl.com olur ve hazır Gmail ayarı bulunmaz.

Çözüm, teknik metinlerde dilden bağımsız kuralı açıkça istemek:

// Locale.ROOT: Türkçe kuralla "INBOX" -> "ınbox" olurdu (noktasız ı)
final String name = fullName.toLowerCase(Locale.ROOT);

Kural olarak şunu koydum: klasör adı, alan adı, protokol kelimesi gibi makineye ait metinlerde Locale.ROOT; kullanıcıya gösterilen Türkçe metinlerde Türkçe kural.

2. JavaScript: Adam Asmaca'da yanlış harf

Adam Asmaca oyunumda oyuncu harfi klavyeden de yazabiliyor. Türkçe oyunda I tuşuna basan oyuncu ı harfini kastediyor. JavaScript'in toLowerCase() metodu dili hiç dikkate almaz ve I'yı her zaman i yapar. Onu kullansaydım "KIŞ" kelimesini arayan oyuncu I'ya bastığında oyun i harfini sorgulayacak ve yanlış harf diye can götürecekti. Bu hatayı ilk e-posta istemcisinde yaşadığım için Adam Asmaca'da baştan dili açıkça veren toLocaleLowerCase'i kullandım:

export function normalizeLetter(input, lang) {
  if (typeof input !== 'string' || [...input].length !== 1) return null;
  const letter = input.toLocaleLowerCase(lang);
  return ALPHABETS[lang].includes(letter) ? letter : null;
}

Aynı oyun İngilizce de oynanabildiği için dil sabit değil, oyunun o anki dili. Harf kutularındaki büyük harfler de aynı şekilde toLocaleUpperCase(lang) ile yazılıyor; yoksa Türkçe kelimedeki i ekranda I olarak görünürdü. Testler iki dili birlikte deniyor:

assert.equal(normalizeLetter('I', 'tr'), 'ı');
assert.equal(normalizeLetter('İ', 'tr'), 'i');
assert.equal(normalizeLetter('I', 'en'), 'i');

3. JavaScript: "kadikoy" yazan Kadıköy'ü bulamadı

Hava durumu uygulamamda şehir arama iki servisle çalışıyor. Open-Meteo'nun yer arama servisi dünya şehirlerinde hızlı, ama Türkçe "ı"yı tanımadığı için Kadıköy'ü bulamıyordu. Bu yüzden Türkiye'deki il ve ilçeler için OpenStreetMap tabanlı Photon'u ekledim.

İki servisten gelen sonuçları kullanıcının yazdığıyla karşılaştırmam gerekiyordu. Kullanıcı telefonda "kadikoy" da yazabilir, "KADIKÖY" de. Bunun için metni tek bir karşılaştırma biçimine indiren bir fonksiyon yazdım:

export function foldText(value) {
  return value
    .toLocaleLowerCase('tr')
    .normalize('NFD') // ş → s + çengel işareti; işaretler bir sonraki satırda silinir
    .replace(/[̀-ͯ]/g, '')
    .replace(/ı/g, 'i');
}

Sıra önemli. Önce Türkçe kuralla küçük harfe çevriliyor, böylece I → ı ve İ → i doğru oluyor. Sonra Unicode ayrıştırmasıyla (NFD) ş, ö, ü gibi harfler temel harf ve işarete ayrılıyor ve işaretler siliniyor. En sonda ı, i'ye çevriliyor; çünkü ı ayrıştırılabilen bir harf değil, kendi başına bir harf. Testi:

assert.equal(foldText('ŞANLIURFA'), 'sanliurfa');
assert.equal(foldText('ısparta'), foldText('Isparta'));
assert.equal(foldText('Kadıköy'), 'kadikoy');

4. Python: "İrem" adı "Irem" oldu

KVKK Belge Asistanı'nda belgelerdeki kişi adlarını model görmeden önce maskeliyorum ve yerlerine sahte adlar koyuyorum. Ad sözlüğünde arama yaparken ve sahte adı yazarken Python'un iki davranışına takıldım:

>>> "İ".lower()
'i̇'          # i + birleşik nokta (U+0307), iki karakter
>>> "irem".capitalize()
'Irem'

İlki, büyük harfle yazılmış "İREM"i sözlükteki "irem" ile eşleştirmeyi bozuyordu: küçük harfe çevrilmiş metin görünüşte aynı, ama içinde fazladan görünmez bir karakter var. İkincisi, sahte veride ve e-posta metinlerinde "İrem" yerine "Irem" yazdırıyordu.

Python'un standart kütüphanesinde dile göre harf çevirisi yok. İki küçük fonksiyon yazdım:

def tr_lower(word: str) -> str:
    return word.replace("I", "ı").replace("İ", "i").lower()

def tr_capitalize(word: str) -> str:
    if not word:
        return word
    first = {"i": "İ", "ı": "I"}.get(word[0], word[0].upper())
    return first + tr_lower(word[1:])

Bu projede hata pahalıydı, çünkü yanlış eşleşen bir ad maskelenmeden modele gidebilirdi. Bu yüzden testler her adı küçük harf, büyük harf, ek almış ve Türkçe harfsiz yazımlarıyla üretiyor ve hiçbirinin maskesiz geçmediğini kontrol ediyor.

5. C#: "ip13" araması "IP13" kodunu bulamadı

Tamirhane'nin stok sayfasında parça kodu ya da adıyla arama yapılıyor. Parça adları Türkçe ("Şarj soketi", "iPhone 13 ekran paneli"), kodlar İngilizce harfli (IP13-SCR). İlk sürümde aramayı Türkçe kültürle, büyük/küçük harf duyarsız yaptım. "şarj" yazınca "Şarj soketi" bulundu, ama "ip13" yazınca IP13-SCR bulunmadı: Türkçe kurala göre i'nin büyüğü İ, yani ip13 ile IP13 aynı kelime değil.

Dilden bağımsız kültüre geçmek de çözüm değildi; bu sefer "ince" araması "İnce kablo"yu bulmaz. Gerçek veride iki tür metin yan yana durduğu için iki kuraldan biri tutarsa eşleşme saydım:

// Türkçede "i"nin büyüğü "İ": sadece Türkçe kurala bakılsa "ip13" araması "IP13-SCR" kodunu bulamazdı.
// Kodlar İngilizce harfli, adlar Türkçe; iki kuraldan biri tutarsa eşleşir.
private static bool Contains(string text, string query) =>
    Turkish.IndexOf(text, query, CompareOptions.IgnoreCase) >= 0
    || CultureInfo.InvariantCulture.CompareInfo.IndexOf(text, query, CompareOptions.IgnoreCase) >= 0;

Test, iki türü birlikte deniyor:

[InlineData("ip13", "iPhone 13 ekran paneli")] // Türkçe kurala göre "i" ≠ "I"; kodlar yine bulunur
[InlineData("şarj", "Şarj soketi USB-C")]
[InlineData("ŞARJ", "Şarj soketi USB-C")]

Öğrendiklerim

Beş hatanın hepsi aynı soruya çıkıyor: bu metin kimin metni?

  • Makinenin metni (klasör adı, alan adı, dosya uzantısı, parça kodu, protokol kelimesi): dilden bağımsız kural. Java'da Locale.ROOT, C#'ta InvariantCulture, JavaScript'te toLowerCase().
  • İnsanın metni (ad, kelime, şehir): metnin dilinin kuralı, açıkça verilerek. Java'da Locale.forLanguageTag("tr"), JavaScript'te toLocaleLowerCase('tr'), C#'ta tr-TR, Python'da elle yazılmış bir fonksiyon.
  • İkisinin karıştığı arama: ya iki kurala birden bakılır ya da metin harf işaretlerinden arındırılarak tek biçime indirilir.

Bir de test kuralı: büyük/küçük harf çeviren her fonksiyonun testinde en az bir I, bir İ ve bir ı bulunmalı. Bu üç harf, İngilizce test verisinin göremediği hataları ilk denemede yakalıyor.