The Two Days I Lost to the Turkish "i"
The app ran fine on my phone, ran fine on the test device, and would not open at all for some users. Finding the reason took me two days, and it came down to a single letter.
On an Android app I had written, I started getting a strange crash report. The app crashed the moment it opened for some users. There was no problem on my phone. No problem on the emulator. No problem on either of the two devices I tested with.
When I looked through the crash reports, I could not find anything in common: different devices, different Android versions, different screen sizes. Two days later I realized the common factor was not in the device — it was in the device's language setting.
The four "i" letters of Turkish
In most languages, the uppercase form of i is I. Not in Turkish.
Turkish has two separate letters, one dotted and one dotless, and each has its own
uppercase form:
| Lowercase | Uppercase | Note |
|---|---|---|
i | İ | Dotted i — the uppercase keeps the dot |
ı | I | Dotless ı — the uppercase has no dot |
Grammatically, this is correct. The problem is that the operating system applies the rule to every string once the device language is Turkish — to the text shown to the user, and to the technical strings inside the code as well.
The bug itself
The code had a comparison that looked like this:
String tip = sunucudanGelenDeger; // e.g. "IMAGE"
if (tip.toLowerCase().equals("image")) {
gorseliGoster();
}
When the device language is English, "IMAGE".toLowerCase() gives
"image" and the condition holds. When the device language is Turkish, the
result is "ımage" — with a dotless ı. The condition fails,
the flow takes an unexpected branch, and the app crashes.
Developers usually run their own device in English. So the bug never shows up on your machine. It only shows up for users, and it looks inconsistent in the reports.
Where does it blow up?
Once I noticed it, I started seeing it everywhere in the code. The risky spots:
- File extension checks:
"FOTO.PNG".toLowerCase()→ not".pnğ", but every check involving"IMG"breaks. - Type and status fields from the server: comparing values like
"ACTIVE","PENDING","INFO". - HTTP headers: lowercasing header names like
"Content-Type"before comparing them. - Search and filtering: if a user types "istanbul" and "İstanbul" is not found, this is why.
- Email comparison: matching uppercase email addresses by lowercasing them first.
- Color and style names: normalizing values like
"LIGHT"and"DARK".
The right fix: machine string or human text?
The fix is not "turn Turkish off". The right question is: is this text going to be shown to a human, or is it going to be used for a machine comparison?
- If a human will see it — a title, a name, a city — use the device language. The user should see "İSTANBUL", not "ISTANBUL".
- If a machine will compare it — a type field, a key, an extension, a header name — use a locale-independent (invariant) conversion.
The corrected version on the Java and Kotlin side:
// WRONG — behaves according to the device language
tip.toLowerCase()
// RIGHT — locale-independent, same result on every device
tip.toLowerCase(Locale.ROOT)
// For text shown to the user, the device language is the right choice
baslik.uppercase(Locale.getDefault())
On the Swift and JavaScript side things are a little different, but the trap is the same:
// Swift — lowercased() is already locale-independent, safe
tip.lowercased()
// But this one is locale-sensitive, do not use it for machine comparisons
tip.lowercased(with: Locale.current)
// JavaScript — toLowerCase() is locale-independent, safe
tip.toLowerCase()
// This one, however, behaves according to the device language
tip.toLocaleLowerCase()
Dropping string-based machine comparisons in favor of an enum removes
this whole class of bug. And even when string comparison is unavoidable, collecting
the conversion into a single helper function and calling that everywhere is both
safer and easier to audit.
How do you test for it?
The fastest way to catch this bug is to set the test device's language to Turkish and walk through the app once from start to finish. In automated tests, you can force the locale:
// Temporarily set the default locale to Turkish in the test
Locale.setDefault(new Locale("tr", "TR"));
If you are building a multilingual app, adding this test to the pre-release checklist turns a two-day bug hunt into a ten-minute check.
Summary
This bug taught me two things. The first is technical: case conversion is not an innocent operation, it is behavior that depends on the user's language. The second is more general: "it works on my machine" is not a diagnosis, only an observation.
When a bug only shows up for certain users, what they have in common is usually not the device but their settings. Language, time zone, and region — the first three places to look when hunting a bug.
- Locale
- Turkish i
- Java
- Kotlin
- Debugging
- i18n
Demir Taşdemir
Mobile App & Web Developer
I have been building software since 2018. I have shipped 11 apps on the App Store and Google Play; right now I am working on 6 mobile apps, 1 e-commerce platform, and 1 desktop game.
Someone who enjoys hunting bugs like this
The bugs that only show up for certain users are the most instructive ones. I enjoy chasing down problems of this kind.