38

When I change the app used language independently on the device language it doesn't take effect until I close the app and restart it. How to not require app to be restarted for loading all nib files and .strings files again depending on the selected language?

I use this to change language at runtime:

NSArray* languages = [NSArray arrayWithObjects:@"ar", @"en", nil]; 
[[NSUserDefaults standardUserDefaults] setObject:languages forKey:@"AppleLanguages"];
Ahmed Said
  • 1,285
  • 5
  • 16
  • 28

5 Answers5

23

This works for me : Swift 4 :

Create a file named BundleExtension.swift and add the following code to it -

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

Now whenever you need to change the language call this method :

func languageButtonAction() {
    // This is done so that network calls now have the Accept-Language as "hi" (Using Alamofire) Check if you can remove these
    UserDefaults.standard.set(["hi"], forKey: "AppleLanguages")
    UserDefaults.standard.synchronize()
    
    // Update the language by swapping bundle
    Bundle.setLanguage("hi")
    
    // Done to reinstantiate the storyboards instantly
    let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
    UIApplication.shared.keyWindow?.rootViewController = storyboard.instantiateInitialViewController()
}
Ankit Kumar Gupta
  • 3,994
  • 4
  • 31
  • 54
11

I had a similar requirement for a Kiosk mode iPad app with tabbed navigation. Not only did the app need to support on-the-fly language changes, but had to do so knowing that most of the tabs were already loaded from the nibs since the app was only restarted (on average) about once a week when a new version was loaded.

I tried several suggestions to leverage the existing Apple localization mechanisms and they all had serious drawbacks, including wonky support in XCode 4.2 for localized nibs -- my IBoutlet connection variables would appear to be set correctly in IB, but at runtime they would often be null!?

I wound up implementing a class that mimicked the Apple NSLocalizedString class but which could handle runtime changes, and whenever a language change was made by a user my class posted a notification. Screens that needed localized strings (and images) to change declared a handleLocaleChange method, which was called at viewDidLoad, and whenever the LocaleChangedNotification was posted.

All of my buttons and graphics were designed to be language independent, although the title text and label text was typically updated in response to locale changes. If I had to change images, I could have done so in the handleLocaleChange methods for each screen, I suppose.

Here is the code. It includes some support for nib/bundle paths which I actually don't use in the final project.

MyLanguage.h // // MyLanguage.h // //

#import <Foundation/Foundation.h>

#define DEFAULT_DICTIONARY_FOR_STRINGS                      @""
#define ACCESSING_ALTERNATE_DICTIONARY_SETS_DEFAULT         1

#define LANGUAGE_ENGLISH_INT  0
#define LANGUAGE_SPANISH_INT  1
#define LANGUAGE_ENGLISH_SHORT_ID  @"en"
#define LANGUAGE_SPANISH_SHORT_ID  @"es"

#define LANGUAGE_CHANGED_NOTIFICATION   @"LANGUAGE_CHANGED"


@interface MyLanguage : NSObject
{
    NSString        *currentLanguage;    
    NSDictionary    *currentDictionary;
    NSBundle        *currentLanguageBundle;
}

+(void) setLanguage:(NSString *)languageName;


+(NSString *)stringFor:(NSString *)srcString forLanguage:(NSString *)languageName;
+(NSString *)stringFor:(NSString *)srcString;

+ (MyLanguage *)singleton;

@property (nonatomic, retain) NSBundle        *currentLanguageBundle;
@property (nonatomic, retain) NSString        *currentLanguage;    
@property (nonatomic, retain) NSDictionary    *currentDictionary;

@end

MyLanguage.m: // // MyLanguage.m

#import "MyLanguage.h"
#import "Valet.h"

#define GUI_STRING_FILE_POSTFIX   @"GUIStrings.plist"

@implementation MyLanguage

@synthesize currentLanguage;   
@synthesize currentDictionary;
@synthesize currentLanguageBundle;

+(NSDictionary *)getDictionaryNamed:(NSString *)languageName
{
    NSDictionary *results = nil;

    // for now, we store dictionaries in a PLIST with the same name.
    NSString *dictionaryPlistFile = [languageName stringByAppendingString:GUI_STRING_FILE_POSTFIX];

    NSString *plistBundlePath = [Valet getBundlePathForFileName:dictionaryPlistFile];

    if ( [[NSFileManager defaultManager] fileExistsAtPath:plistBundlePath] )
    {
        // read it into a dictionary
        NSDictionary *newDict = [NSDictionary dictionaryWithContentsOfFile:plistBundlePath]; 
        results = [newDict valueForKey:@"languageDictionary"];

    }// end if

    return results;
}

+(NSString *)stringFor:(NSString *)srcString forDictionary:(NSString *)languageName;
{
    MyLanguage *gsObject = [MyLanguage singleton];

    // if default dictionary matches the requested one, use it.
    if ([gsObject.currentLanguage isEqualToString:languageName])
    {
        // use default
        return [MyLanguage stringFor:srcString];
    }// end if
    else
    {
        // get the desired dictionary
        NSDictionary *newDict = [MyLanguage getDictionaryNamed:languageName];

        // default is not desired!
        if (ACCESSING_ALTERNATE_DICTIONARY_SETS_DEFAULT)
        {
            gsObject.currentDictionary = newDict;
            gsObject.currentLanguage = languageName;
            return [MyLanguage stringFor:srcString];
        }// end if
        else
        {
            // use current dictionary for translation.
            NSString *results = [gsObject.currentDictionary valueForKey:srcString];

            if (results == nil)
            {
                return srcString;
            }// end if

            return results;
        }
    }

}

+(void) setLanguage:(NSString *)languageName;
{
    MyLanguage *gsObject = [MyLanguage singleton];

    // for now, we store dictionaries in a PLIST with the same name.
    // get the desired dictionary
    NSDictionary *newDict = [MyLanguage getDictionaryNamed:languageName];

    gsObject.currentDictionary = newDict;
    gsObject.currentLanguage = languageName;   


    // now set up the bundle for nibs
    NSString *shortLanguageIdentifier = @"en";
    if ([languageName contains:@"spanish"] || [languageName contains:@"espanol"] || [languageName isEqualToString:LANGUAGE_SPANISH_SHORT_ID])
    {
        shortLanguageIdentifier = LANGUAGE_SPANISH_SHORT_ID;
    }// end if
    else
        shortLanguageIdentifier = LANGUAGE_ENGLISH_SHORT_ID;

//    NSArray *languages = [NSArray arrayWithObject:shortLanguageIdentifier];
//    [[NSUserDefaults standardUserDefaults] setObject:languages forKey:@"AppleLanguages"]; 
//    
    NSString *path= [[NSBundle mainBundle] pathForResource:shortLanguageIdentifier ofType:@"lproj"];
    NSBundle *languageBundle = [NSBundle bundleWithPath:path];
    gsObject.currentLanguageBundle = languageBundle;


    [[NSNotificationCenter defaultCenter] postNotificationName:LANGUAGE_CHANGED_NOTIFICATION object:nil];

}


+(NSString *)stringFor:(NSString *)srcString;
{
    MyLanguage *gsObject = [MyLanguage singleton];
    // default is to do nothing.
    if (gsObject.currentDictionary == nil || gsObject.currentLanguage == nil || [gsObject.currentLanguage isEqualToString:DEFAULT_DICTIONARY_FOR_STRINGS] )
    {
        return srcString;
    }// end if

    // use current dictionary for translation.
    NSString *results = [gsObject.currentDictionary valueForKey:srcString];

    if (results == nil)
    {
        return srcString;
    }// end if


    return results;
}



#pragma mark -
#pragma mark Singleton methods

static MyLanguage *mySharedSingleton = nil;

-(void) lateInit;
{

}

// PUT THIS METHOD DECLARATION INTO THE HEADER
+ (MyLanguage *)singleton;
{
    if (mySharedSingleton == nil) {
        mySharedSingleton = [[super allocWithZone:NULL] init];
        [mySharedSingleton lateInit];
    }
    return mySharedSingleton;
}

+ (id)allocWithZone:(NSZone *)zone
{    return [[self singleton] retain]; }

- (id)copyWithZone:(NSZone *)zone
{    return self; }

- (id)retain
{    return self; }

- (NSUInteger)retainCount //denotes an object that cannot be released
{    return NSUIntegerMax;  }

- (oneway void)release    //do nothing
{   }

- (id)autorelease
{     return self; }


@end
software evolved
  • 4,314
  • 35
  • 45
  • Just some notes (which may not be apparent) in case anyone wants to re-use my code: the locale PLIST files are the language short IDs followed by GUIStings.plist as in esGUIStrings.plist, and the root object in the plist is a dictionary named "languageDictionary". The entries in the dictionary consist of the strings to be translated as keys (such as "Unlock", and "Log in") and the values are the translated strings (such as "Desbloquear" and "Iniciar la sesion"). – software evolved Sep 12 '12 at 15:03
  • Valet is a helper class I created that acts as a higher level file system interface than NSFileManager. I tried to remove all references to it before posting the code, but it looks like I missed one. You can see, later in the code, a similar line that doesn't use Valet: NSString *path= [[NSBundle mainBundle] pathForResource:shortLanguageIdentifier ofType:@"lproj"]; – software evolved Nov 26 '12 at 23:35
8

Don't rely on strings that you have set in your nib file. Use your nib only for layout & setup of views. Any string that is shown to the user (button text, etc) needs to be in your Localizable.strings files, and when you load your nib you need to set the text on the corresponding view/control accordingly.

To get the bundle for the current language:

NSString *path = [[NSBundle mainBundle] pathForResource:currentLanguage ofType:@"lproj"];
if (path) {
    NSBundle *localeBundle = [NSBundle bundleWithPath:path];
}

And to use the bundle to obtain your localized strings:

NSLocalizedStringFromTableInBundle(stringThatNeedsToBeLocalized, nil, localeBundle, nil);

Also for date formatting, you might want to look into

[NSDateFormatter dateFormatFromTemplate:@"HH:mm:ss"" options:0 locale:locale];

To use that you will need to create a NSLocale for the corresponding language/country which you wish to use.

mamills
  • 1,786
  • 1
  • 15
  • 15
  • But what about the localized images in xib files? example: buttons images. And what about the different layouts and label sizes in xib files for different localizations? – Ahmed Said Feb 23 '12 at 18:16
  • 1
    See http://stackoverflow.com/questions/3787751/loading-a-localized-uiimage for an example on loading images. – mamills Feb 24 '12 at 15:46
  • You may need to dynamically size your labels, etc. For example, you might need to use -[NSString sizeWithFont: constrainedToSize: lineBreakMode:] to determine the height (or width) needed for certain text and then set the frame accordingly. – mamills Feb 24 '12 at 15:58
  • All that being said, using a different language/locale than that which the device uses isn't simple. Even if you do all of the above, [NSError localizedErrorDescription] will return text according the device settings (or perhaps according to the NSUserDefaults' "AppleLanguages"). But from what I've seen from other questions & answers you must set that in your main before starting the UIApplication and so you cannot change it dynamically as your app runs without having your app restart. – mamills Feb 24 '12 at 15:59
7

Heres what I did. I guess the trick was to use NSLocalizedStringFromTableInBundle instead of NSLocalizedString.

For all strings, use this

someLabel.text = NSLocalizedStringFromTableInBundle(@"Your String to be localized, %@",nil,self.localeBundle,@"some context for translators");

To change language, run this code

    NSString * language = @"zh-Hans"; //or whatever language you want
    NSString *path = [[NSBundle mainBundle] pathForResource:language ofType:@"lproj"];
    if (path) {
        self.localeBundle = [NSBundle bundleWithPath:path];
    }
    else {
        self.localeBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"] ];
    }

After this, you will likely want to call whatever update code to update the strings to the new languages, for e.g. run this again

someLabel.text = NSLocalizedStringFromTableInBundle(@"Your String to be localized, %@",nil,self.localeBundle,@"some context for translators");

Thats all. No need restart app. Compatible with system settings as well (if you set a language through iOS settings, it will work too). No need external library. No need jailbreak. And it works with genstrings too.

Of course, you should still do the usual for your app settings to persist:

[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObjects:@"zh-Hans", nil] forKey:@"AppleLanguages"];
[[NSUserDefaults standardUserDefaults] synchronize];

(and do a check in your viewDidLoad or something)

NSString * language = [[NSLocale preferredLanguages] objectAtIndex:0];
    NSString *path = [[NSBundle mainBundle] pathForResource:language ofType:@"lproj"];
    if (path) {
        self.localeBundle = [NSBundle bundleWithPath:path];
    }
    else {
        self.localeBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"] ];
    }
Pnar Sbi Wer
  • 468
  • 6
  • 9
2

You should create your own macro similar to NSLocalizedString but bases the bundle it chooses a string from on a NSUserDefaults value you set (i.e. don't worry about what the value of apples language defaults value is)

When you change the language you should send out a notification, which view controllers, views etc should listen for and refresh themselves

wattson12
  • 11,176
  • 2
  • 32
  • 34