Localizing a SwiftUI app into Turkish: what actually broke

3 min read

SwiftUIiOSLocalization

String Catalogs made the translation the easy part. The bugs came from the dotted İ, from mixing up language and region, and from money.

Backmark shipped in English. The day it went live I started on Turkish, for antique dealers in Türkiye. You would expect the work to be translation, but the 278 strings were the easy part. The real work was in three other places.

String Catalogs did their job

If every string in a SwiftUI app already goes through Text("...") or String(localized:), adding a language is data, not a refactor. Xcode collects the keys into Localizable.xcstrings, and you fill in the second column. The camera permission text lives in a separate InfoPlist.xcstrings, which is easy to forget until a Turkish phone asks for the camera in English.

One choice to make early is tone. I wrote the Turkish in the formal "siz", the way Apple's own Turkish is written. An app that talks to strangers about their belongings should sound like a polite shop assistant, not a friend.

The dotted İ

Turkish has two letters i: dotted (i, İ) and dotless (ı, I). Lowercase i becomes İ in Turkish, not I. Most code never notices this until it does.

Backmark shows the item's category in capitals, like SILVER or FURNITURE. I used localizedUppercase, which follows the device's locale. On an iPhone set to Türkiye with the app in English, "Silver" came out as SİLVER. The opposite case was just as wrong: Turkish "Mobilya" on an English iPhone became MOBILYA, without its dot.

The fix is one line. Uppercase in the language the app is showing, not the language of the device:

word.uppercased(with: Locale(identifier: appLanguageCode))

The same letter breaks comparison. Backmark checks the maker's name read from a mark against the name the model proposed. In Turkish that check has to fold the dotless ı first, and skip trade words like fabrikası (factory), or two readings of the same mark don't agree.

Language and region are two different things

It is tempting to treat "Turkish" as one switch: Turkish text, Turkish lira, Turkish marketplaces. Then think of a Turkish speaker living in Germany. They want to read Turkish, but they pay in euros and buy on eBay Germany.

So the app reads two things separately. The language decides the text, and the language the AI writes its answer in. The region decides the currency and which marketplaces the app opens: eBay's own site in 14 countries, and in Türkiye, sahibinden.com first and eBay.com second.

Money

Estimates are made and stored in US dollars, and converted only when they are shown. That keeps old scans comparable when the rate moves. The rate is the European Central Bank's daily reference rate, refreshed at most every six hours, with a bundled copy so the first launch works offline. Converted values are rounded to two significant figures, because "₺8,437" pretends to a precision that an estimate doesn't have, and the screen says which rate it used and on which day.

Test in the language you ship

My simulator runs in Turkish now, so every screen I check during development is the Turkish one, and Turkish words are often longer than the English ones they replace. The unit tests are pinned to English and the US region, so they don't change their answer depending on whose machine runs them.

If you are adding a language to an iPhone app, the translation is the part you can plan. Leave time for the letters, the money and the difference between where someone lives and what they read.