DEV Community

Cover image for Forcing iOS localization at runtime -the right way.
Eldar E.
Eldar E.

Posted on • Edited on

6 2

Forcing iOS localization at runtime -the right way.

iOS localization, and how it should be done

From Apple internalization docs:
Localization is the process of translating your app into multiple languages.

Sounds simple enough, and most of the time it is. You add resources for the languages you wish to support and iOS will automatically select and use one according to user’s device language.

But what if your app specification requires you to force a specific language at runtime? Here is where things get a bit tricky.

In this short article, I’m going to talk about forcing localization, and the right way to approach this topic.

So, you need to force a language?

Here are some of the reasons you might need to force a specific language:

  1. Allow users to pick a language, and switch it on the fly.

  2. The app is designated for a specific country but must be displayed in a different language.

  3. The same app needs to be released to different countries and used only with their native tongue.

At Healthy.io I had a requirement similar to #3, same app with multiple languages. Usually, the simplest way to achieve this is to set a different target for each variation of the app and compile only with the Base language resources.

But, there’s a catch. You need to manage multiple targets and their settings, like Build Phases, Build Rules, etc.
Every change you make to one target’s settings will need to be duplicated to other targets.

An alternative is to keep a single target, with multiple schemes and set the language at runtime.

Pros:

  • Single target, easy to maintain.
  • Extremely easy to add a new app variations with new languages.

Cons:

  • Depends on your localization resources, it might be bloated since your binary was compiled with all the languages.
  • Apple doesn’t really like this approach, but it shouldn’t cause a rejection during the review.

Forcing a language at runtime

**Here is what Apple has to say on the matter: **
In general, you should not change the iOS system language (via use of the AppleLanguages pref key) from within your application.
This goes against the basic iOS user model for switching languages in the Settings app, and also uses a preference key that is not documented,
meaning that at some point in the future, the key name could change, which would break your application.

After a short research you’ve definitely seen this:

UserDefaults.standard.set("de", forKey: "AppleLanguage")

While it does work, it’s not a complete solution for one simple reason:
It requires an app restart. Meaning, the user starts the app for the first time, sees a wrong language, closes the app and starts it again. Great user experience! 😏

What should you do?

The above line of code is the right direction, but by itself, it’s not enough to provide a good user experience.

**From Apple: **
If you want to switch languages in your application, you can do so via manually loading resource files in your bundle.
You can use NSBundle:pathForResource:ofType:inDirectory:forLocalization: for this purpose, but keep in mind that your application would be responsible for all loading of localized data.

In order to achieve the right language on the first app start, you need to access the desired language bundle yourself:

let localizedTextKey = "hello"
guard let bundlePath = Bundle.main.path(forResource: "he", ofType: "lproj"), let bundle = Bundle(path: bundlePath) else {
return NSLocalizedString(localizedTextKey, comment: "")
}
return NSLocalizedString(localizedTextKey, tableName: nil, bundle: bundle, comment: "")

Great! But we don’t want to deal with this part everywhere: “keep in mind that your application would be responsible for all loading of localized data”.
To make it work seamlessly we’ll need to swizzle localizedString(forKey:value:table:) to access the desired bundle.

In case you’re not familiar with swizzling, you should read Method swizzling in iOS swift.

From the article:
Method swizzling is the process of changing the implementation of an existing selector at runtime. Simply speaking, we can change the functionality of a method at runtime.

(Please note, that while I'm against swizzling in general, I think cases like these are a good example of when it's ok to swizzle.)

The complete code:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
let currentLanguage = "he"
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UserDefaults.standard.set(currentLanguage, forKey: "AppleLanguage")
Bundle.swizzleLocalization()
return true
}
}
extension Bundle {
static func swizzleLocalization() {
let orginalSelector = #selector(localizedString(forKey:value:table:))
guard let orginalMethod = class_getInstanceMethod(self, orginalSelector) else { return }
let mySelector = #selector(myLocaLizedString(forKey:value:table:))
guard let myMethod = class_getInstanceMethod(self, mySelector) else { return }
if class_addMethod(self, orginalSelector, method_getImplementation(myMethod), method_getTypeEncoding(myMethod)) {
class_replaceMethod(self, mySelector, method_getImplementation(orginalMethod), method_getTypeEncoding(orginalMethod))
} else {
method_exchangeImplementations(orginalMethod, myMethod)
}
}
@objc private func myLocaLizedString(forKey key: String,value: String?, table: String?) -> String {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let bundlePath = Bundle.main.path(forResource: appDelegate.currentLanguage, ofType: "lproj"),
let bundle = Bundle(path: bundlePath) else {
return Bundle.main.myLocaLizedString(forKey: key, value: value, table: table)
}
return bundle.myLocaLizedString(forKey: key, value: value, table: table)
}
}

And one last thing; To ensure RTL/LTR switch, you need to reload the rootViewController.

Don’t reinvent the wheel

That's it? Not quite. If you need Storyboard/Nibs support or just don't want to manage this yourself, then I have good news for you! There's a great library called LanguageManager-iOS.
It’s small, easy to use, maintained, and supports Carthage, Cocoa-Pods and SPM. Jackpot!!

Quick usage example on how to set the default language on app start with LanguageManager-iOS:

import LanguageManager_iOS
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
LanguageManager.shared.defaultLanguage = .he
return true
}
}

IMPORTANT UPDATE: LanguageManager-iOS No longer supports swizzling

The LanguageManager-iOS lib v1.1.4+, no longer swizzles, thus requires you to call String.localiz() yourself. You can find more information in the Github issue: https://github.com/Abedalkareem/LanguageManager-iOS/issues/46

textAligntment = .natural

textAlignment = .natural can’t be trusted on first app start, and needs to be handled manually for UITextView, UILabel, and UITextField.

**Important Note: **Just to be on the safe side, you should set the text before textAligntment is set.

textView.text = "my awesome localized text"
textView.textAlignment = LanguageManager.shared.isRightToLeft ? .right : .left

Localized Info.plist

Sadly, Localized Info.plist ignores runtime language set, and so far I was not able to resolve this issue.

Final thoughts

When you need to stray from Apple’s path, localization becomes a hassle; So I’m really glad someone took the time to make and maintain a lib like LanguageManager-iOS.

If you have notes, additional issues/solutions, please don’t keep them to yourself.

Cheers! 🎈

Eldar.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more