Dört harf, iki çift

Türk alfabesinde i harfinin iki ayrı çifti vardır: noktalı i büyüdüğünde İ, noktasız ı büyüdüğünde I olur. İngilizce kurallarla yazılmış yazılımın aklında ise tek bir çift vardır: i ile I. Bu iki varsayım karşılaştığında metin sessizce bozulur. istanbul bir yerde ISTANBUL, başka bir yerde İSTANBUL olur; bir dosya adı, bir kullanıcı adı ya da bir protokol anahtarı beklediğiniz gibi eşleşmez. Yazılımcılar arasında bu hata sınıfına çoğu zaman “Turkey test” denir.

Bu yazı sorunun hangi katmanlarda ortaya çıktığını örneklerle gösteriyor: büyük/küçük harf dönüşümü, karşılaştırma ve arama, sıralama, slug ve URL, kimlikler ve protokol anahtarları. JavaScript ve Python örneklerini kendim çalıştırdım ve çıktıları olduğu gibi aktardım (Node v20.15.1 ile ICU 74.2, Python 3.12.13; 11 Ekim 2026). Java, C#/.NET ve SQL için kod çalıştırmadım; o dillerdeki ifadeler resmî belgelere dayanır ve belgenin söylediğinin ötesine geçmez.

İ/ı ile ilgili karakterler
KarakterKod noktasıUnicode adıTürkçedeki eşi
iU+0069LATIN SMALL LETTER Ibüyüğü İ
İU+0130LATIN CAPITAL LETTER I WITH DOT ABOVEküçüğü i
ıU+0131LATIN SMALL LETTER DOTLESS Ibüyüğü I
IU+0049LATIN CAPITAL LETTER Iküçüğü ı

Beşinci bir karakter de işin içine girer: U+0307 COMBINING DOT ABOVE (birleşik nokta). Varsayılan kuralla İ küçültülünce i ile bu nokta işareti ortaya çıkar. Görünmez olduğu için bu yazıda ve şemalarda her zaman kod noktasıyla yazılıyor.

Büyük/küçük harf dönüşümü

En basit dönüşümle başlayalım. JavaScript'te toUpperCase() ve toLowerCase() yerel ayardan bağımsız çalışır ve Unicode'un varsayılan eşlemesini uygular; toLocaleUpperCase("tr-TR") ve toLocaleLowerCase("tr-TR") ise Türkçe kuralı uygular. Aşağıdaki program beş sözcüğü iki kuralla dönüştürür. İ küçültülünce çıkan birleşik nokta görünmez olduğundan çıktıda <U+0307> diye yazdırdım.

case.mjs: beş sözcük, iki kural.js
01const cps = (s) => [...s].map((c) => "U+" + c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")).join(" ");02const vis = (s) => s.replace(/\p{M}/gu, (m) => `<${cps(m)}>`);03 04const words = ["istanbul", "Iğdır", "İzmir", "file", "TITLE"];05console.log("word".padEnd(10), "upper()".padEnd(10), "upper(tr-TR)".padEnd(14), "lower()".padEnd(14), "lower(tr-TR)");06for (const w of words) {07  console.log(08    w.padEnd(10),09    w.toUpperCase().padEnd(10),10    w.toLocaleUpperCase("tr-TR").padEnd(14),11    vis(w.toLowerCase()).padEnd(14),12    w.toLocaleLowerCase("tr-TR"),13  );14}15 16console.log("İ lower()      :", cps("İ".toLowerCase()));17console.log("İ lower(tr-TR) :", cps("İ".toLocaleLowerCase("tr-TR")));18console.log("lengths        :", "İzmir".toLowerCase().length, "İzmir".toLocaleLowerCase("tr-TR").length);
case.mjs çıktısı (Node v20.15.1).text
01$ node case.mjs02word       upper()    upper(tr-TR)   lower()        lower(tr-TR)03istanbul   ISTANBUL   İSTANBUL       istanbul       istanbul04Iğdır      IĞDIR      IĞDIR          iğdır          ığdır05İzmir      İZMIR      İZMİR          i<U+0307>zmir  izmir06file       FILE       FİLE           file           file07TITLE      TITLE      TITLE          title          tıtle08İ lower()      : U+0069 U+030709İ lower(tr-TR) : U+006910lengths        : 6 5

Üç satır derse değer. istanbul varsayılan kuralla ISTANBUL, Türkçe kuralla İSTANBUL olur; iki sonucu karşılaştıran kod false alır. İzmir varsayılan kuralla küçültülünce i, birleşik nokta ve zmir dizisine döner, uzunluk 6 olur; Türkçe kuralla beş harfli izmir çıkar. TITLE ise Türkçe kuralla tıtle olur, çünkü I burada noktasız ı'ya gider. Hiçbiri hata değildir; her kural kendi dilinde doğrudur. Hata, yanlış yerde yanlış kuralı kullanmaktır.

Dört satırlı karşılaştırma. Büyük harf: varsayılan kuralda i ve ı için I; tr-TR kuralında i için İ, ı için I. Küçük harf: varsayılan kuralda I için i, İ için i ve U+0307 birleşik noktası; tr-TR kuralında I için ı, İ için i. Altta üç sözcük örneği: istanbul büyük harfle ISTANBUL ya da İSTANBUL; İzmir küçük harfle 6 kod birimli i, U+0307, zmir ya da 5 harfli izmir; TITLE küçük harfle title ya da tıtle olur.
Aynı girdi iki kuralla farklı sonuç verir; en çok şaşıran satır İ'nin küçültülmesidir: varsayılan kuralda i ile U+0307 (iki kod birimi), tr-TR kuralında tek harf olan i.

Büyük harfe çevirmek bilgi kaybettirir. Varsayılan kuralda hem i hem ı aynı I harfine gider; ISTANBUL metninden hangisinin kastedildiğini geri çıkaramazsınız. Küçük harfe çevirmek de geri dönüşlü değildir: Iğdır kenti varsayılan kuralla iğdır olur, oysa doğrusu ığdır. Dönüşümün sonucunu bu yüzden yalnızca karşılaştırma anahtarı olarak kullanın, orijinali olduğu gibi saklayın.

Kural Unicode'un SpecialCasing.txt dosyasında yazılıdır. Dosya, İ (U+0130) için koşulsuz küçültme eşlemesini 0069 0307 olarak verir; tr ve az bölümlerinde ise aynı harf 0069'a gider. I için tr ve az kuralı, arkasında birleşik nokta yoksa 0131 (ı) olmasıdır. Microsoft'un .NET belgesi de aynı davranışın Azerice (az) kültürde de görüldüğünü belirtir; yani sorun yalnızca Türkçeye özgü değildir.

Python'da sonuç aynı: str.upper(), str.lower() ve str.casefold() yerel ayara bakmaz; belge Unicode'un varsayılan dönüşümünü ve katlamasını anlatır. Aşağıdaki program aynı soruları Python'a sorar.

case.py: Python'un varsayılan eşlemesi ve Unicode eşdeğerliği.python
01import unicodedata02 03 04def cps(s):05    return " ".join(f"U+{ord(c):04X}" for c in s)06 07 08print("i".upper(), "ı".upper(), "I".lower(), "title".upper())09 10lowered = "İzmir".lower()11print(len(lowered), cps(lowered[:2]), len(unicodedata.normalize("NFC", lowered)))12print("İ".casefold() == "i\N{COMBINING DOT ABOVE}", "ı".casefold() == "ı", "I".casefold())13 14decomposed = unicodedata.normalize("NFD", "İ")15print(cps(decomposed), unicodedata.normalize("NFC", decomposed) == "İ")16print(unicodedata.name("İ"), "|", unicodedata.name("ı"))
case.py çıktısı (Python 3.12.13).text
01$ python3 case.py02I I i TITLE036 U+0069 U+0307 604True True i05U+0049 U+0307 True06LATIN CAPITAL LETTER I WITH DOT ABOVE | LATIN SMALL LETTER DOTLESS I

İlk satır I I i TITLE: i ve ı ikisi de I olur, I küçülünce i olur. İkinci satır İzmir küçültülünce 6 kod noktası çıktığını ve normalleştirmenin bunu azaltmadığını gösterir. Üçüncü satırda casefold() aynı şeyi yapar: İ için i ile birleşik nokta. Son iki satır Unicode eşdeğerliğini hatırlatır: İ kanonik olarak I ile U+0307'ye açılır ve NFC bunu geri toplar; i ile U+0307 ise tek bir harfe birleşmez.

Karşılaştırma ve arama

En yaygın hata, büyük/küçük harfe bakmadan eşitlik denemektir. Düzenli ifadelerin i bayrağı Türkçeyi bilmez: iki yönde de false döner ve u bayrağı bunu değiştirmez. Aşağıdaki program bunu ve üç Türkçe-duyarlı alternatifi sınar.

compare.mjs: düzenli ifade, küçültme anahtarı, localeCompare ve katlama.js
01console.log(/İSTANBUL/i.test("istanbul"), /istanbul/i.test("İSTANBUL"));02console.log(/İSTANBUL/iu.test("istanbul"), /istanbul/iu.test("İSTANBUL"));03 04const key = (s) => s.toLocaleLowerCase("tr-TR");05console.log(key("İSTANBUL") === key("istanbul"), key("ISTANBUL") === key("istanbul"));06 07const same = (a, b) => a.localeCompare(b, "tr", { sensitivity: "accent" }) === 0;08console.log(same("istanbul", "İSTANBUL"), same("ığdır", "IĞDIR"), same("ISTANBUL", "İSTANBUL"));09console.log("istanbul".localeCompare("İSTANBUL", "en", { sensitivity: "accent" }));10 11const MAP = { ç: "c", ğ: "g", ı: "i", ö: "o", ş: "s", ü: "u" };12const fold = (s) => s.toLocaleLowerCase("tr-TR").replace(/[çğıöşü]/g, (c) => MAP[c]);13console.log(["istanbul", "ISTANBUL", "İSTANBUL", "Istanbul"].map(fold).join(" "));
compare.mjs çıktısı (Node v20.15.1).text
01$ node compare.mjs02false false03false false04true false05true true false06-107istanbul istanbul istanbul istanbul

İlk iki satır düzenli ifadelerin sonucu: /İSTANBUL/i.test("istanbul") ve /istanbul/i.test("İSTANBUL") ikisi de false verdi, u bayrağıyla da. Üçüncü satırda iki taraf toLocaleLowerCase("tr-TR") ile küçültülünce İSTANBUL ile istanbul eşleşiyor (true); ISTANBUL ise eşleşmiyor (false), çünkü Türkçe kuralda o metin ıstanbul olur. Dördüncü satır, localeCompare ile sensitivity: "accent" kullanan same işlevinin aynı sonucu verdiğini gösterir; ığdır ile IĞDIR da eşleşir, ISTANBUL ile İSTANBUL eşleşmez. Beşinci satırda aynı karşılaştırma "en" ile -1 döndü; İngilizce kurallar İyi aksanlı bir I sayar.

En hassas karar şu: ISTANBUL yazan kullanıcı İstanbul mu demek istedi, yoksa ıstanbul mu? ASCII klavyeli ya da caps lock'u açık bir kullanıcı çoğunlukla ilkini kastetmiştir. Arama kutusunda bu yüzden iki tarafı da Türkçe harfleri ASCII'ye katlayan bir anahtara çevirin (altıncı satırdaki fold): istanbul, ISTANBUL, İSTANBUL ve Istanbul aynı anahtara iner. Bedeli, ayrı sözcüklerin birleşmesidir: kır ile kir aynı anahtarı alır. Arama için kabul edilebilir bu kayıp, kimlik doğrulamada kabul edilemez.

Python'da casefold() Türkçe-duyarlı değildir. Aşağıdaki küçük yardımcılar i/ı/İ/I çiftlerini önceden eşler, sonra standart metotları çağırır.

helpers.py: Türkçe büyütme ve küçültme yardımcıları.python
01TR_UPPER = str.maketrans({"i": "İ", "ı": "I"})02TR_LOWER = str.maketrans({"İ": "i", "I": "ı"})03 04 05def tr_upper(s):06    return s.translate(TR_UPPER).upper()07 08 09def tr_lower(s):10    return s.translate(TR_LOWER).lower()11 12 13print(tr_upper("istanbul"), tr_upper("ığdır"), tr_upper("file"))14print(tr_lower("TITLE"), tr_lower("İzmir"), len(tr_lower("İzmir")))15 16print("İSTANBUL".casefold() == "istanbul".casefold())17print(tr_lower("İSTANBUL") == tr_lower("istanbul"))18print(tr_lower("ISTANBUL") == tr_lower("istanbul"))
helpers.py çıktısı (Python 3.12.13).text
01$ python3 helpers.py02İSTANBUL IĞDIR FİLE03tıtle izmir 504False05True06False

tr_upper ve tr_lower bu önceden birleşmiş karakter örnekleri içindir; NFD biçimindeki I + U+0307 gibi bağlama bağlı eşlemeleri işlemez. Tam Unicode desteği için ICU kullanın. False, True, False çıktısında casefold() iki yazımı eşitlemez, tr_lower eşitler; ISTANBUL ayrı kalır.

Sıralama: collation

Sıralama karşılaştırmanın bir üst katıdır ve aynı yerden bozulur. Array.prototype.sort() karşılaştırma işlevi verilmezse dizgeleri UTF-16 kod birimi sırasıyla dizer; ç, ş, ü, İ ve ı ASCII'nin dışında olduğundan listenin sonuna düşer. Türk alfabesi ise a b c ç d e f g ğ h ı i j k l m n o ö p r s ş t u ü v y z sırasındadır (29 harf).

sort.mjs: kod birimi sırası, İngilizce ve Türkçe collator.js
01const letters = ["ı", "i", "İ", "I", "ç", "c", "z", "ş", "s", "ü", "u"];02console.log("sort()           :", [...letters].sort().join(" "));03console.log('Collator("en")   :', [...letters].sort(new Intl.Collator("en").compare).join(" "));04console.log('Collator("tr")   :', [...letters].sort(new Intl.Collator("tr").compare).join(" "));05 06const words = ["şeker", "sefer", "Işık", "ılık", "iş", "İzmir", "çay", "cam"];07console.log("sort()           :", [...words].sort().join(" "));08console.log('Collator("tr")   :', [...words].sort(new Intl.Collator("tr").compare).join(" "));
sort.mjs çıktısı (Node v20.15.1, ICU 74.2).text
01$ node sort.mjs02sort()           : I c i s u z ç ü İ ı ş03Collator("en")   : c ç i I İ ı s ş u ü z04Collator("tr")   : c ç ı I i İ s ş u ü z05sort()           : Işık cam iş sefer çay İzmir ılık şeker06Collator("tr")   : cam çay ılık Işık iş İzmir sefer şeker
Girdi: ı i İ I ç c z ş s ü u. Varsayılan Array.prototype.sort() sırası I c i s u z ç ü İ ı ş; ASCII dışı harfler (ç ü İ ı ş) listenin sonuna düşer ve her harfin altında kod noktası yazar. Intl.Collator("tr") sırası c ç ı I i İ s ş u ü z; ı ile I, i ile İ aynı harfin küçük ve büyük hâli olarak yan yana durur ve her harfin altında Türk alfabesindeki sırası yazar.
Kod birimi sırası ASCII dışı harfleri listenin sonuna atar; Intl.Collator("tr") Türk alfabesinin sırasını izler. Sonuçlar Node v20.15.1 ve ICU 74.2 ile 11 Ekim 2026'da alındı.

İlk satır varsayılan sıradır: I c i s u z ç ü İ ı ş. İkinci satır Intl.Collator("en") sonucudur; kod birimi sırasından iyidir ama Türkçe değildir (c ç i I İ ı s ş u ü z). Üçüncü satır Intl.Collator("tr") sonucudur: c ç ı I i İ s ş u ü z. Bu sırada ı ile I aynı harfin küçük ve büyük hâli olarak yan yana durur; i ile İ de öyle. Sözcüklerde etki daha somuttur: Türkçe sırada ılık ve Işık, iş ile İzmir'den önce gelir; varsayılan sırada ise Işık en başa, ılık sona yakın bir yere gider.

Python'da sorted() aynı kod noktası sırasını verir. Aşağıdaki oyuncak anahtar yalnızca 29 harfi bilir: harf sırasını ve küçük harfin büyük harften önce gelmesini uygular. Rakamları, noktalama işaretlerini ve â gibi aksanlı harfleri doğru sıralamaz; gerçek uygulamada kullanmayın, yalnızca sıranın mantığını göstermek için yazdım.

sort.py: Türk alfabesine göre oyuncak sıralama anahtarı.python
01ALPHABET = "abcçdefgğhıijklmnoöprsştuüvyz"02RANK = {letter: n for n, letter in enumerate(ALPHABET)}03 04TR_LOWER = str.maketrans({"İ": "i", "I": "ı"})05 06 07def tr_key(word):08    base = [RANK.get(c, len(ALPHABET) + ord(c)) for c in word.translate(TR_LOWER).lower()]09    return base, [c.isupper() for c in word]10 11 12letters = ["ı", "i", "İ", "I", "ç", "c", "z", "ş", "s", "ü", "u"]13print(len(ALPHABET))14print("sorted()  :", " ".join(sorted(letters)))15print("tr_key    :", " ".join(sorted(letters, key=tr_key)))16 17words = ["şeker", "sefer", "Işık", "ılık", "iş", "İzmir", "çay", "cam"]18print("sorted()  :", " ".join(sorted(words)))19print("tr_key    :", " ".join(sorted(words, key=tr_key)))
sort.py çıktısı (Python 3.12.13).text
01$ python3 sort.py022903sorted()  : I c i s u z ç ü İ ı ş04tr_key    : c ç ı I i İ s ş u ü z05sorted()  : Işık cam iş sefer çay İzmir ılık şeker06tr_key    : cam çay ılık Işık iş İzmir sefer şeker

locale.strxfrm ile işletim sisteminin sıralamasını da kullanabilirsiniz, ama sonuç kütüphaneden kütüphaneye değişir. Aşağıdaki program aynı harfleri iki yerel ayarla sıralar.

locale_check.py: sistem yerel ayarı ve locale.strxfrm.python
01import locale02import sys03 04try:05    locale.setlocale(locale.LC_ALL, "")06except locale.Error:07    sys.exit("locale not installed")08 09print(locale.setlocale(locale.LC_ALL))10print("title".upper(), "ı".upper(), "İ".lower() == "i\N{COMBINING DOT ABOVE}")11letters = ["ı", "i", "İ", "I", "ç", "c", "z", "ş", "s", "ü", "u"]12print("strxfrm :", " ".join(sorted(letters, key=locale.strxfrm)))
locale_check.py çıktısı, iki yerel ayarda (Python 3.12.13).text
01$ LC_ALL=en_US.UTF-8 python3 locale_check.py02en_US.UTF-803TITLE I True04strxfrm : c ç i I İ ı s ş u ü z05$ LC_ALL=tr_TR.UTF-8 python3 locale_check.py06tr_TR.UTF-807TITLE I True08strxfrm : c ç I ı İ i s ş u ü z

İki şey öne çıkıyor. upper() ve lower() sonuçları tr_TR.UTF-8 altında da değişmedi: title yine TITLE, ı yine I. Sıralama ise ortama göre değişti ve bu makinedeki glibc'nin Türkçe sırası (I ı İ i), ICU'nun Intl.Collator("tr") sırasından (ı I i İ) farklı çıktı. “Türkçe sıralama” tek bir sonuç değil, kullandığınız kütüphanenin kararıdır.

Veritabanlarında bu karar collation ile verilir. PostgreSQL belgesine göre collation yalnızca sıralamayı belirlemez: lower, upper, initcap gibi büyük/küçük harf işlevleri ve desen eşleme işleçleri de ondan etkilenir; belge libc ve icu sağlayıcılarını anlatır. MySQL belgesi dile özgü collation'ların UCA tabanlı olduğunu ve adlarında dil adı ya da yerel ayar kodu geçtiğini söyler; Türkçe için belirteç tr ya da turkish'tir, utf8mb4_turkish_ci böyle bir addır. SQL Server'ın varsayılan collation tablosu Türkçe (Türkiye) için Turkish_CI_AS verir; CI büyük/küçük harfe duyarsız, AS aksana duyarlı demektir. Bu üç sistemi çalıştırmadım; sonucu kendi veritabanınızda sınayın.

Slug, URL ve arama motoru

Slug üretiminde en yaygın yöntem üç adımdır: küçük harfe çevir, ASCII dışındaki her şeyi at, kalanları tireyle birleştir. Türkçe başlıklarda bu yöntem sessizce bozulur. Aşağıdaki program onu, Türkçe harfleri önce ASCII karşılıklarına eşleyen yöntemle karşılaştırır; ayrıca iki URL gözlemi içerir.

slug.mjs: naif ve Türkçe-duyarlı slug, yüzde kodlama, sunucu adı.js
01const naive = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");02 03const MAP = { ç: "c", ğ: "g", ı: "i", ö: "o", ş: "s", ü: "u" };04const fold = (s) => s.toLocaleLowerCase("tr-TR").replace(/[çğıöşü]/g, (c) => MAP[c]);05const slug = (s) => fold(s).replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");06 07for (const title of ["İstanbul Işık", "Çalışma Ağacı", "Iğdır"]) {08  console.log(title.padEnd(16), "naive:", naive(title).padEnd(16), "tr:", slug(title));09}10 11console.log(encodeURIComponent("İstanbul"), encodeURIComponent("ığdır"));12for (const url of ["https://İSTANBUL.example/", "https://ISTANBUL.example/"]) {13  console.log(url, "->", new URL(url).hostname);14}
slug.mjs çıktısı (Node v20.15.1).text
01$ node slug.mjs02İstanbul Işık    naive: i-stanbul-i-k    tr: istanbul-isik03Çalışma Ağacı    naive: al-ma-a-ac       tr: calisma-agaci04Iğdır            naive: i-d-r            tr: igdir05%C4%B0stanbul %C4%B1%C4%9Fd%C4%B1r06https://İSTANBUL.example/ -> xn--istanbul-o0e.example07https://ISTANBUL.example/ -> istanbul.example

İstanbul Işık naif yöntemde i-stanbul-i-k olur: İ küçültülünce i ile birleşik nokta çıkar, nokta ASCII olmadığı için tireye döner ve sözcüğü ortadan böler; ş ile ı da ayırıcıya dönüşüp Işıkı parçalar. Çalışma Ağacı al-ma-a-ac, Iğdır i-d-r olur. Türkçe-duyarlı yöntem üçünü de okunur adrese çevirir: istanbul-isik, calisma-agaci, igdir. Sitedeki slug oluşturucunun kullandığı slugify işlevini aynı İstanbul Işık girdisiyle yerelde çalıştırdığımda da istanbul-isik aldım.

URL'de iki ayrı konu var. Birincisi yüzde kodlama: encodeURIComponent İstanbul için %C4%B0stanbul, ığdır için %C4%B1%C4%9Fd%C4%B1r üretir. Bu adresler geçerlidir ama insan için okunmaz. İkincisi alan adlarıdır: Node'un URL sınıfı https://İSTANBUL.example/ adresinin sunucu adını xn--istanbul-o0e.example olarak verdi; https://ISTANBUL.example/ için ise istanbul.example verdi. Yani büyük İ içeren bir alan adı küçük harfli ASCII sürümüyle aynı sunucu adına inmedi. Bu bir garanti değil, bu çalıştırmanın gözlemidir; yine de kullanıcıdan gelen sunucu adlarını karşılaştırmadan önce URL gibi standart bir ayrıştırıcıdan geçirmek için iyi bir nedendir.

Arama motorlarının İ/ı'yı nasıl ele aldığına dair ölçülmüş bir bilgim yok, bu yüzden iddiada bulunmuyorum. Güvenli seçenekler kendi tarafınızda: kalıcı adresi ASCII ve küçük harfle üretin, adresi sonradan değiştirirseniz eski adresten yenisine yönlendirme kurun ve site içi arama anahtarını (3. bölümdeki fold) slug ile aynı mantıkla üretin. Hazır araç isterseniz slug oluşturucu Türkçe harfleri ASCII'ye çevirir; metni Türkçe kuralla dönüştürmek için büyük/küçük harf dönüştürücü var.

Kimlikler ve protokol anahtar kelimeleri: locale-independent işlemler

Makineye ait dizgeler (HTTP üstbilgi adları, URL şemaları, yapılandırma anahtarları, dosya uzantıları, HTML etiketleri, tanımlayıcılar) kullanıcının diline göre değişmemelidir. Java belgesi bunu açıkça söyler: argümansız toLowerCase() ve toUpperCase() yerel ayara duyarlıdır ve tanımlayıcılar, protokol anahtarları, HTML etiketleri gibi dizgelerde beklenmedik sonuç verebilir. Belge, "title".toUpperCase() ifadesinin Türkçe bir yerel ayarda "TİTLE" döndürdüğünü yazar ve Locale.ROOT kullanılmasını söyler.

Microsoft'un .NET belgesi güvenlik boyutunu gösterir: IsFileURI("file:") gibi bir çağrı ABD İngilizcesi kültüründe true, Türkçe kültürde false dönebilir; böylece FILE: ile başlayan adresleri engelleyen bir kontrol Türkçe sistemlerde atlatılabilir. Belge, dil bilgisi gerektirmeyen karşılaştırmalar için StringComparison.Ordinal ya da OrdinalIgnoreCase kullanmanızı önerir.

JavaScript'te toLowerCase() ve toUpperCase() zaten yerel ayardan bağımsızdır; tehlike, Türkçe için yazılmış kodun kimlik karşılaştırmasına sızmasıdır. Aşağıdaki program tam bu hatayı gösterir: aynı FILE: dizgesi toLowerCase() ile file: olur, tr-TR kuralıyla fıle: olur.

identifiers.mjs: protokol şeması ve ASCII'ye sınırlı küçültme.js
01const scheme = "FILE:";02console.log(scheme.toLowerCase() === "file:");03console.log(scheme.toLocaleLowerCase("tr-TR") === "file:", scheme.toLocaleLowerCase("tr-TR"));04 05const asciiLower = (s) => s.replace(/[A-Z]+/g, (m) => m.toLowerCase());06console.log(asciiLower("ID"), asciiLower("İD"), asciiLower("ID") === "id", asciiLower("İD") === "id");
identifiers.mjs çıktısı (Node v20.15.1).text
01$ node identifiers.mjs02true03false fıle:04id İd true false

İkinci satır false fıle: verir. Son satır tanımlayıcılar için alternatif bir politikadır: asciiLower yalnızca A-Z harflerini küçültür ve hiçbir yerel ayara bakmaz; İD bu yüzden İd kalır ve id ile eşleşmez. Bu bilinçli bir sonuçtur: kimlikte hangi karakterlerin kabul edildiğini siz belirlersiniz. En sağlam yol çoğu zaman kimliği kayıtta kısıtlı bir ASCII kümesine (örneğin [a-z0-9_-]) sınırlamaktır.

  • Makineye ait kimlik ve anahtar: yerel ayardan bağımsız ya da sıra tabanlı (ordinal) işlem; mümkünse kabul edilen karakter kümesini ASCII ile sınırlayın.
  • Kullanıcıya gösterilen Türkçe metin: yerel ayarı kodda açıkça yazın ("tr-TR"); ortamdan devralmayın.
  • Karşılaştırma anahtarı: orijinal değeri saklayın, yalnızca anahtarı katlayın.
  • Sistem sınırlarında (HTTP, dosya sistemi, veritabanı) hangi tarafın hangi kuralı uyguladığını yazın.

Dil dil kontrol listesi

Aşağıdaki tablo her dil için tuzağı ve iki güvenli yolu özetler. Son sütuna dikkat edin: JavaScript ve Python satırlarını bu yazıdaki programlarla çalıştırdım; Java, C# ve SQL satırları yalnızca resmî belgelere dayanır ve çalıştırılmadı.

Dillere göre İ/ı tuzakları ve güvenli seçenekler
DilTuzakKimlik ve anahtarlar içinTürkçe metin içinDoğrulama
JavaScripttoUpperCase()/toLowerCase() Türkçeyi bilmez; /i düzenli ifadesi İ ve ı'yı eşlemez; argümansız localeCompare ortamı izleditoLowerCase(), ASCII'ye sınırlı kimliktoLocaleUpperCase("tr-TR"), Intl.Collator("tr"), localeCompare(…, "tr", {sensitivity})Çalıştırıldı: Node v20.15.1, ICU 74.2
JavaArgümansız toUpperCase()/toLowerCase() varsayılan yerel ayarı kullanır; Türkçe yerel ayarda "title".toUpperCase() "TİTLE" verir. equalsIgnoreCase yerel ayara bakmaz ama Türkçeyi bilmeztoUpperCase(Locale.ROOT), toLowerCase(Locale.ROOT)Locale.forLanguageTag("tr-TR"); yerel ayara duyarlı karşılaştırma için CollatorÇalıştırılmadı: Java SE 21 belgesi
C#/.NETToUpper()/ToLower() geçerli kültürü kullanır; Compare kültüre, argümansız Equals ordinal kurala bağlıdır. Kültüre bağlı "file:" denetimi Türkçede başarısız olabilirToUpperInvariant(), StringComparison.Ordinal/OrdinalIgnoreCasenew CultureInfo("tr-TR") ile ToUpper(culture); kültürü açıkça belirten karşılaştırmaÇalıştırılmadı: Microsoft Learn
Pythonupper(), lower(), casefold() Unicode varsayılanını uygular: "i".upper() I verir, "İ".lower() iki karakter verir; sorted() kod noktası sırasıdırlower()/casefold() ve ASCII'ye sınırlı kimlikÖnceden i/ı/İ/I eşlemesi (tr_upper, tr_lower); sıralama için ICU tabanlı kütüphane ya da locale.strxfrmÇalıştırıldı: Python 3.12.13
SQLSonuç sütunun ya da veritabanının collation'ına bağlıdır; PostgreSQL'de ayrıca sağlayıcı (libc ya da icu) fark yaratırKimlik sütunlarında dilden bağımsız bir collation ve tek biçimli (ör. küçük harfli ASCII) değerMySQL utf8mb4_turkish_ci, SQL Server Turkish_CI_AS gibi Türkçe collation'lar; PostgreSQL'de ICU ya da libc Türkçe collation'ıÇalıştırılmadı: PostgreSQL, MySQL ve SQL Server belgeleri

Tablo bir karar özetidir, ölçüm sonucu değildir. Java, C# ve SQL davranışı ortama ve collation ayarına bağlıdır; kendi ortamınızda doğrulayın.

Java: belgelenmiş kalıp, çalıştırılmadı.java
01String key = header.toUpperCase(Locale.ROOT);02String label = title.toUpperCase(Locale.forLanguageTag("tr-TR"));
C#: belgelenmiş kalıp, çalıştırılmadı.csharp
01string key = header.ToUpperInvariant();02bool same = string.Equals(a, b, StringComparison.OrdinalIgnoreCase);03string label = title.ToUpper(new CultureInfo("tr-TR"));

Test stratejisi

Turkey test hatalarının çoğu geliştiricinin makinesinde değil, bir müşterinin ya da CI'ın Türkçe yerel ayarlı ortamında patlar. Tersini yapın: Türkçeyi hem test girdisi hem test ortamı olarak kendiniz sağlayın.

  • Sabit bir Türkçe sınama kümesi tutun: istanbul, ISTANBUL, İSTANBUL, Iğdır, ığdır, IĞDIR, İzmir, TITLE, file, FILE:.
  • Aynı kümeyi NFC ve NFD biçiminde deneyin: İ tek kod noktası olarak da, I ve U+0307 olarak da gelebilir.
  • Her işlem için beklentiyi ayrı yazın: büyük harf, küçük harf, uzunluk, eşitlik, sıralama, slug ve düzenli ifade.
  • Yerel ayarı açıkça verdiğiniz ve ortamdan okuduğunuz yerleri ayrı etiketleyin.
  • Testleri en az iki ortamda çalıştırın: İngilizce ve Türkçe (tr-TR) yerel ayarı.

Aşağıdaki küçük kontrol programı bu fikri gösterir. İlk dört sınama yerel ayarı açıkça veren ya da hiç kullanmayan çağrıları sınar. Son ikisi ortamın yerel ayarına güvenen kodu temsil eder ve bilerek en-US varsayar. Programı iki ortamda çalıştırdım.

ci-check.mjs: açık ve ortama bağlı yerel ayar sınamaları.js
01console.log("default locale:", Intl.DateTimeFormat().resolvedOptions().locale);02console.log("title".toLocaleUpperCase(), "(toLocaleUpperCase, no argument)");03 04const letters = ["ı", "i", "İ", "I"];05const checks = [06  ['"TITLE".toLowerCase() is "title"', "TITLE".toLowerCase() === "title"],07  ['"file".toUpperCase() is "FILE"', "file".toUpperCase() === "FILE"],08  ['"istanbul" to tr-TR upper is "İSTANBUL"', "istanbul".toLocaleUpperCase("tr-TR") === "İSTANBUL"],09  ["Collator('tr') order is ı I i İ", [...letters].sort(new Intl.Collator("tr").compare).join(" ") === "ı I i İ"],10  ["ambient sort order is i I İ ı", [...letters].sort(new Intl.Collator().compare).join(" ") === "i I İ ı"],11  ["ambient accent-equal istanbul/İSTANBUL is false", "istanbul".localeCompare("İSTANBUL", undefined, { sensitivity: "accent" }) !== 0],12];13for (const [name, ok] of checks) console.log(ok ? "ok  " : "FAIL", name);14process.exitCode = checks.every(([, ok]) => ok) ? 0 : 1;
ci-check.mjs çıktısı, iki ortamda (Node v20.15.1).text
01$ LC_ALL=en_US.UTF-8 node ci-check.mjs02default locale: en-US03TITLE (toLocaleUpperCase, no argument)04ok   "TITLE".toLowerCase() is "title"05ok   "file".toUpperCase() is "FILE"06ok   "istanbul" to tr-TR upper is "İSTANBUL"07ok   Collator('tr') order is ı I i İ08ok   ambient sort order is i I İ ı09ok   ambient accent-equal istanbul/İSTANBUL is false10$ LC_ALL=tr_TR.UTF-8 node ci-check.mjs11default locale: tr-TR12TITLE (toLocaleUpperCase, no argument)13ok   "TITLE".toLowerCase() is "title"14ok   "file".toUpperCase() is "FILE"15ok   "istanbul" to tr-TR upper is "İSTANBUL"16ok   Collator('tr') order is ı I i İ17FAIL ambient sort order is i I İ ı18FAIL ambient accent-equal istanbul/İSTANBUL is false19# exit 1

İngilizce ortamda altı sınamanın hepsi ok verdi. LC_ALL=tr_TR.UTF-8 ile ilk dört sınama yine geçti; ortamın yerel ayarına dayanan son ikisi FAIL verdi ve çıkış kodu 1 oldu: Intl.Collator() ile argümansız localeCompare ortamın tr-TR olduğunu gördü. Bir ayrıntı daha: argümansız "title".toLocaleUpperCase() iki ortamda da TITLE döndürdü; yani “ortamı izleme” davranışı API'ye ve motora göre değişebilir. Bu, ortamın Türkçe olduğunu da olmadığını da varsaymamanız için bir nedendir: Türkçe istediğiniz her yerde dil etiketini yazın. FAIL satırları hata değil, kodun ortama bağlı olduğunun kanıtıdır.

Diğer çalışma zamanlarında aynı fikir farklı kurulur; bunlar çalıştırılmadı. .NET belgesindeki örnek Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR") ile kültürü değiştirir. Java'da testin başında varsayılan yerel ayarı Locale.setDefault ile Türkçe yapıp sonra eski değerine döndürebilirsiniz. Python'da locale.setlocale ile sistemde kurulu bir Türkçe yerel ayar seçilir (yukarıdaki locale_check.py gibi). Veritabanı testlerinde sütunun collation'ını açıkça yazın ve hem büyük İ hem noktasız ı içeren satırlarla sorgulayın.

Kendi metninizle deneyin

Bu yazıdaki örnekleri kendi metninizle denemek için sitedeki Türkçe İ/ı testi aracını kullanabilirsiniz (yazının sonundaki bağlantı da oraya gider). Araç aynı soruyu iki kuralla yan yana yanıtlar: kök (varsayılan) yerel ayar ve tr-TR. Hesap tarayıcınızda yapılır ve yapıştırdığınız hiçbir kod çalıştırılmaz.

Kısa kılavuz: üstteki alana her satıra bir sözcük gelecek şekilde metin yapıştırın ya da Örnek yükle düğmesini kullanın; isterseniz ikinci alana bir karşılaştırma metni yazın. Araç her satır için büyük harf, küçük harf, küçültülmüş uzunluk, NFC/NFD uzunluğu, eşitlik, düzenli ifade ve slug sonuçlarını iki sütunda gösterir. Farklı çıkanlar Fark var etiketiyle işaretlenir, U+0307 ise COMBINING DOT ABOVE adıyla yazılır. Altta iki sıralama (Array.prototype.sort() ve Intl.Collator("tr")) karşılaştırılır.

Araç yalnızca teşhis içindir. Metni dönüştürmek isterseniz büyük/küçük harf dönüştürücüyü, adres üretmek için slug oluşturucuyu kullanın. Aracın sonuçları tarayıcınızın ICU verisine bağlıdır ve tarayıcıya ya da sürüme göre değişebilir; Java, C# ve SQL tarafını ise kendi ortamınızda ayrıca doğrulayın.

Özet: Türkçe İ sorunu bir hata değil, iki doğru kuralın yanlış yerde buluşmasıdır. Kuralı kodda açıkça yazın, kimlikleri yerel ayardan ayırın ve Türkçeyi test ortamına koyun.

Resmî kaynaklar ve ileri okuma

Bu yazıdaki dil davranışları ve belge anlatımları aşağıdaki kaynaklardan 11 Ekim 2026'da kontrol edildi. JavaScript ve Python örnekleri ayrıca çalıştırıldı; Java, C#/.NET ve SQL ifadeleri yalnızca bu belgelere dayanır:

  1. Oracle Java SE 21: java.lang.String (toUpperCase, toLowerCase, equalsIgnoreCase)
  2. Microsoft Learn: Best practices for comparing strings in .NET
  3. Microsoft Learn: String.ToUpper method
  4. Python documentation: str.lower, str.upper and str.casefold
  5. PostgreSQL documentation: Collation Support
  6. MySQL 8.4 Reference Manual: Unicode Character Sets
  7. Microsoft Learn: Collation and Unicode support (SQL Server)
  8. MDN: String.prototype.toLocaleUpperCase()
  9. Unicode Character Database: SpecialCasing.txt

Kaynak sayfaları yaşayan belgelerdir ve değişebilir. JavaScript sonuçları Node v20.15.1 (ICU 74.2, Unicode 15.1), Python sonuçları Python 3.12.13 (unicodedata 15.0.0) ile bir Linux makinede alındı; başka tarayıcı, motor ya da kütüphane sürümleri farklı sonuç verebilir. Java, C#/.NET ve SQL ifadeleri çalıştırılmadı. “Turkey test” bu hata sınıfı için yaygın kullanılan bir addır; bir kaynağa atfedilmemiştir. Sitedeki araçların bağlantıları 5. ve 9. bölümdedir; Türkçe İ/ı testi aracına ayrıca yazının sonundaki düğmeden ulaşabilirsiniz.

✳

Dört harf, iki çift: sorun, kodun hangi kuralı kullandığını açıkça yazmamasında.

Türkçe İ/ı testini aç ↗