Turkish and Azerbaijani write four letters where English writes two: dotted i and İ, dotless
ı and I. Lower-case i goes with upper-case İ, and ı with I. Unicode keeps this in its
special casing table
(SpecialCasing.txt), so
every case change has two possible answers, depending on whether it uses the locale.
The outputs below come from Node.js 26 and Swift 6.4 on macOS.
| Code | Result |
|---|---|
"istanbul".toLocaleUpperCase('tr') |
İSTANBUL |
"istanbul".toUpperCase() |
ISTANBUL |
"TITLE".toLocaleLowerCase('tr') |
tıtle |
"TITLE".toLowerCase() |
title |
"istanbul".uppercased(with: Locale(identifier: "tr")) (Swift) |
İSTANBUL |
"istanbul".uppercased() (Swift) |
ISTANBUL |
"İ".toLowerCase(), "İ".lowercased() |
i̇: i followed by U+0307 COMBINING DOT ABOVE |
Two different mistakes follow from this, and GlobalLingo reports both.
Keys are identifiers. Code that changes their case with the device's locale breaks on Turkish and Azerbaijani devices, even in an app that only ships English:
// The server sends "IDLE"; the translation file has "status.idle".
t(`status.${state.toLocaleLowerCase()}`); // on a Turkish device: "status.ıdle", not found
t(`status.${state.toLowerCase()}`); // "status.idle" everywhere
NSLocalizedString("status.\(name)".localizedLowercase, comment: "") // follows the device
NSLocalizedString("status.\(name)".lowercased(), comment: "") // does not
fixtures/expo-i18next/App.tsx and
fixtures/swiftui-catalog have one
each. Keys that contain İ or ı themselves (TRI002) have the same problem from the other side:
no case change gives them back.
Translated text is the opposite: an upper-case heading in Turkish needs the Turkish mapping, or "Kitap teslim edildi" ("the book was delivered") becomes "KITAP TESLIM EDILDI" instead of "KİTAP TESLİM EDİLDİ", with four wrong letters. GlobalLingo reports a case change right after a lookup when the app ships Turkish or Azerbaijani:
t('pickup.city').toUpperCase(); // TRI003
t('pickup.city').toLocaleUpperCase(i18n.language); // the app's language
String(localized: "Pickup in \(city)").uppercased() // TRI003
String(localized: "Pickup in \(city)").uppercased(with: locale) // the language of the text
Dart has no locale-aware toUpperCase()
(its documentation: "uses the language
independent Unicode mapping and thus only works in some languages"). In Flutter, put the
upper-case text in the ARB file as a message of its own, so the Turkish translator writes the
dotted capital:
{
"pickupHeading": "PICKUP IN {city}",
"@pickupHeading": { "placeholders": { "city": { "type": "String" } } }
}
The city itself is data; if it must be upper case too, upper-case it where the app knows the language, for example on the server.
Both mistakes pass every test that runs in English, and both are invisible in a simulator set to English. They show up as raw keys or misspelled words for users with a Turkish or Azerbaijani device, which is why GlobalLingo checks the code for them instead of waiting for a screenshot.