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

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.

Related

Change UIView Appearance (LTR) When language is Hebrew without restarting app Swift4

In my App, I have only one language which is Hebrew. I have changed language on Appdelegate in didfinish launch. Now I have only one issue, On app first time launch did not change language and UIView appearance(LTR) When I run second time it have changed whole app language in Hebrew and UIView. I need language change on first time. I have checked too many answers but it has not working. Please let me know if anything for me.
import Foundation
import UIKit
class LocalizationSystem:NSObject {
var bundle: Bundle!
class var sharedInstance: LocalizationSystem {
struct Singleton {
static let instance: LocalizationSystem = LocalizationSystem()
}
return Singleton.instance
}
override init() {
super.init()
bundle = Bundle.main
}
func localizedStringForKey(key:String, comment:String) -> String {
return bundle.localizedString(forKey: key, value: comment, table: nil)
}
func localizedImagePathForImg(imagename:String, type:String) -> String {
guard let imagePath = bundle.path(forResource: imagename, ofType: type) else {
return ""
}
return imagePath
}
//MARK:- setLanguage
// Sets the desired language of the ones you have.
// If this function is not called it will use the default OS language.
// If the language does not exists y returns the default OS language.
func setLanguage(languageCode:String) {
var appleLanguages = UserDefaults.standard.object(forKey: "AppleLanguages") as! [String]
appleLanguages.remove(at: 0)
appleLanguages.insert(languageCode, at: 0)
UserDefaults.standard.set(appleLanguages, forKey: "AppleLanguages")
UserDefaults.standard.set(languageCode, forKey: "languageCode")
UserDefaults.standard.synchronize() //needs restrat
if let languageDirectoryPath = Bundle.main.path(forResource: languageCode, ofType: "lproj") {
bundle = Bundle.init(path: languageDirectoryPath)
} else {
resetLocalization()
}
}
//MARK:- resetLocalization
//Resets the localization system, so it uses the OS default language.
func resetLocalization() {
bundle = Bundle.main
}
//MARK:- getLanguage
// Just gets the current setted up language.
func getLanguage() -> String {
let appleLanguages = UserDefaults.standard.object(forKey: "AppleLanguages") as! [String]
let preferredLanguage = appleLanguages[0]
if preferredLanguage.contains("-") {
let array = preferredLanguage.components(separatedBy: "-")
return array[0]
}
return preferredLanguage
}
}
// Calling in Appdelegate
LocalizationSystem.sharedInstance.setLanguage(languageCode: "he")
UIView.appearance().semanticContentAttribute = .forceRightToLeft

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

how to change storyboard based on language at runtime in swift

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.

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.

iOS: Convert ISO Alpha 2 to Alpha 3 country code

Is it possible to convert ISO 3166 alpha-2 country code to alpha 3 country code in iOS, for instance DE to DEU?
Base on the answer Franck here is the code swift4 for loading the plist and then to convert 3 letters Country ISO code
The plist: Full conversion ISO 3166-1-Alpha2 to Alpha3
//
// CountryUtility.swift
//
import Foundation
struct CountryUtility {
static private func loadCountryListISO() -> Dictionary<String, String>? {
let pListFileURL = Bundle.main.url(forResource: "iso3166_2_to_iso3166_3", withExtension: "plist", subdirectory: "")
if let pListPath = pListFileURL?.path,
let pListData = FileManager.default.contents(atPath: pListPath) {
do {
let pListObject = try PropertyListSerialization.propertyList(from: pListData, options:PropertyListSerialization.ReadOptions(), format:nil)
guard let pListDict = pListObject as? Dictionary<String, String> else {
return nil
}
return pListDict
} catch {
print("Error reading regions plist file: \(error)")
return nil
}
}
return nil
}
/// Convertion ISO 3166-1-Alpha2 to Alpha3
/// Country code of 2 letters to 3 letters code
/// E.g: PT to PRT
static func getCountryCodeAlpha3(countryCodeAlpha2: String) -> String? {
guard let countryList = CountryUtility.loadCountryListISO() else {
return nil
}
if let countryCodeAlpha3 = countryList[countryCodeAlpha2]{
return countryCodeAlpha3
}
return nil
}
static func getLocalCountryCode() -> String?{
guard let countryCode = NSLocale.current.regionCode else { return nil }
return countryCode
}
/// This function will get full country name based on the phone Locale
/// E.g. Portugal
static func getLocalCountry() -> String?{
let countryLocale = NSLocale.current
guard let countryCode = countryLocale.regionCode else { return nil }
let country = (countryLocale as NSLocale).displayName(forKey: NSLocale.Key.countryCode, value: countryCode)
return country
}
}
To use you just need to:
if let countryCode = CountryUtility.getLocalCountryCode() {
if let alpha3 = CountryUtility.getCountryCodeAlpha3(countryCodeAlpha2: countryCode){
print(alpha3) ///result: PRT
}
}

Resources