after spending several days I've actually found the solution. doesn't need re-launch, quite elegant: http://www.factorialcomplexity.com/blog/2015/01/28/how-to-change-localization-internally-in-your-ios-application.html , check the method #2.
It doesn't require to manually re-establish all the titles and texts, just overrides the localization for the custom NSBundle category. Works on both Obj-C and Swift projects (after some tuning) like a charm.
I had some doubts if it will be approved by apple, but it actually did.
This allows to change the language just by updating a UserDefaults key.
This is based on the great answer from @dijipiji. This is a Swift 3 version.
extension String {
var localized: String {
if let _ = UserDefaults.standard.string(forKey: "i18n_language") {} else {
// we set a default, just in case
UserDefaults.standard.set("fr", forKey: "i18n_language")
UserDefaults.standard.synchronize()
}
let lang = UserDefaults.standard.string(forKey: "i18n_language")
let path = Bundle.main.path(forResource: lang, ofType: "lproj")
let bundle = Bundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "")
}
}
Usage
Just add .localized to your string, as such :
"MyString".localized , MyString being a key in the Localizable.strings file.
To call this from Swift, ensure your Bridging Header has:
#import "NSBundle+Language.h"
Then from your code, call:
Bundle.setLanguage("es")
Things to note:
I did not include any sample code to show a language picker or anything. The original linked post does include some.
I changed this code to not change anything persistently. The next time the app runs, it will still try to use the user's preferred language. (The one exception is right-to-left languages, see below)
You can do this anytime before a view is loaded, and the new strings will take effect. However, if you need to change a view that's already loaded, you may want to re-initialize the rootViewController as the original post says.
This should work for right-to-left languages, but it sets two internal persistent preferences in NSUserDefaults for those languages. You may want to undo that by setting the language back to the user's default upon app exit: Bundle.setLanguage(Locale.preferredLanguages.first!)
Jeremy's answer (here) works well on Swift 4 too (I just tested with a simple app and I changed language used in initial view controller).
Here is the Swift version of the same piece of code (for some reasons, my teammates prefer Swift-only than mixed with Objective-C, so I translated it):
import Foundation
enum Language: Equatable {
case english(English)
case chinese(Chinese)
case korean
case japanese
enum English {
case us
case uk
case australian
case canadian
case indian
}
enum Chinese {
case simplified
case traditional
case hongKong
}
}
extension Language {
var code: String {
switch self {
case .english(let english):
switch english {
case .us: return "en"
case .uk: return "en-GB"
case .australian: return "en-AU"
case .canadian: return "en-CA"
case .indian: return "en-IN"
}
case .chinese(let chinese):
switch chinese {
case .simplified: return "zh-Hans"
case .traditional: return "zh-Hant"
case .hongKong: return "zh-HK"
}
case .korean: return "ko"
case .japanese: return "ja"
}
}
var name: String {
switch self {
case .english(let english):
switch english {
case .us: return "English"
case .uk: return "English (UK)"
case .australian: return "English (Australia)"
case .canadian: return "English (Canada)"
case .indian: return "English (India)"
}
case .chinese(let chinese):
switch chinese {
case .simplified: return "简体中文"
case .traditional: return "繁體中文"
case .hongKong: return "繁體中文 (香港)"
}
case .korean: return "한국어"
case .japanese: return "日本語"
}
}
}
extension Language {
init?(languageCode: String?) {
guard let languageCode = languageCode else { return nil }
switch languageCode {
case "en", "en-US": self = .english(.us)
case "en-GB": self = .english(.uk)
case "en-AU": self = .english(.australian)
case "en-CA": self = .english(.canadian)
case "en-IN": self = .english(.indian)
case "zh-Hans": self = .chinese(.simplified)
case "zh-Hant": self = .chinese(.traditional)
case "zh-HK": self = .chinese(.hongKong)
case "ko": self = .korean
case "ja": self = .japanese
default: return nil
}
}
}
"Locale.current.languageCode" will always return system setting language.
So we have to use "Locale.preferredLanguages.first". However the return value looks like "ko-US". This is problem ! So I made the LocaleManager to get only the language code.
LocaleManager.swift
import Foundation
struct LocaleManager {
/// "ko-US" → "ko"
static var languageCode: String? {
guard var splits = Locale.preferredLanguages.first?.split(separator: "-"), let first = splits.first else { return nil }
guard 1 < splits.count else { return String(first) }
splits.removeLast()
return String(splits.joined(separator: "-"))
}
static var language: Language? {
return Language(languageCode: languageCode)
}
}
Use like this
guard let languageCode = LocaleManager.languageCode, let title = RemoteConfiguration.shared.logIn?.main?.title?[languageCode] else {
return NSLocalizedString("Welcome!", comment: "")
}
return title
Here is what Apple says about changing the language;
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.
So as a recommendation, you should navigate your users to general settings page of your app which can be found under
Settings -> [your_app_name] -> Preferred Language
In order to open application settings directly from your app,
You may use these pieces of code;
For Swift;
let settingsURL = URL(string: UIApplication.openSettingsURLString)!
UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
Hint:Before navigating to settings page, it is better to popup and say what the users should do after they switched to settings page is a better user experience idea for your application.
How to transition away from a custom language selector in your app
With systemwide support for in-app language selectors, you no longer need to provide a way to select languages within your app if you support iOS 13 or macOS Catalina or later. If you currently offer such a UI, you should remove it to avoid customer confusion and potential conflict with the system.
If you’d like to guide people to the system settings for language selection, you can replace your app’s custom UI with a flow that launches directly into the Settings app on iOS.
Set system language code to i18n_language key in StandardUserDefaults.
extension String {
var localized: String {
if let _ = UserDefaults.standard.string(forKey: "i18n_language") {} else {
// we set a default, just in case
let lang = Bundle.main.preferredLocalizations.first ?? "en"
UserDefaults.standard.set(lang, forKey: "i18n_language")
UserDefaults.standard.synchronize()
}
let lang = UserDefaults.standard.string(forKey: "i18n_language")
let path = Bundle.main.path(forResource: lang, ofType: "lproj")
let bundle = Bundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "")
}
}
If you're using SwiftUI. In practice, overriding Bundle has proven unreliable.
This will allow you to override the used language on the fly reliably. You just need to make set your app-supported languages to SupportedLanguageCode.
(you might need to reload if you want to localize the current view instantly)
import SwiftUI
class Language {
static let shared = Language()
static let overrideKey = "override.language.code"
var currentBundle: Bundle!
init() {
loadCurrentBundle()
}
func loadCurrentBundle() {
let path = Bundle.main.path(forResource: current.rawValue, ofType: "lproj")!
currentBundle = Bundle(path: path)!
}
enum SupportedLanguageCode: String, Equatable, CaseIterable {
case en
case ar
case de
case es
case fr
case hi
case it
case ja
case ko
case nl
case ru
case th
case tr
case vi
case pt_BR = "pt-BR"
case zh_Hans = "zh-Hans"
case zh_Hant = "zh-Hant"
}
func set(language: Language.SupportedLanguageCode) {
UserDefaults.standard.set(language.rawValue, forKey: type(of: self).overrideKey)
loadCurrentBundle()
}
var current: Language.SupportedLanguageCode {
let code = UserDefaults.standard.string(forKey: type(of: self).overrideKey) ?? Locale.current.languageCode!
guard let language = Language.SupportedLanguageCode(rawValue: code) else {
fatalError("failed to load language")
}
return language
}
}
extension String {
var localized: String {
Language.shared.currentBundle.localizedString(forKey: self, value: nil, table: nil)
}
}
You're just loading up needed Bundle and specifying that bundle when you localize in the overridden localized.