How to force NSLocalizedString to use a specific language

Viewed 157021

On iPhone NSLocalizedString returns the string in the language of the iPhone. Is it possible to force NSLocalizedString to use a specific language to have the app in a different language than the device ?

28 Answers

I had the same problem recently and I didn't want to start and patch my entire NSLocalizedString nor force the app to restart for the new language to work. I wanted everything to work as-is.

My solution was to dynamically change the main bundle's class and load the appropriate bundle there:

Header file

@interface NSBundle (Language)
+(void)setLanguage:(NSString*)language;
@end

Implementation

#import <objc/runtime.h>

static const char _bundle=0;

@interface BundleEx : NSBundle
@end

@implementation BundleEx
-(NSString*)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName
{
    NSBundle* bundle=objc_getAssociatedObject(self, &_bundle);
    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],[BundleEx class]);
    });
    objc_setAssociatedObject([NSBundle mainBundle], &_bundle, language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:@"lproj"]] : nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

So basically, when your app starts and before you load your first controller, simply call:

[NSBundle setLanguage:@"en"];

When your user changes his preferred language in your setting screen, simply call it again:

[NSBundle setLanguage:@"fr"];

To reset back to system defaults, simply pass nil:

[NSBundle setLanguage:nil];

Enjoy...

For those who need a Swift version:

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

I like best Mauro Delrio's method. I also have added the following in my Project_Prefix.pch

#import "Language.h"    
#define MyLocalizedString(key, alt) [Language get:key alter:alt]

So if you ever want to use the standard method (that uses NSLocalizedString) you can make a quick syntax substitution in all files.

In swift 4, I have solved it without needing to restart or use libraries.

After trying many options, I found this function, where you pass the stringToLocalize (of Localizable.String, the strings file) that you want to translate, and the language in which you want to translate it, and what it returns is the value for that String that you have in Strings file:

func localizeString (stringToLocalize: String, language: String) -> String
{
    let path = Bundle.main.path (forResource: language, ofType: "lproj")
    let languageBundle = Bundle (path: path!)
    return languageBundle! .localizedString (forKey: stringToLocalize, value: "", table: nil)
}

Taking into account this function, I created this function in a Swift file:

struct CustomLanguage {
    
    func createBundlePath () -> Bundle {
        let selectedLanguage = //recover the language chosen by the user (in my case, from UserDefaults)
        let path = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj")
        return Bundle(path: path!)!
    }
}

To access from the whole app, and in each string of the rest of ViewControllers, instead of putting:

NSLocalizedString ("StringToLocalize", comment: “")

I have replaced it with

let customLang = CustomLanguage() //declare at top
let bundleLanguage = customLang.createBundle()

NSLocalizedString("StringToLocalize", tableName: nil, bundle: bundleLanguage, value: "", comment: “”) //use in each String

I do not know if it's the best way, but I found it very simple, and it works for me, I hope it helps you!

For Swift you can override the main.swift file and set the UserDefaults string there before you app runs. This way you do not have to restart the App to see the desired effect.

import Foundation
import UIKit

// Your initialisation code here
let langCultureCode: String = "LANGUAGE_CODE"

UserDefaults.standard.set([langCultureCode], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()

UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))

paired together with the removal of @UIApplicationMain in your AppDelegate.swift file.

For me, Test-Plan user, specifying -AppleLanguages (en_BZ) (English Belize) in the TestPlan's Arguments Passed On Launch is working.

Since we use en_GB and en_BZ, iOS always preferred en_GB. Even so when I switched the devices language to EN and the region to BZ and when setting these values in the Test Plans Application Language and Application Region settings . Nothing helped except the good old arguments approach above :)

enter image description here

Related