Search code examples
iosiphoneswiftinternationalizationnsuserdefaults

Changing language on the fly in swift


Now I know that apple does not recommend this.

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.

However, this is an application that changing the language on the fly makes sense, just trust me on that. I also know this question was asked here: Changing language on the fly, in running iOS, programmatically. This however is getting old and I was wondering if there are any newer, better, or easier ways to do this. Currently in my app, I have a language choosing screen. Clicking on of the buttons in this view calls the following function with the language the button is associated with:

 func changeLang(language: String) {

    if language != (currentLang as! String?)! {
        func handleCancel(alertView: UIAlertAction!)
        {

        }
        var alert = UIAlertController(title: NSLocalizedString("language", comment: "Language"), message: NSLocalizedString("languageWarning", comment: "Warn User of Language Change Different Than Defaults"), preferredStyle: UIAlertControllerStyle.Alert)

        alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler:handleCancel))
        alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.Default, handler:{ (UIAlertAction) in

            NSUserDefaults.standardUserDefaults().setObject([language], forKey: "AppleLanguages")
            NSUserDefaults.standardUserDefaults().synchronize()
            println(self.currentLang)

            let alert = UIAlertView()
            alert.title = NSLocalizedString("language", comment: "Sign In Failed")
            alert.message = NSLocalizedString("languageChangeNotification", comment: "Notify of language change")
            alert.addButtonWithTitle(NSLocalizedString("ok", comment: "Okay"))
            alert.show()

            self.performSegueWithIdentifier("welcome", sender: AnyObject?())


        }))
        self.presentViewController(alert, animated: true, completion: {
        })
    } else {
        self.performSegueWithIdentifier("welcome", sender: AnyObject?())
    }

}

Example:

@IBAction func english(sender: UIButton) {
        changeLang("en")

    }

If the user picks a language different than their own, they get a confirmation alert, and then are requested to restart there device. This is what I want to change. It appears that this section of NSUSerDefaults is not synchronized until the app restarts. Evidence:

let currentLang: AnyObject? = NSLocale.preferredLanguages()[0]
println(currentLang)
// Prints english
changeLang("zh-Hans")
println(currentLang)
// Prints english still until restart 

The current internationalization system apple has is great, and I plan on using it. However, how can I change the language on the fly, maybe by forcing an update on the NSUSerDefaults?

Edit: I recommend using this library to do this now. Best of luck!


Solution

  • Basically you have to teach you bundle how to switch languages by loading different bundle files.

    I translated my Objective-C code to Swift — with leaving the NSBundle category untouched.

    enter image description here

    The result is a view controller class that offers a languageDidChange() method for overriding.


    NSBundle+Language.h

    #import <Foundation/Foundation.h>
    
    @interface NSBundle (Language)
    +(void)setLanguage:(NSString*)language;
    
    @end
    

    NSBundle+Language.m

    #import "NSBundle+Language.h"
    #import <objc/runtime.h>
    
    static const char associatedLanguageBundle=0;
    
    @interface PrivateBundle : NSBundle
    @end
    
    @implementation PrivateBundle
    -(NSString*)localizedStringForKey:(NSString *)key
                                value:(NSString *)value
                                table:(NSString *)tableName
    {
        NSBundle* bundle=objc_getAssociatedObject(self, &associatedLanguageBundle);
        return bundle ? [bundle localizedStringForKey:key
                                                value:value
                                                table:tableName] : [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],[PrivateBundle class]);
        });
    
        objc_setAssociatedObject([NSBundle mainBundle], &associatedLanguageBundle, language ?
                                 [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:@"lproj"]] : nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    @end
    

    AppDelegate.swift

    import UIKit
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        var window: UIWindow?
    
    
        func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    
            NSNotificationCenter.defaultCenter().addObserver(self, selector: "languageWillChange:", name: "LANGUAGE_WILL_CHANGE", object: nil)
    
            let targetLang = NSUserDefaults.standardUserDefaults().objectForKey("selectedLanguage") as? String
    
            NSBundle.setLanguage((targetLang != nil) ? targetLang : "en")
            return true
        }
    
        func languageWillChange(notification:NSNotification){
            let targetLang = notification.object as! String
            NSUserDefaults.standardUserDefaults().setObject(targetLang, forKey: "selectedLanguage")
            NSBundle.setLanguage(targetLang)
            NSNotificationCenter.defaultCenter().postNotificationName("LANGUAGE_DID_CHANGE", object: targetLang)
        }    
    }
    

    BaseViewController.swift

    import UIKit
    
    
    
    class BaseViewController: UIViewController {
    
        @IBOutlet weak var englishButton: UIButton!
        @IBOutlet weak var spanishButton: UIButton!
    
        deinit{
            NSNotificationCenter.defaultCenter().removeObserver(self)
        }
        override func viewDidLoad() {
            super.viewDidLoad()
            NSNotificationCenter.defaultCenter().addObserver(self, selector: "languageDidChangeNotification:", name: "LANGUAGE_DID_CHANGE", object: nil)
            languageDidChange()
    
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
        @IBAction func switchLanguage(sender: UIButton) {
    
            var localeString:String?
            switch sender {
            case englishButton: localeString = "en"
            case spanishButton: localeString = "es"
            default: localeString = nil
            }
    
    
            if localeString != nil {
                NSNotificationCenter.defaultCenter().postNotificationName("LANGUAGE_WILL_CHANGE", object: localeString)
            }
        }
    
    
        func languageDidChangeNotification(notification:NSNotification){
            languageDidChange()
        }
    
        func languageDidChange(){
    
        }
    
    
    }
    

    ViewController.swift

    import UIKit
    
    class ViewController: BaseViewController {
    
        @IBOutlet weak var helloLabel: UILabel!
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func languageDidChange() {
            super.languageDidChange()
            self.helloLabel.text = NSLocalizedString("Hello", comment: "")
    
        }
    }
    

    instead of using subclasses of BaseViewController, your viewcontrollers could also post "LANGUAGE_WILL_CHANGE" and listen for "LANGUAGE_DID_CHANGE"

    I pushed the complete project here: ImmediateLanguageSwitchSwift