iOS apps will run on many devices in many regions. To reach the most users, your application should handle text, audio files, numbers, currency, and graphics in ways appropriate to the locales where your application will be used. The framework automatically selects the resources that best match the device. Such a behavior is ok for most apps. But for some specialized apps, it might be required to change the localization at runtime, e.g. through in-app Settings.

In-app localization

On iOS, there are a lot of arguments as to why this is not a good pattern. Generally, from a programmatic point of view, it's not easy if you are using Storyboards and there are a lot of hacks involved. Anyway, if this is something that you absolutely must include in your app, you are at the right place.

Loading localized string in code

Let's get the simple things out of the way first. To load localized strings in code, you first need to load the bundle for the appropriate language.

let bundle: Bundle = .main  
// Try to load from language specific bundle
if let path = bundle.path(forResource: "fr", ofType: "lproj"),  
  let bundle = Bundle(path: path) {
  return bundle.localizedString(forKey: "Hello, World", value: nil)
} 
// Load from Base bundle
else if let path = bundle.path(forResource: LCLBaseBundle, ofType: "lproj"),  
  let bundle = Bundle(path: path) {
  return bundle.localizedString(forKey: "Hello, World", value: nil)
}

Persist selected language

The next step that is pretty straightforward is to persist the user's selection of language in UserDefaults.

UserDefaults.standard.set(selectedLanguage, forKey: "PGCurrentLanguageKey")  
UserDefaults.standard.synchronize()  

You need to read the USerDefaults later whenever the localization of a string is required.

func currentLanguage() {  
  if let currentLanguage = UserDefaults.standard.object(forKey: "PGCurrentLanguageKey") as? String {
    return currentLanguage
  }
  return defaultLanguage()
}

The above two are so common, that there are already libraries available to handle this stuff for you. Localize-Swift is one such library that handles this and provides very clean helpers to help your localization needs. With it, you can write things like:

textLabel.text = "Hello World".localized()

Localize.setCurrentLanguage("fr")

Localize.resetCurrentLanguageToDefault()  

Storyboard Localization

If you start using this, one thing that you will notice straight away is that it doesn't help with Storyboard localization. Why? The initial locale on the Storyboard is read right after you load the storyboard. This could be done in your AppDelegate if you manually load the storyboard and set the window or automatically by UIKit if you have set the Storyboard in your project settings.

Restart app after language change

One possible solution for localized storyboards is to update the AppleLanguages key in UserDefaults when the user changes the language in the app and then force the user to restart the app.

NotificationCenter.default.addObserver(self, selector: #selector(languageUpdated), name: NSNotification.Name(LCLLanguageChangeNotification), object: nil)

func languageUpdated() {  
  UserDefaults.standard.set(Localize.currentLanguage(), forKey: "AppleLanguages")
  UserDefaults.standard.synchronize()
  // TODO: Show alert asking user to restart the app for language change to take effect
}

Use stubs in storyboard

The second alternative is to just use stubs for all text in the storyboard and then set the text manually in code. This might be very cumbersome for even standard size apps, but is definitely the best way to go to allow a perfect flow for the user when he changes the language.

Reload Storyboard locally after a language update

This requires an extension on Bundle that tricks it into loading strings from the current localization instead of the cached locale that UIKit selects when starting the app. Create an extension on Bundle and override

func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {  
  let bundle: Bundle = bundle ?? .main
  if let path = bundle.path(forResource: Localize.currentLanguage(), ofType: "lproj"), let bundle = Bundle(path: path) {
    return bundle?.localizedString(forKey: key, value: value, table: tableName) ?? ""
  } else {
    return super.localizedString(forKey: key, value: value, table: tableName)
  }
}

This makes the Storyboard load strings from the updated locale for any view controllers that are constructed after the change. In order to change the existing views, you still need to either change them manually, reconstruct the current view controller (dismiss/pop and present/push again) or simple send him back to the original view controller of the app. In your AppDelegate, after a language change, do this:

var storybaord = UIStoryboard(name: "Main", bundle: nil)  
window.rootViewController = storybaord.instantiateInitialViewController()