How can I change locale programmatically with Swift
Asked Answered
P

19

100

I am making ios app on XCODE 6.3 by Swift. And my app will have the choose language function like the image below

enter image description here

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

Photoconduction answered 1/5, 2015 at 10:55 Comment(5)
https://mcmap.net/q/111908/-manual-language-selection-in-an-ios-app-iphone-and-ipad OR https://mcmap.net/q/111908/-manual-language-selection-in-an-ios-app-iphone-and-ipadCrank
If you could solve it, please post the answer hereOringa
For change language "on the fly" you can use cocoapods.org/pods/L10n-swift.Moguel
<p>You can change global NSLocale.current</p> medium.com/@konradpiekos93/…Dated
Accepted and all other comments are buggy work arounds. It should be implemented in this way: https://mcmap.net/q/110464/-how-can-i-change-locale-programmatically-with-swiftPseudohermaphrodite
K
107

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")
Karnes answered 31/7, 2015 at 10:42 Comment(5)
The swift 3 version : 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: "") }Koblenz
Congrats, this is by far the best solution I've come across. I just made a modification in order to be able to change the language dynamically. I'll post it as an answer.Illusive
But how to reload a storyboard when you change the language? Currently when I change the language then the Strings with .localized() work but the storyboard doesn't reflect into selected language.Bedspring
Hi Hardik, In this case your storyboard labels will need to be set from code and you should run the code every time your language changesKarnes
This answer doesn't answer the question, which was about a storyboard in a local language. It is just a sugar to translate a String.Tutelary
I
31

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")
Illusive answered 14/12, 2016 at 10:36 Comment(4)
but how to reload a storyboard when you change the language? Currently when I change the language then the Strings with .localized() work but the storyboard does not change at all.Bedspring
I faced a like problem. My work around is simply to change the language in a view controller called 'Settings', then close that view and voila! all my labels and buttons change language on the fly. I'm working on optimizing it and if I find a way I'll post a solution.Cere
Yep @PhilipBorges that's about what happens on my app too, though I have some other strings in the View Controller where I change the language. I just call an updateStrings() method where I just reset those strings. Hardik, I don't think this is the right solution if you're looking to load a whole other storyboard when you change the language. Out of curiosity, why do you need to load another storyboard in the first place ? Seems like a hassle.Illusive
You shouldn’t use synchronize(). Apple docs literally states that everywhere (at least for iOS 10 and 11): developer.apple.com/documentation/foundation/userdefaults/…Hereupon
P
28

Swift 4.2

In my case, if user change the language setting, I have to update 2 things at runtime.

1. Localizable.strings

Localizable strings

2. Storyboard Localization

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)

Select language screen


"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
Protoplast answered 23/11, 2018 at 8:4 Comment(1)
This answer should be marked as correct!!!Hasen
B
19

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")

New way on Xcode 15

Localize your app with String Catalog

Localizing your app is a big part of making it more inclusive and available to people in other languages.

Previously, if you wanted to localize your app, you had to maintain strings and stringsdict files yourself. It was a lot of manual work that often resulted in missing localized content.

Apple has made it easier for developers to localize our apps, starting with Xcode 15 and String Catalogs.

Official documentation

Example Source

Source from

enter image description here

Bibliolatry answered 7/11, 2018 at 9:51 Comment(6)
Does this method change strings on the fly when change language?Cheyennecheyne
Yes, if you want to change language just for your app inside in your app at runtime, it is not related to device OS languageBibliolatry
Hi, I cannot make this work on the fly. Text is updated if view was reloaded, but for current controller is stays the same. Any suggestions how to fix this?Physiotherapy
If you want view text elements will automatic update you should change device language, if you want change them on the fly you should change they manually(update all text with new a locale, move all ui elements which is use text to translate to common function in controller and when locale will change pass to controller notification and call to that common function again)Bibliolatry
And how do you handle language change?Blubbery
Awesome answer. But I would make the bundle optional and use Bundle.main in cases the language isn't there.Trifoliate
P
12

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)
    }

}
Pinnule answered 5/12, 2017 at 9:28 Comment(1)
is working perfect but crash on when you change on english version. I use objective c code it works perfectly on both case.Patinous
I
10

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.

Indictable answered 4/2, 2016 at 18:24 Comment(8)
Can you put here some link or the code of Swift variant ?Lyrebird
@Indictable does this work for the changing of the language direction of the project as well? Meaning that if i want to change to a RTL language, it will now flip all UI elements and constraints as is suppose to?Kippy
@royherma, not sure, but it should, as it changes whole localization settings.Indictable
@rPragma, I did it with Obj-C category definition, called from the swift code.Indictable
Cool thanks! I actually found a different solution for when you just want to force localization on an app before all UI elements are loaded (for swift), so if anyone winds up here and is looking for something like that, check this out https://mcmap.net/q/110467/-setting-quot-applelanguages-quot-doesn-39-t-change-app-languageKippy
Wow, this actually works to change the language on-the-fly. I will post an answer with a simplified class that can be called from Swift.Googins
Hi friends, how can implement the #2 method in Xamarin iOS? object_setClass, objc_setAssociatedObject methods, aren't in the framework =/Driscoll
Just implement obj-c category and then call the setLanguage method from XamarinIndictable
W
8

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

Wellwisher answered 13/11, 2015 at 3:44 Comment(2)
Thx! Worked for me to force set app language on start, added those lines to AppDelegate.Kelp
But the problem is that this changes the app language when it is relaunched. Unless the language won't change. It is discussed in the the answer where you are referring. https://mcmap.net/q/108237/-how-to-force-nslocalizedstring-to-use-a-specific-languageUnteach
C
7

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.

Coleorhiza answered 27/4, 2020 at 15:21 Comment(3)
Accepted and all other comments are buggy work around. It should be implemented in this way.Pseudohermaphrodite
This is likely the best answer ! ThanksSoosoochow
I would not agree with "best solution". If user has not set any preffered languages in iOS Settings, then this setting is not visible. So other solutions is a good workaround if you want to provide the changes all the time.Waltz
G
3

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!)

Googins answered 20/3, 2017 at 16:50 Comment(2)
Looks good, except I'm using NSLocalizedStringWithDefaultValue and not adding my localized text into a storyboard :(Audie
I posted a Swift version for Swift-lover here. Thanks Jeremy for simplifying the code.Pinnule
H
3

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.

Hohenzollern answered 3/10, 2020 at 4:55 Comment(0)
S
2

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.

Stacystadholder answered 12/7, 2017 at 10:47 Comment(2)
Hi! Could you please point to Apple’s recommendation you mention (to use OS language instead of custom switcher)? I am of an opinion that if you state something like this, you should always provide the link to the source at least.Hereupon
sorry - I missed u'r question. a lot of time is passed, but here u go developer.apple.com/forums/thread/… and another that points to same post - developer.apple.com/forums/thread/…Stacystadholder
C
2

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: "")
  }
}
Clemens answered 8/11, 2017 at 10:9 Comment(0)
E
2

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: "")
Ernieernst answered 9/12, 2017 at 15:7 Comment(0)
S
2
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: "")
}
}
Sweatt answered 26/2, 2019 at 22:9 Comment(0)
C
1

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: "")
 } 
}
Curare answered 25/2, 2021 at 14:14 Comment(0)
A
0

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: "")
    }
}
Aqua answered 27/10, 2017 at 19:37 Comment(0)
H
0

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.

Hermaphrodite answered 27/2, 2019 at 10:4 Comment(0)
D
0

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.

Dahomey answered 12/5, 2021 at 9:48 Comment(0)
S
0

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.

Surbeck answered 3/11, 2021 at 15:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.