iOS – and Apple OSs in general – have excellent I18N support. You more or less don’t have to worry about date and number formatting as long as you are following the rules. The essential rules are:
- all translations are prepared beforehand and compiled into the app
- language choice is done outside your app, in system' Settings
Fairly often, I have requests from clients to implement in-app language change, which should (at least) instantly translate the app. There is no API support in iOS frameworks for this.
It’s not impossible though.
Force-load correct set of translations
First issue is that all the translations are kept inside LANGUAGE_CODE.lproj folders, like
When your app starts, iOS looks into
UserDefaults.standard.value(forKey: "AppleLanguages") value and loads the corresponding translations from the
.lproj folder. It processes all the
.storyboard files plus makes an internal dictionary (or something similar) that
NSLocalizedString() is using to load appropriate string.
This is done at the app start and there is no way I know of to force-change the language without restarting the app itself.
So we need to cheat and since iOS runtime is dynamism heaven enabled by Objective-C, we can actually do that.
- subclass Bundle and override its
localizedString(forKey…)method to check for
- force-change the Class of the Bundle.main to be that subclass so our method is called instead of default one (thanks Objective-C!)
- when app’s language change is initiated somewhere in the app, load the Main bundle again but with appropriate .lproj
- saved that loaded bundle as Bundle.main.localizedBundle
Things now work on their own. This will be enough to instantly translate the app if your app is using
LocalizedString() only. If your strings are in IB files though, then you need to force-reload those files.
In the demo app, I am pushing the notification that informs everyone about Locale change and AppDelegate is responding to that by reloading the
1 2 3 4 5 6 7 8 9 10 11
Reloading string translations is not enough, though. Large number of system-wide methods on various types is consulting
Locale.current (or rather
Locale.autoupdatingCurrent..?) to perform its function.
Example: say you have a
UITextField for number input. You set its keyboard to DecimalPad which display decimal separator as lower left button. The caption and the actual character that’s a result of the tap is the value of
decimalSeparator property on the currently active Locale.
Further, if you try to convert
textField.text to a
Decimal, conversion will accept only that one value as valid decimal separator. So if you’ve entered “5,15” and your
localeIdentifier is “en_US”, that will be converted to
5.15. Bummer. While this is solvable issue by re-setting cached Formatters, the keyboard issue mentioned above is not.
Thus you need to create your own custom keyboard (yuck!) or…you can swizzle
NSLocale.current property. (Thanks again, Objective-C!)
1 2 3 4 5 6 7 8 9 10 11 12
NSLocale.app is my own custom Locale I build in any way I need. I start with system’s original value for
autoupdatingCurrent and then add and/or replace components using whatever app-level or user-level settings I have.
Swizzling needs to be done only once, as early as possible in app’s lifecycle.
I wrote demo app which shows all of this in action. It’s available on GitHub, called LanguageSwitcher. Code is over-commented as guidance what you can do and where to go from there.
Look into the
localize() method in demo’s ViewController. This is actually my preferred method to handle instant translations as there’s no loss of user context. It’s more work but it leads to better customer experience.
If you cache your DateFormatter and NumberFormatter instances - as you certainly should - then on language change you need to make sure to re-set up Locale and dateFormat values.
There’s a non-critical issue with the keyboard: when the keyboard is shown, iOS will cache that generated view. Thus if you do this sequence:
- open the app
- make sure English is shown
- tap the text field so the keyboard appears (you can see the
.shown as decimal separator)
- dismiss it
- switch to Serbian (which uses
,as decimal separator
- tap the text field again
You can see that keyboard view still shows the
. but if you tap it, it will correctly enter
,. Hence it’s annoyance at best.
I’m not aware of any way to force-clear the keyboard cache in iOS. If you do, please let me know.