← blog · 25 Ekim 2026

Türkçe İ Sorunu: Yerel Ayara Bağlı Büyük Küçük Harf Dönüşümü, Veritabanı Karşılaştırma Kuralları ve Testte Yakalamak

"ID".toLowerCase() Türkçe yerel ayarda "ıd" döner ve enum, yapılandırma anahtarı ya da e-posta tekilliği sessizce bozulur. Makine metni ile insan metnini ayırmak, dil ve PostgreSQL düzeyinde doğru çağrılar ve bu hatayı CI'da yakalamanın yolu.

Bir uygulama İngilizce metinle yıllarca sorunsuz çalışır, sonra Türkçe yerel ayarlı bir makinede "ID".toLowerCase() çağrısı "ıd" döndürür ve bir yapılandırma anahtarı bulunamaz, bir enum çözümlenemez, bir e-posta adresi ikinci hesap olarak kaydolur. Sebep Türkçedeki dört harfli i ailesidir: I ile ı, İ ile i. Unicode'un varsayılan dönüşümü I harfini i yapar; Türkçe ve Azerice kuralı ise ı yapar. Hangi kuralın uygulanacağı çoğu dilde çalışma anındaki yerel ayara bağlıdır ve bu ayar geliştiricinin dizüstünde, CI'da ve üretimde farklıdır. Bu yazı sorunun iki ayrı yüzünü ayırmayı, dil ve veritabanı düzeyinde doğru çağrıları ve testte bu hatayı nasıl yakalayacağınızı anlatıyor.

İki ayrı problem var, ikisi tek çözümle kapanmaz

Büyük küçük harf dönüşümü iki farklı amaçla yapılır ve hataların çoğu bu ikisini karıştırmaktan doğar.

Birincisi makine metni: protokol anahtarları, HTTP başlık adları, enum değerleri, dosya uzantıları, yapılandırma anahtarları, SQL anahtar sözcükleri. Bunlar insan dili değildir. Karşılaştırmaları yerel ayardan bağımsız olmalı, hatta çoğu zaman yalnızca ASCII üzerinde tanımlıdır. Burada Türkçe kural uygulamak düpedüz hatadır.

İkincisi insan metni: kullanıcının yazdığı ad, ürün başlığı, arama kutusuna girilen sözcük. Burada Türkçe kural uygulamamak hatadır. "İSTANBUL" araması "istanbul" kaydını bulmalı, "ılık" kelimesi büyütüldüğünde "ILIK" olmalı, "İLIK" değil.

Kural basit: makine metninde kültürden bağımsız, sabit bir dönüşüm kullanın; insan metninde kültürü açıkça verin. Hiçbir durumda işletim sisteminin o anki yerel ayarına güvenmeyin. Varsayılan yerel ayarı kullanan bir çağrı iki ihtimalden birinde mutlaka yanlıştır ve hangisinde yanlış olduğu kodun koştuğu makineye göre değişir.

Dil dil varsayılan davranış

Sorunu büyüten şey, dillerin varsayılanlarının birbirine benzememesidir.

Java en tehlikelisidir: String.toLowerCase() ve toUpperCase() parametresiz çağrıldığında JVM'in varsayılan yerel ayarını kullanır. Türkçe ayarlı bir JVM'de "title".toUpperCase() sonucu "TİTLE" olur ve Enum.valueOf patlar. Makine metni için toLowerCase(Locale.ROOT), insan metni için toLowerCase(Locale.forLanguageTag("tr")) yazın. Büyük küçük harf duyarsız eşitlik için equalsIgnoreCase karakter karakter karşılaştırır ve yerel ayara bakmaz; tanımlayıcılar için uygundur, Türkçe arama için değil.

.NET benzer: ToLower() ve ToUpper() geçerli kültürü kullanır. Makine metninde ToLowerInvariant() ya da daha iyisi karşılaştırmayı hiç dönüştürmeden string.Equals(a, b, StringComparison.OrdinalIgnoreCase) ile yapın. Kod analizinde CA1304 (kültür belirtin) ve CA1309 (sıralı karşılaştırma kullanın) kurallarını hata düzeyine çekmek bu sınıfı derleme aşamasında yakalar.

JavaScript toLowerCase() yerel ayardan bağımsızdır, yani makine metni için güvenlidir ama Türkçe için yanlıştır. Üstelik "İ".toLowerCase() tek karakter değil, i ile birleşen üst nokta (U+0307) olmak üzere iki kod noktası döndürür. Uzunluğu değişen bir dönüşüm, alan uzunluğu sınırlarını ve indeks hesaplarını bozar. Türkçe metin için toLocaleLowerCase("tr") kullanın; parametresiz toLocaleLowerCase() yine tarayıcının ya da Node sürecinin ayarına düşer.

Python str.lower() ve casefold() yerel ayardan bağımsızdır ve aynı U+0307 davranışını gösterir. Standart kütüphanede Türkçeye özel dönüşüm yoktur; gerekiyorsa ICU bağlayıcısı kullanılır ya da I ve İ önceden elle eşlenir.

Go strings.ToLower Unicode varsayılanını uygular. Türkçe için strings.ToLowerSpecial(unicode.TurkishCase, s) standart kütüphanede hazır gelir.

Görüldüğü gibi "yerel ayardan bağımsız" diller makine metninde güvenli, insan metninde yanlış; "yerel ayara bağlı" diller ise ortamına göre ikisinden birinde yanlıştır. Hiçbir dilde varsayılan çağrı ikisini birden doğru yapmaz.

Veritabanında aynı sorun, daha kalıcı hâliyle

Uygulamadaki hata bir istekte görünür ve kaybolur. Veritabanındaki hata indekse, benzersiz kısıta ve kayıtlı veriye yerleşir.

PostgreSQL'de lower(), upper() ve ILIKE sütunun karşılaştırma kuralına (collation) bağlıdır. Veritabanı Türkçe libc ayarıyla oluşturulduysa lower('ID') sonucu 'ıd' olur. E-posta tekilliğini lower(email) üzerindeki benzersiz indeksle sağlıyorsanız, aynı sorgu farklı ayarlı bir veritabanında farklı sonuç verir; yedeği başka ayarla geri yüklediğinizde ikiz kayıtlar ortaya çıkabilir.

Makine metni sayılan alanlarda (e-posta, kullanıcı adı, slug, harici kimlik) en sağlam yol, normalleştirmeyi uygulamada yerel ayardan bağımsız yapmak ve veritabanına zaten küçük harfli değeri yazmaktır. Veritabanı yalnızca bayt eşitliğine bakar, belirsizlik kalmaz:

ALTER TABLE users
  ADD CONSTRAINT email_is_normalized CHECK (email = lower(email COLLATE "C"));
CREATE UNIQUE INDEX users_email_key ON users (email);

COLLATE "C" ile yapılan lower() yalnızca ASCII harflere dokunur, ayardan etkilenmez ve kısıt her ortamda aynı davranır.

İnsan metninde arama için ICU ile deterministik olmayan bir karşılaştırma kuralı tanımlanabilir:

CREATE COLLATION tr_ci (provider = icu, locale = 'tr-TR-u-ks-level2', deterministic = false);
CREATE TABLE products (name text COLLATE tr_ci);

ks-level2 büyük küçük harfi yok sayar ama aksanı korur; ks-level1 aksanı da yok sayar, yani "şeker" ile "seker" eşit olur. Arama kutusu için ikincisi çoğu zaman daha kullanışlıdır. Bu yaklaşımın bedeli vardır: deterministik olmayan kurallarda LIKE desteği PostgreSQL 18'den önce yoktur, eşitlik karşılaştırması bayt eşitliğinden pahalıdır ve indeksin bu kuralla kurulması gerekir. Türkçe sıralama ise ayrı bir konudur; ORDER BY için ICU kurulumuyla gelen "tr-TR-x-icu" kuralını kullanırsanız Ç, Ğ, Ö, Ş, Ü harfleri Z'den sonraya değil doğru yerlerine düşer.

Tuzaklar

Hata yalnızca bir ortamda çıkar. Konteyner imajlarının çoğu C ya da C.UTF-8 ayarıyla gelir; geliştiricinin Türkçe ayarlı makinesinde testler patlar, CI yeşildir. Ya da tersine, sunucuya "Türkçe karakter düzgün görünsün" diye LANG=tr_TR.UTF-8 verilir ve yıllardır çalışan bir kütüphane birden bozulur. Yerel ayarı bir görüntü ayarı sanmak bu sınıfın en sık kökenidir.

Kütüphaneler de etkilenir. Kendi kodunuzu düzeltmeniz yetmez; parametresiz toUpperCase kullanan bir şablon motoru, JSON eşleyicisi ya da sürücü aynı hatayı üretir. Bu yüzden süreç düzeyinde varsayılanı sabitlemek (JVM için -Duser.language=en, .NET için değişmez kültür modu) savunma katmanı olarak değerlidir, ama tek önlem olmamalıdır.

Dönüşüm uzunluğu değiştirir. İ küçültülünce iki kod noktası, Almancadaki ß büyütülünce SS olur. Dönüşümden sonra uzunluk sınırı kontrol ediyor ya da önceden hesaplanmış konumla metin kesiyorsanız yanlış sonuç alırsınız. Sınırı normalleştirilmiş değer üzerinde uygulayın.

Unicode normalleştirmesi ayrı bir adımdır. İ hem tek kod noktası hem I artı birleşen nokta olarak yazılabilir. Kullanıcı girdisini karşılaştırmadan önce NFC'ye çevirmezseniz iki aynı görünen değer eşit çıkmaz.

Dönüştürerek karşılaştırmak yavaştır ve gereksizdir. a.toLowerCase() == b.toLowerCase() iki yeni metin üretir ve hangi kuralı kullandığını gizler. Dilin büyük küçük harf duyarsız karşılaştırma işlevini, kuralı açıkça vererek kullanın.

Testte yakalamak

Bu hata kod incelemesinde gözden kaçar, çünkü toLowerCase() masum görünür. Yakalamanın güvenilir yolu test paketini en az bir kez Türkçe yerel ayarla koşturmaktır: CI'da ikinci bir iş, LANG=tr_TR.UTF-8 ve JVM için -Duser.language=tr -Duser.country=TR. İmajda ilgili yerel ayar üretilmemişse ayar sessizce C'ye düşer ve test bir şey kanıtlamaz; işin başında geçerli ayarı yazdırıp doğrulayın. Bunun yanına I, ı, İ, i içeren sabit test verisi ekleyin: tanımlayıcı olarak kullanılan bir enum adı, bir e-posta, bir arama sorgusu.

Ne zaman bu kadarına gerek yok

Uygulamanız yalnızca ASCII tanımlayıcılarla çalışıyor, insan metnini hiç dönüştürmüyor ve karşılaştırmıyorsa, dil düzeyinde yerel ayardan bağımsız çağrıları zorunlu kılmak ve CI'da Türkçe ayarlı tek bir iş koşmak yeterlidir. ICU karşılaştırma kuralları, Türkçe arama katmanı ve sıralama ancak kullanıcıya dönük arama ya da listeleme varsa maliyetine değer. Arama zaten ayrı bir arama motoruna taşınmışsa, Türkçe çözümleyiciyi orada yapılandırmak veritabanında deterministik olmayan kurallarla uğraşmaktan daha az ağrılıdır.