how to change storyboard based on language at runtime in swift - ios

To support localization I have made two Main.stroyboard, one for Persian and one for English (rather than just have .string file)
It acts good for different localization.
Now I want to have language options in my app, so user can change the language of app. For this I want to load correct storyboard based on user selection.
what i am using is as following:
let path = Bundle.main.path(forResource: "fa", ofType: "lproj")
let bundle = Bundle(path: path!)
let nsb = UIStoryboard.init(name: "Main", bundle: bundle)
let vc = nsb.instantiateViewController(withIdentifier: "MainController")
self.navigationController?.pushViewController(vc, animated: true)
Although it changes the Storyboard based on user selection but all the images are missing!
is there any reason for that?
is my method statndard?

You can change it at runtime:
define a new bundle class:
var bundleKey: UInt8 = 0
class AnyLanguageBundle: Bundle {
override func localizedString(forKey key: String,
value: String?,
table tableName: String?) -> String {
guard let path = objc_getAssociatedObject(self, &bundleKey) as? String,
let bundle = Bundle(path: path) else {
return super.localizedString(forKey: key, value: value, table: tableName)
}
return bundle.localizedString(forKey: key, value: value, table: tableName)
}
}
and then extend Bundle class like this:
extension Bundle {
class func setLanguage(_ language: String) {
defer {
object_setClass(Bundle.main, AnyLanguageBundle.self)
}
objc_setAssociatedObject(Bundle.main, &bundleKey, Bundle.main.path(forResource: language, ofType: "lproj"), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
and finally for change language at runtime use this:
Bundle.setLanguage("tr")
Bundle.setLanguage("en")..
when you change the language you should reload your view controller by instantiating again your viewcontroller from its storyboard because we changed our bundle.

Related

How to change language inside of the app in swift [duplicate]

I am making ios app on XCODE 6.3 by Swift.
And my app will have the choose language function like the image below
I already have storyboard for my local language.
But i can't find out how to change the localization programmatically off the app by the button.
Anyone know how to do it
Here's a way to change it on the fly with Swift, add an extension function to String:
extension String {
func localized(lang:String) ->String {
let path = NSBundle.mainBundle().pathForResource(lang, ofType: "lproj")
let bundle = NSBundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "")
}}
Swift 4:
extension String {
func localized(_ lang:String) ->String {
let path = Bundle.main.path(forResource: lang, ofType: "lproj")
let bundle = Bundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "")
}}
then assuming you have the regular Localizable.strings set up with lang_id.lproj ( e.g. en.lproj, de.lproj etc. ) you can use this anywhere you need:
var val = "MY_LOCALIZED_STRING".localized("de")
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.
Changing the language
UserDefaults.standard.set("en", forKey: "i18n_language")
Swift 4.2
In my case, if user change the language setting, I have to update 2 things at runtime.
1. Localizable.strings
2. Storyboard Localization
I make #John Pang code more swifty
BundleExtension.swift
import UIKit
private var bundleKey: UInt8 = 0
final class BundleExtension: Bundle {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
return (objc_getAssociatedObject(self, &bundleKey) as? Bundle)?.localizedString(forKey: key, value: value, table: tableName) ?? super.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
static let once: Void = { object_setClass(Bundle.main, type(of: BundleExtension())) }()
static func set(language: Language) {
Bundle.once
let isLanguageRTL = Locale.characterDirection(forLanguage: language.code) == .rightToLeft
UIView.appearance().semanticContentAttribute = isLanguageRTL == true ? .forceRightToLeft : .forceLeftToRight
UserDefaults.standard.set(isLanguageRTL, forKey: "AppleTe zxtDirection")
UserDefaults.standard.set(isLanguageRTL, forKey: "NSForceRightToLeftWritingDirection")
UserDefaults.standard.set([language.code], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
guard let path = Bundle.main.path(forResource: language.code, ofType: "lproj") else {
log(.error, "Failed to get a bundle path.")
return
}
objc_setAssociatedObject(Bundle.main, &bundleKey, Bundle(path: path), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
Language.swift
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
}
}
}
Use like this
var languages: [Language] = [.korean, .english(.us), .english(.uk), .english(.australian), .english(.canadian), .english(.indian),
.chinese(.simplified), .chinese(.traditional), .chinese(.hongKong),
.japanese]
Bundle.set(language: languages[indexPath.row].language)
"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
Usable code in Swift 4:
extension Bundle {
private static var bundle: Bundle!
public static func localizedBundle() -> Bundle! {
if bundle == nil {
let appLang = UserDefaults.standard.string(forKey: "app_lang") ?? "ru"
let path = Bundle.main.path(forResource: appLang, ofType: "lproj")
bundle = Bundle(path: path!)
}
return bundle;
}
public static func setLanguage(lang: String) {
UserDefaults.standard.set(lang, forKey: "app_lang")
let path = Bundle.main.path(forResource: lang, ofType: "lproj")
bundle = Bundle(path: path!)
}
}
and
extension String {
func localized() -> String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.localizedBundle(), value: "", comment: "")
}
func localizeWithFormat(arguments: CVarArg...) -> String{
return String(format: self.localized(), arguments: arguments)
}
}
call:
let localizedString = "enter".localized()
set new a locale(for example "ru"):
Bundle.setLanguage(lang: "ru")
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 UIKit
private var kBundleKey: UInt8 = 0
class BundleEx: Bundle {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let bundle = objc_getAssociatedObject(self, &kBundleKey) {
return (bundle as! Bundle).localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
static let once: Void = {
object_setClass(Bundle.main, type(of: BundleEx()))
}()
class func setLanguage(_ language: String?) {
Bundle.once
let isLanguageRTL = Bundle.isLanguageRTL(language)
if (isLanguageRTL) {
UIView.appearance().semanticContentAttribute = .forceRightToLeft
} else {
UIView.appearance().semanticContentAttribute = .forceLeftToRight
}
UserDefaults.standard.set(isLanguageRTL, forKey: "AppleTextDirection")
UserDefaults.standard.set(isLanguageRTL, forKey: "NSForceRightToLeftWritingDirection")
UserDefaults.standard.synchronize()
let value = (language != nil ? Bundle.init(path: (Bundle.main.path(forResource: language, ofType: "lproj"))!) : nil)
objc_setAssociatedObject(Bundle.main, &kBundleKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
class func isLanguageRTL(_ languageCode: String?) -> Bool {
return (languageCode != nil && Locale.characterDirection(forLanguage: languageCode!) == .rightToLeft)
}
}
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.
Swift 4
UserDefaults.standard.set(["es", "de", "it"], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
Swift 3
NSUserDefaults.standardUserDefaults().setObject(["es", "de", "it"], forKey: "AppleLanguages")
NSUserDefaults.standardUserDefaults().synchronize()
Source: here
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)
For Obj-C;
NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
[[UIApplication sharedApplication] openURL: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.
The solution linked by whiteagle actually works to switch the language on the fly. Here's the post.
I simplified the sample code on there to a single .h/.m that will change the language on-the-fly, in memory. I've shown how to call it from Swift 3.
Header:
//
// NSBundle+Language.h
// ios_language_manager
//
// Created by Maxim Bilan on 1/10/15.
// Copyright (c) 2015 Maxim Bilan. All rights reserved.
//
#import <Foundation/Foundation.h>
#interface NSBundle (Language)
+ (void)setLanguage:(NSString *)language;
#end
Implementation:
//
// NSBundle+Language.m
// ios_language_manager
//
// Created by Maxim Bilan on 1/10/15.
// Copyright (c) 2015 Maxim Bilan. All rights reserved.
//
#import "NSBundle+Language.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
static const char kBundleKey = 0;
#interface BundleEx : NSBundle
#end
#implementation BundleEx
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName
{
NSBundle *bundle = objc_getAssociatedObject(self, &kBundleKey);
if (bundle) {
return [bundle localizedStringForKey:key value:value table:tableName];
}
else {
return [super localizedStringForKey:key value:value table:tableName];
}
}
#end
#implementation NSBundle (Language)
+ (void)setLanguage:(NSString *)language
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
object_setClass([NSBundle mainBundle], [BundleEx class]);
});
BOOL isLanguageRTL = [self isLanguageRTL:language];
if (isLanguageRTL) {
if ([[[UIView alloc] init] respondsToSelector:#selector(setSemanticContentAttribute:)]) {
[[UIView appearance] setSemanticContentAttribute:
UISemanticContentAttributeForceRightToLeft];
}
}else {
if ([[[UIView alloc] init] respondsToSelector:#selector(setSemanticContentAttribute:)]) {
[[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceLeftToRight];
}
}
[[NSUserDefaults standardUserDefaults] setBool:isLanguageRTL forKey:#"AppleTextDirection"];
[[NSUserDefaults standardUserDefaults] setBool:isLanguageRTL forKey:#"NSForceRightToLeftWritingDirection"];
[[NSUserDefaults standardUserDefaults] synchronize];
id value = language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:#"lproj"]] : nil;
objc_setAssociatedObject([NSBundle mainBundle], &kBundleKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+ (BOOL)isLanguageRTL:(NSString *)languageCode
{
return ([NSLocale characterDirectionForLanguage:languageCode] == NSLocaleLanguageDirectionRightToLeft);
}
#end
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!)
First of all - this is bad idea and Apple recommend to use iOS selected language for localization.
But if you really need it you can make some small service for such purpose
enum LanguageName: String {
case undefined
case en
case es
case fr
case uk
case ru
case de
case pt
}
let DynamicLanguageServiceDidDetectLanguageSwitchNotificationKey = "DynamicLanguageServiceDidDetectLanguageSwitchNotificationKey"
func dynamicLocalizableString(_ key: String) -> String {
return LanguageService.service.dynamicLocalizedString(key)
}
class LanguageService {
private struct Defaults {
static let keyCurrentLanguage = "KeyCurrentLanguage"
}
static let service:LanguageService = LanguageService()
var languageCode: String {
get {
return language.rawValue
}
}
var currentLanguage:LanguageName {
get {
var currentLanguage = UserDefaults.roxy.object(forKey: Defaults.keyCurrentLanguage)
if currentLanguage == nil {
currentLanguage = Locale.preferredLanguages[0]
}
if var currentLanguage = currentLanguage as? String,
let lang = LanguageName(rawValue: currentLanguage.truncatedBy(by:2)) {
return lang
}
return LanguageName.en
}
}
var defaultLanguageForLearning:LanguageName {
get {
var language: LanguageName = .es
if currentLanguage == language {
language = .en
}
return language
}
}
func switchToLanguage(_ lang:LanguageName) {
language = lang
NotificationCenter.default.post(name: NSNotification.Name(rawValue: DynamicLanguageServiceDidDetectLanguageSwitchNotificationKey), object: nil)
}
func clearLanguages() {
UserDefaults.roxy.setValue(nil, forKey:Defaults.keyCurrentLanguage)
print(UserDefaults.roxy.synchronize())
}
private var localeBundle:Bundle?
fileprivate var language: LanguageName = LanguageName.en {
didSet {
let currentLanguage = language.rawValue
UserDefaults.roxy.setValue(currentLanguage, forKey:Defaults.keyCurrentLanguage)
UserDefaults.roxy.synchronize()
setLocaleWithLanguage(currentLanguage)
}
}
// MARK: - LifeCycle
private init() {
prepareDefaultLocaleBundle()
}
//MARK: - Private
fileprivate func dynamicLocalizedString(_ key: String) -> String {
var localizedString = key
if let bundle = localeBundle {
localizedString = NSLocalizedString(key, bundle: bundle, comment: "")
} else {
localizedString = NSLocalizedString(key, comment: "")
}
return localizedString
}
private func prepareDefaultLocaleBundle() {
var currentLanguage = UserDefaults.roxy.object(forKey: Defaults.keyCurrentLanguage)
if currentLanguage == nil {
currentLanguage = Locale.preferredLanguages[0]
}
if let currentLanguage = currentLanguage as? String {
updateCurrentLanguageWithName(currentLanguage)
}
}
private func updateCurrentLanguageWithName(_ languageName: String) {
if let lang = LanguageName(rawValue: languageName) {
language = lang
}
}
private func setLocaleWithLanguage(_ selectedLanguage: String) {
if let pathSelected = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj"),
let bundleSelected = Bundle(path: pathSelected) {
localeBundle = bundleSelected
} else if let pathDefault = Bundle.main.path(forResource: LanguageName.en.rawValue, ofType: "lproj"),
let bundleDefault = Bundle(path: pathDefault) {
localeBundle = bundleDefault
}
}
}
And than make rootViewControllerClass like:
import Foundation
protocol Localizable {
func localizeUI()
}
and
class LocalizableViewController: UIViewController, Localizable {
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.localizeUI), name: NSNotification.Name(rawValue:DynamicLanguageServiceDidDetectLanguageSwitchNotificationKey), object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
localizeUI()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
extension LocalizableViewController: Localizable {
// MARK: - Localizable
func localizeUI() {
fatalError("Must Override to provide inApp localization functionality")
}
}
Than inherit every controller from LocalizableViewController and implement localizeUI()
And instead of NSLocalizedString use dynamicLocalizableString like :
func localizeOnceUI() {
label.text = dynamicLocalizableString("keyFrom<"Localizable.strings">")
}
To switch language:
LanguageService.service.switchToLanguage(.en)
Also note - additional steps and modification of logic required if you want to dynamically localize your widgets or other app parts.
Here is my solution with String extension. Improved safety from #Das answer.
extension String {
var localized: String {
guard let path = Bundle.main.path(forResource: Locale.current.regionCode?.lowercased(), ofType: "lproj"), let bundle = Bundle(path: path) else {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
}
}
class ViewController: UIViewController {
#IBOutlet weak var resetOutlet: MyButton! {
didSet {
resetOutlet.setTitle("RESET".localized().uppercased(), for: .normal)
}
}`
}
extension String {
func localized(tableName: String = "Localizable") -> String {
if let languageCode = Locale.current.languageCode, let preferredLanguagesFirst = Locale.preferredLanguages.first?.prefix(2) {
if languageCode != preferredLanguagesFirst {
if let path = Bundle.main.path(forResource: "en", ofType: "lproj") {
let bundle = Bundle.init(path: path)
return NSLocalizedString(self, tableName: tableName, bundle: bundle!, value: self, comment: "")
}
}
}
return NSLocalizedString(self, tableName: tableName, value: self, comment: "")
}
}
How to support per-app language settings in your app: https://developer.apple.com/news/?id=u2cfuj88
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.
On iOS, add the following:
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
On macOS, direct people to System Preferences > Language & Region to add a per-language setting for your app.
Here is an updated answer for Swift 4
let language = "es" // replace with Locale code
guard let path = Bundle.main.path(forResource: language, ofType: "lproj") else {
return self
}
guard let bundle = Bundle(path: path) else {
return self
}
return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
Optimized code for mr.boyfox.
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: "")
}
}
Latest Swift syntax :
import Foundation
extension String {
func localized(lang:String) ->String {
let path = Bundle.main.path(forResource: lang, ofType: "lproj")
let bundle = Bundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "")
}
}
This is extended John Pang's solution, if you need to translate system Strings immediately (Back, Cancel, Done...):
private var kBundleKey: UInt8 = 0
class BundleEx: Bundle {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let bundle = objc_getAssociatedObject(self, &kBundleKey) {
return (bundle as! Bundle).localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
private var kBundleUIKitKey: UInt8 = 0
class BundleUIKitEx: Bundle {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let bundle = objc_getAssociatedObject(self, &kBundleUIKitKey) {
return (bundle as! Bundle).localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
static let once: Void = {
object_setClass(Bundle.main, type(of: BundleEx()))
object_setClass(Bundle(identifier:"com.apple.UIKit"), type(of: BundleUIKitEx()))
}()
class func setLanguage(_ language: String?) {
Bundle.once
let isLanguageRTL = Bundle.isLanguageRTL(language)
if (isLanguageRTL) {
UIView.appearance().semanticContentAttribute = .forceRightToLeft
} else {
UIView.appearance().semanticContentAttribute = .forceLeftToRight
}
UserDefaults.standard.set(isLanguageRTL, forKey: "AppleTextDirection")
UserDefaults.standard.set(isLanguageRTL, forKey: "NSForceRightToLeftWritingDirection")
UserDefaults.standard.synchronize()
let value = (language != nil ? Bundle.init(path: (Bundle.main.path(forResource: language, ofType: "lproj"))!) : nil)
objc_setAssociatedObject(Bundle.main, &kBundleKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if let uiKitBundle = Bundle(identifier: "com.apple.UIKit") {
var valueUIKit: Bundle? = nil
if let lang = language,
let path = uiKitBundle.path(forResource: lang, ofType: "lproj") {
valueUIKit = Bundle(path: path)
}
objc_setAssociatedObject(uiKitBundle, &kBundleUIKitKey, valueUIKit, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
class func isLanguageRTL(_ languageCode: String?) -> Bool {
return (languageCode != nil && Locale.characterDirection(forLanguage: languageCode!) == .rightToLeft)
}
}
If you want to translate system strings, you have to do the same for UIKit Bundle.
For localization during runtime can be used one of the next libraries:
Localize_Swift
or
LanguageManager-iOS
If you want to change localization according to Apple recommendations you can find the description in #Muhammad Asyraf's answer.
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.

How to obtain NSLocalizedString for specific locale, or how to get non-localized string

Is there a way of getting the localized string for a particular locale, other than the current locale?
Something like a reverse lookup, i.e. I have the localized value, I want to get to the base localized version or even the key.
myLocalizedString.getLocalizedString( locale : NSLocale )
I am well aware of the fact that this isn't super clean, but I don't need explanations as to why it's not a good idea. I'm just in a situation where it seems the least bad.
You can simply call this method by sending language code & key string.
func localizestring(for languageCode : String , keyString : String) -> String {
//language code like --> en/ef/da
let path = Bundle.main.path(forResource: languageCode, ofType: "lproj")
let bundle = Bundle(path: path!)
return NSLocalizedString(keyString , tableName: nil, bundle: bundle!, value: "", comment: "")
}
I solved this by extending String with this method in Swift. You can get localized string for any locale you have in your app this way.
extension String {
func localized(forLanguageCode lanCode: String) -> String {
guard
let bundlePath = Bundle.main.path(forResource: lanCode, ofType: "lproj"),
let bundle = Bundle(path: bundlePath)
else { return "" }
return NSLocalizedString(
self,
bundle: bundle,
value: " ",
comment: ""
)
}
}
Example (get localized string for ukrainian language when system language is english):
"settings_choose_language".localized(forLanguageCode: "uk")

Localization of app doesn't change language of cocoapods used within app

I am doing localization in my app. When I change the language it updates everywhere in my app except in pods that I have used. If I restart the app, pod's language is updated. Is there any way I can change the language of pods when I change the app's language.
You would need to swap the mainBundle of your application as soon as the user changes their language preferences inside the app.
The idea is to have a custom Bundle and extension:
var bundleKey: UInt8 = 0
class AnyLanguageBundle: Bundle {
override func localizedString(forKey key: String,
value: String?,
table tableName: String?) -> String {
guard let path = objc_getAssociatedObject(self, &bundleKey) as? String,
let bundle = Bundle(path: path) else {
return super.localizedString(forKey: key, value: value, table: tableName)
}
return bundle.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
class func setLanguage(_ language: String) {
defer {
object_setClass(Bundle.main, AnyLanguageBundle.self)
}
objc_setAssociatedObject(Bundle.main, &bundleKey, Bundle.main.path(forResource: language, ofType: "lproj"), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
And when you would like to update your app language:
func updateAppLanguageToIT() {
UserDefaults.standard.set("it", forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
// Update the language by swaping bundle
Bundle.setLanguage("it")
// Done to reintantiate the storyboards instantly
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
UIApplication.shared.keyWindow?.rootViewController = storyboard.instantiateInitialViewController()
}
A few references on how to achieve this are available already in some posts/github pages like those two:
https://github.com/maximbilan/Language-Manager-iOS
http://www.codebales.com/how-programmatically-change-app-language-without-restarting-app

Can I programatically change the Localizable.strings file on button click with out restarting the app

I am creating an app which uses 2 languages Arabic and English. I have managed to change the layout to RTL for Arabic and normal for English. Also I have added the Localizable.strings file for Arabic and English.
The app picks up English and normal layout shows when English is selected and picks up Arabic and RTL layout is shown when the app is started the first time or on every restart.
It does not pick up Arabic or English Localizable.strings file on runtime. Is there a way to do this.
You can change the current bundle you read from
extension String {
func localizedStr(language:String) -> String {
let path = Bundle.main.path(forResource: language, ofType: "lproj")
let bundleName = Bundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundleName!, value: "", comment: "")
}
}
In Action
see demo here local
Try following code...it
might help you to come out from your problem.
Step 1
extension String {
/// Returns the localized string value
public var localized: String {
if let bundleName:String = UserDefaults.standard.value(forKey: "USER_LANG") as? String {
let path = Bundle.main.path(forResource: bundleName, ofType: "lproj")
let bundle = Bundle.init(path: path!)
return localize(withBundle: bundle!)
} else {
return localize(withBundle: Bundle.main)
}
}
public func localize(withBundle bundle: Bundle) -> String
{
return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
}
}
Step 2
Store the bundle name on a button click in user default.
Example
// Strore Base bundleName when english button is clicked
UserDefaults.standard.set("Base", forKey: "USER_LANG")
// Strore fr(short from of french) bundleName when french button is clicked
UserDefaults.standard.set("fr", forKey: "USER_LANG")
Step 3
Usage
In String File
"lbl_name"="name";
// name will convert in French and English too
"lbl_name".localized
Thank you!!!

How can I change locale programmatically with Swift

I am making ios app on XCODE 6.3 by Swift.
And my app will have the choose language function like the image below
I already have storyboard for my local language.
But i can't find out how to change the localization programmatically off the app by the button.
Anyone know how to do it
Here's a way to change it on the fly with Swift, add an extension function to String:
extension String {
func localized(lang:String) ->String {
let path = NSBundle.mainBundle().pathForResource(lang, ofType: "lproj")
let bundle = NSBundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "")
}}
Swift 4:
extension String {
func localized(_ lang:String) ->String {
let path = Bundle.main.path(forResource: lang, ofType: "lproj")
let bundle = Bundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "")
}}
then assuming you have the regular Localizable.strings set up with lang_id.lproj ( e.g. en.lproj, de.lproj etc. ) you can use this anywhere you need:
var val = "MY_LOCALIZED_STRING".localized("de")
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.
Changing the language
UserDefaults.standard.set("en", forKey: "i18n_language")
Swift 4.2
In my case, if user change the language setting, I have to update 2 things at runtime.
1. Localizable.strings
2. Storyboard Localization
I make #John Pang code more swifty
BundleExtension.swift
import UIKit
private var bundleKey: UInt8 = 0
final class BundleExtension: Bundle {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
return (objc_getAssociatedObject(self, &bundleKey) as? Bundle)?.localizedString(forKey: key, value: value, table: tableName) ?? super.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
static let once: Void = { object_setClass(Bundle.main, type(of: BundleExtension())) }()
static func set(language: Language) {
Bundle.once
let isLanguageRTL = Locale.characterDirection(forLanguage: language.code) == .rightToLeft
UIView.appearance().semanticContentAttribute = isLanguageRTL == true ? .forceRightToLeft : .forceLeftToRight
UserDefaults.standard.set(isLanguageRTL, forKey: "AppleTe zxtDirection")
UserDefaults.standard.set(isLanguageRTL, forKey: "NSForceRightToLeftWritingDirection")
UserDefaults.standard.set([language.code], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
guard let path = Bundle.main.path(forResource: language.code, ofType: "lproj") else {
log(.error, "Failed to get a bundle path.")
return
}
objc_setAssociatedObject(Bundle.main, &bundleKey, Bundle(path: path), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
Language.swift
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
}
}
}
Use like this
var languages: [Language] = [.korean, .english(.us), .english(.uk), .english(.australian), .english(.canadian), .english(.indian),
.chinese(.simplified), .chinese(.traditional), .chinese(.hongKong),
.japanese]
Bundle.set(language: languages[indexPath.row].language)
"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
Usable code in Swift 4:
extension Bundle {
private static var bundle: Bundle!
public static func localizedBundle() -> Bundle! {
if bundle == nil {
let appLang = UserDefaults.standard.string(forKey: "app_lang") ?? "ru"
let path = Bundle.main.path(forResource: appLang, ofType: "lproj")
bundle = Bundle(path: path!)
}
return bundle;
}
public static func setLanguage(lang: String) {
UserDefaults.standard.set(lang, forKey: "app_lang")
let path = Bundle.main.path(forResource: lang, ofType: "lproj")
bundle = Bundle(path: path!)
}
}
and
extension String {
func localized() -> String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.localizedBundle(), value: "", comment: "")
}
func localizeWithFormat(arguments: CVarArg...) -> String{
return String(format: self.localized(), arguments: arguments)
}
}
call:
let localizedString = "enter".localized()
set new a locale(for example "ru"):
Bundle.setLanguage(lang: "ru")
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 UIKit
private var kBundleKey: UInt8 = 0
class BundleEx: Bundle {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let bundle = objc_getAssociatedObject(self, &kBundleKey) {
return (bundle as! Bundle).localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
static let once: Void = {
object_setClass(Bundle.main, type(of: BundleEx()))
}()
class func setLanguage(_ language: String?) {
Bundle.once
let isLanguageRTL = Bundle.isLanguageRTL(language)
if (isLanguageRTL) {
UIView.appearance().semanticContentAttribute = .forceRightToLeft
} else {
UIView.appearance().semanticContentAttribute = .forceLeftToRight
}
UserDefaults.standard.set(isLanguageRTL, forKey: "AppleTextDirection")
UserDefaults.standard.set(isLanguageRTL, forKey: "NSForceRightToLeftWritingDirection")
UserDefaults.standard.synchronize()
let value = (language != nil ? Bundle.init(path: (Bundle.main.path(forResource: language, ofType: "lproj"))!) : nil)
objc_setAssociatedObject(Bundle.main, &kBundleKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
class func isLanguageRTL(_ languageCode: String?) -> Bool {
return (languageCode != nil && Locale.characterDirection(forLanguage: languageCode!) == .rightToLeft)
}
}
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.
Swift 4
UserDefaults.standard.set(["es", "de", "it"], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
Swift 3
NSUserDefaults.standardUserDefaults().setObject(["es", "de", "it"], forKey: "AppleLanguages")
NSUserDefaults.standardUserDefaults().synchronize()
Source: here
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)
For Obj-C;
NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
[[UIApplication sharedApplication] openURL: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.
The solution linked by whiteagle actually works to switch the language on the fly. Here's the post.
I simplified the sample code on there to a single .h/.m that will change the language on-the-fly, in memory. I've shown how to call it from Swift 3.
Header:
//
// NSBundle+Language.h
// ios_language_manager
//
// Created by Maxim Bilan on 1/10/15.
// Copyright (c) 2015 Maxim Bilan. All rights reserved.
//
#import <Foundation/Foundation.h>
#interface NSBundle (Language)
+ (void)setLanguage:(NSString *)language;
#end
Implementation:
//
// NSBundle+Language.m
// ios_language_manager
//
// Created by Maxim Bilan on 1/10/15.
// Copyright (c) 2015 Maxim Bilan. All rights reserved.
//
#import "NSBundle+Language.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
static const char kBundleKey = 0;
#interface BundleEx : NSBundle
#end
#implementation BundleEx
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName
{
NSBundle *bundle = objc_getAssociatedObject(self, &kBundleKey);
if (bundle) {
return [bundle localizedStringForKey:key value:value table:tableName];
}
else {
return [super localizedStringForKey:key value:value table:tableName];
}
}
#end
#implementation NSBundle (Language)
+ (void)setLanguage:(NSString *)language
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
object_setClass([NSBundle mainBundle], [BundleEx class]);
});
BOOL isLanguageRTL = [self isLanguageRTL:language];
if (isLanguageRTL) {
if ([[[UIView alloc] init] respondsToSelector:#selector(setSemanticContentAttribute:)]) {
[[UIView appearance] setSemanticContentAttribute:
UISemanticContentAttributeForceRightToLeft];
}
}else {
if ([[[UIView alloc] init] respondsToSelector:#selector(setSemanticContentAttribute:)]) {
[[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceLeftToRight];
}
}
[[NSUserDefaults standardUserDefaults] setBool:isLanguageRTL forKey:#"AppleTextDirection"];
[[NSUserDefaults standardUserDefaults] setBool:isLanguageRTL forKey:#"NSForceRightToLeftWritingDirection"];
[[NSUserDefaults standardUserDefaults] synchronize];
id value = language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:#"lproj"]] : nil;
objc_setAssociatedObject([NSBundle mainBundle], &kBundleKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+ (BOOL)isLanguageRTL:(NSString *)languageCode
{
return ([NSLocale characterDirectionForLanguage:languageCode] == NSLocaleLanguageDirectionRightToLeft);
}
#end
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!)
First of all - this is bad idea and Apple recommend to use iOS selected language for localization.
But if you really need it you can make some small service for such purpose
enum LanguageName: String {
case undefined
case en
case es
case fr
case uk
case ru
case de
case pt
}
let DynamicLanguageServiceDidDetectLanguageSwitchNotificationKey = "DynamicLanguageServiceDidDetectLanguageSwitchNotificationKey"
func dynamicLocalizableString(_ key: String) -> String {
return LanguageService.service.dynamicLocalizedString(key)
}
class LanguageService {
private struct Defaults {
static let keyCurrentLanguage = "KeyCurrentLanguage"
}
static let service:LanguageService = LanguageService()
var languageCode: String {
get {
return language.rawValue
}
}
var currentLanguage:LanguageName {
get {
var currentLanguage = UserDefaults.roxy.object(forKey: Defaults.keyCurrentLanguage)
if currentLanguage == nil {
currentLanguage = Locale.preferredLanguages[0]
}
if var currentLanguage = currentLanguage as? String,
let lang = LanguageName(rawValue: currentLanguage.truncatedBy(by:2)) {
return lang
}
return LanguageName.en
}
}
var defaultLanguageForLearning:LanguageName {
get {
var language: LanguageName = .es
if currentLanguage == language {
language = .en
}
return language
}
}
func switchToLanguage(_ lang:LanguageName) {
language = lang
NotificationCenter.default.post(name: NSNotification.Name(rawValue: DynamicLanguageServiceDidDetectLanguageSwitchNotificationKey), object: nil)
}
func clearLanguages() {
UserDefaults.roxy.setValue(nil, forKey:Defaults.keyCurrentLanguage)
print(UserDefaults.roxy.synchronize())
}
private var localeBundle:Bundle?
fileprivate var language: LanguageName = LanguageName.en {
didSet {
let currentLanguage = language.rawValue
UserDefaults.roxy.setValue(currentLanguage, forKey:Defaults.keyCurrentLanguage)
UserDefaults.roxy.synchronize()
setLocaleWithLanguage(currentLanguage)
}
}
// MARK: - LifeCycle
private init() {
prepareDefaultLocaleBundle()
}
//MARK: - Private
fileprivate func dynamicLocalizedString(_ key: String) -> String {
var localizedString = key
if let bundle = localeBundle {
localizedString = NSLocalizedString(key, bundle: bundle, comment: "")
} else {
localizedString = NSLocalizedString(key, comment: "")
}
return localizedString
}
private func prepareDefaultLocaleBundle() {
var currentLanguage = UserDefaults.roxy.object(forKey: Defaults.keyCurrentLanguage)
if currentLanguage == nil {
currentLanguage = Locale.preferredLanguages[0]
}
if let currentLanguage = currentLanguage as? String {
updateCurrentLanguageWithName(currentLanguage)
}
}
private func updateCurrentLanguageWithName(_ languageName: String) {
if let lang = LanguageName(rawValue: languageName) {
language = lang
}
}
private func setLocaleWithLanguage(_ selectedLanguage: String) {
if let pathSelected = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj"),
let bundleSelected = Bundle(path: pathSelected) {
localeBundle = bundleSelected
} else if let pathDefault = Bundle.main.path(forResource: LanguageName.en.rawValue, ofType: "lproj"),
let bundleDefault = Bundle(path: pathDefault) {
localeBundle = bundleDefault
}
}
}
And than make rootViewControllerClass like:
import Foundation
protocol Localizable {
func localizeUI()
}
and
class LocalizableViewController: UIViewController, Localizable {
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.localizeUI), name: NSNotification.Name(rawValue:DynamicLanguageServiceDidDetectLanguageSwitchNotificationKey), object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
localizeUI()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
extension LocalizableViewController: Localizable {
// MARK: - Localizable
func localizeUI() {
fatalError("Must Override to provide inApp localization functionality")
}
}
Than inherit every controller from LocalizableViewController and implement localizeUI()
And instead of NSLocalizedString use dynamicLocalizableString like :
func localizeOnceUI() {
label.text = dynamicLocalizableString("keyFrom<"Localizable.strings">")
}
To switch language:
LanguageService.service.switchToLanguage(.en)
Also note - additional steps and modification of logic required if you want to dynamically localize your widgets or other app parts.
Here is my solution with String extension. Improved safety from #Das answer.
extension String {
var localized: String {
guard let path = Bundle.main.path(forResource: Locale.current.regionCode?.lowercased(), ofType: "lproj"), let bundle = Bundle(path: path) else {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}
return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
}
}
class ViewController: UIViewController {
#IBOutlet weak var resetOutlet: MyButton! {
didSet {
resetOutlet.setTitle("RESET".localized().uppercased(), for: .normal)
}
}`
}
extension String {
func localized(tableName: String = "Localizable") -> String {
if let languageCode = Locale.current.languageCode, let preferredLanguagesFirst = Locale.preferredLanguages.first?.prefix(2) {
if languageCode != preferredLanguagesFirst {
if let path = Bundle.main.path(forResource: "en", ofType: "lproj") {
let bundle = Bundle.init(path: path)
return NSLocalizedString(self, tableName: tableName, bundle: bundle!, value: self, comment: "")
}
}
}
return NSLocalizedString(self, tableName: tableName, value: self, comment: "")
}
}
How to support per-app language settings in your app: https://developer.apple.com/news/?id=u2cfuj88
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.
On iOS, add the following:
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
On macOS, direct people to System Preferences > Language & Region to add a per-language setting for your app.
Here is an updated answer for Swift 4
let language = "es" // replace with Locale code
guard let path = Bundle.main.path(forResource: language, ofType: "lproj") else {
return self
}
guard let bundle = Bundle(path: path) else {
return self
}
return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
Optimized code for mr.boyfox.
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: "")
}
}
Latest Swift syntax :
import Foundation
extension String {
func localized(lang:String) ->String {
let path = Bundle.main.path(forResource: lang, ofType: "lproj")
let bundle = Bundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "")
}
}
This is extended John Pang's solution, if you need to translate system Strings immediately (Back, Cancel, Done...):
private var kBundleKey: UInt8 = 0
class BundleEx: Bundle {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let bundle = objc_getAssociatedObject(self, &kBundleKey) {
return (bundle as! Bundle).localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
private var kBundleUIKitKey: UInt8 = 0
class BundleUIKitEx: Bundle {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let bundle = objc_getAssociatedObject(self, &kBundleUIKitKey) {
return (bundle as! Bundle).localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
static let once: Void = {
object_setClass(Bundle.main, type(of: BundleEx()))
object_setClass(Bundle(identifier:"com.apple.UIKit"), type(of: BundleUIKitEx()))
}()
class func setLanguage(_ language: String?) {
Bundle.once
let isLanguageRTL = Bundle.isLanguageRTL(language)
if (isLanguageRTL) {
UIView.appearance().semanticContentAttribute = .forceRightToLeft
} else {
UIView.appearance().semanticContentAttribute = .forceLeftToRight
}
UserDefaults.standard.set(isLanguageRTL, forKey: "AppleTextDirection")
UserDefaults.standard.set(isLanguageRTL, forKey: "NSForceRightToLeftWritingDirection")
UserDefaults.standard.synchronize()
let value = (language != nil ? Bundle.init(path: (Bundle.main.path(forResource: language, ofType: "lproj"))!) : nil)
objc_setAssociatedObject(Bundle.main, &kBundleKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if let uiKitBundle = Bundle(identifier: "com.apple.UIKit") {
var valueUIKit: Bundle? = nil
if let lang = language,
let path = uiKitBundle.path(forResource: lang, ofType: "lproj") {
valueUIKit = Bundle(path: path)
}
objc_setAssociatedObject(uiKitBundle, &kBundleUIKitKey, valueUIKit, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
class func isLanguageRTL(_ languageCode: String?) -> Bool {
return (languageCode != nil && Locale.characterDirection(forLanguage: languageCode!) == .rightToLeft)
}
}
If you want to translate system strings, you have to do the same for UIKit Bundle.
For localization during runtime can be used one of the next libraries:
Localize_Swift
or
LanguageManager-iOS
If you want to change localization according to Apple recommendations you can find the description in #Muhammad Asyraf's answer.
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.

Resources