Hi guys!
It’s been a LONG time since my last post – I was busy lately getting married to the lovely Einat 🙂
This time I wanted to tell you about how we added in-app localization to Any.DO
Since we added localization to Any.DO a few months ago, we got a lot of feedback asking us to add the option to change the language from inside the app.
As you all know, the common localization method is to simply use NSLocalizedString(key, comment) – a macro for [[NSBundle mainBundle] localizedStringForKey:(key) value:@”” table:nil].
This macro takes the translated string from the appropriate localized strings file. Behind the scenes, NSLocalizedString() uses the “AppleLanguages” key in NSUserDefaults to determine the user’s settings for preferred languages, and uses the first value in the returned array to decide which localized strings file to use.
Basically, this means that the language the app is translated to is decided by the user’s global settings.
When I first saw this feature request, I thought it’s pretty much a waste of time – since I never saw an app on the app store that allows users to change the language from inside the app. I thought that if they really wanted to change the language, they should just change their user settings.
This is obviously a pretty stupid reason. Your users usually knows what they really want, and you should try your best to offer them the best user experience to make your app simple and fun. After all, with almost 1M apps in the app store – a well polished user experience makes all the difference.
When I decided to add in-app localization, I searched stack overflow first (as you should always do). Most solutions were to simply replace the “AppleLanguages” key in the NSUserDefaults to replace the default language (see this post for example). The problem with this solution is that it forces the user to restart the app in order to work. Pretty lame.
I ended up implementing the in-app localizations this way:
Step 1
Add to your settings page, the option to easily change the application language. I think it’s very important to make this as simple and straight forward as possible.
Keep the value of the selected language in your userDefaults:
-(NSString *) applicationLanguage { if (currentAppLanguage) return currentAppLanguage; // The default language, if not changed from the settings menu, is the device's locale. currentAppLanguage = [[NSUserDefaults standardUserDefaults] valueForKey:@"configurationManager_applicationLanguage"]; if (!currentAppLanguage) { currentAppLanguage = [[NSLocale preferredLanguages] objectAtIndex:0]; self.applicationLanguage = currentAppLanguage; } return currentAppLanguage; } -(void) setApplicationLanguage:(NSString *)applicationLanguage { if (applicationLanguage != currentAppLanguage) { [[NSUserDefaults standardUserDefaults] setValue:applicationLanguage forKey:@"configurationManager_applicationLanguage"]; [[NSUserDefaults standardUserDefaults] synchronize]; currentAppLanguage = applicationLanguage; [[NSNotificationCenter defaultCenter] postNotificationName:kApplicationLanguageChanged object:nil]; } }
Note that I’m posting a notification called kApplicationLanguageChanged when the application language changes. I will explain this further in a second.
Step 2
To your global class (I have a global singelton class called Utils – If you don’t have such a class you can simply use your AppDelegate), add the following method:
-(NSString *) languageSelectedStringForKey:(NSString *)key { NSString *selectedLanguage = [ConfigurationManager instance].applicationLanguage; NSString *path= [[NSBundle mainBundle] pathForResource:selectedLanguage ofType:@"lproj"]; if (!path) { path = [[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"]; } NSBundle* languageBundle = [NSBundle bundleWithPath:path]; NSString* str=[languageBundle localizedStringForKey:key value:@"" table:nil]; return str; }
What this method does is simply finds the appropriate bundle according to your selected application language (note that I keep my selected applicationLanguage in a helper class called ConfigurationManager), and uses this bundle to call the localizedStringForKey method. This is the same method that is being called when you use NSLocalizedString().
Note that in this implementation, you search the bundle every time the method is called. This is actually a pretty lightweight function, but if performance is very important in your app – you can improve it by caching the selected bundle, and only change it when the language changes.
Now add a simple global accessor to replace NSLocalizedString:
#define AnyDOLocalizedString(key, comment) \ [[Utils instance] languageSelectedStringForKey:key]
Step 3
Now all you have to do is replace all the called to NSLocalizedString to your new method (AnyDOLocalizedString in our example).
To make sure that all your strings actually change when you change the application language you have two options:
1. Set all the texts in ViewWillAppear instead of ViewDidLoad – This is kind of a redundancy, but it’s the easiest and fastest way to make sure that the strings are always up to date
2. Listen to kApplicationLanguageChanged on your ViewControllers and reset the texts whenever the language changes:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadTexts) name:kApplicationLanguageChanged object:nil]; -(void) reloadTexts { self.myLabel.text = AnyDOLocalizedString(@"the text", nil); }
Thats it. It’s a pretty simple implementation that your users will highly appreciate.
You can check out Any.DO to see how it feels in a real app.