I would like to tell you about an interesting experience gained in the process of developing a free so far currency converter, my second application in the Finance category. The first, Money iQ, was written while working in a small company and even managed to visit the 1st place of the Russian App Store. I will post a small dev story about creating an application a little later in another blog, if it is interesting, and in this article I would like to dwell on such a problem as an instant language change inside the application.
Actually, the problem.
Probably, many had to deal with multilingual applications. I'm talking not only about applications for iOS, but in general about applications that support multiple languages. In the settings, there is a “Language / Language / Idioma” item, which allows you to set the language that the user needs.
This option works differently in different situations. In some applications, you have to restart them to install a new language. In some, everything happens instantly. How to implement the second approach when writing applications for iOS, in the article and will be discussed.
What does Apple offer?
Strictly speaking, Apple advocates that the application use the locale that is set to default on the phone. This is very reasonable, since the Russian person will most likely put the Russian language, and he will want to see applications in Russian.
')
However, this does not always work. In some applications, translation is far from ideal - words do not fit on buttons, or look translated with the help of an undying Prompt, or just annoyed that they translated the text, but did not localize the pictures. I want to see the application whole, put the English language and use it at your pleasure. Some provide such an opportunity, and I propose to find out exactly how they do it.
How the application sets the locale.
It's simple. There is such a thing as NSBundle - a set of localized application resources. If the application contains a directory like ru.lproj and the phone locale is set to ru_RU, then let's say a call
[ [ NSBundle mainBundle ] loadNibNamed : @ "xib_name" ... ]
At first, it will try to find the corresponding resource in the ru.lproj directory, and if it did not work out, it will return the default one, which is in the root.
Further. Applications that support multiple languages are more likely to use NSLocalizedString. This construct - NSLocalizedString (@ "string", @ "comment") unfolds into
[ [ NSBundle mainBundle ] localizedStringForKey : @ "string" value : @ "" table : nil ]
What to do if you want to change the locale to non-default? A relatively popular way to solve this problem is after the user chooses a language to kill the application, and on a subsequent launch, replace the default locale from which NSBundle will load resources. Something like that:
main.m:
[ [ NSUserDefaults standardUserDefaults ] setObject : [ NSArray arrayWithObjects : @ "en_US" , nil ]
forKey : @ "AppleLanguages" ] ;
[ [ NSUserDefaults standardUserDefaults ] synchronize ] ;
@autoreleasepool {
return UIApplicationMain ( argc, argv, nil , NSStringFromClass ( [ YourApp class ] ) ) ;
}
Hereinafter, MA is a prefix, decrypted as MyApp, there is no secret meaning :)The decision is not the worst, because as a result, the locale changes to the stated one, but what happens is not very user friendly - production costs.
However, I increasingly notice applications in which the locale changes without restarting the application, such as in the excellent application from Booking.com (not an advertisement at all - the application is really very good). At some point, I set out to find out how it all works.
Preparatory work
First stage.
I will not dwell on the preparation for the localization of the application, many
articles have been written on this topic. I will only emphasize that your life will become much easier if at the very beginning you set the default application locale correctly and write
NSLocalizedString ( @ "string" , @ "comment" )
instead of simple
@ "string"
For those who do not know, the second parameter in NSLocalizedString is a comment that is automatically added to the new localization file generated by the genstrings command. Very helpful .
Second phase.
To do something so that when calling a localization macro, it would be used not a mainBundle, but a kind of “custom” bundle, which contains the localization resources we specified.
To do this, create a new singleton MALocalizationSystem (the implementation of the singleton on objc will leave google and the dispatch_once;) trendy now;) to which we add the methods:
+ ( MALocalizationSystem * ) sharedLocalizationSystem;
- ( NSString * ) localizedStringForKey : ( NSString * ) key value : ( NSString * ) comment;
- ( void ) setLanguage : ( NSString * ) language;
- ( NSString * ) getLanguage;
The implementation of the methods is as simple as slippers:
static MALocalizationSystem * _sharedLocalizationSystem = nil ; // singleton instance
static NSBundle * bundle = nil ; // current bundle. Initializing with the [NSBundle mainBundle] value in the init method
static NSString * _currentLanguage = nil ; // current language
- ( NSString * ) localizedStringForKey : ( NSString * ) key value : ( NSString * ) comment
{
return [ bundle localizedStringForKey : key value : comment table : nil ] ;
}
- ( void ) setLanguage : ( NSString * ) lang
{
if ( _currentLanguage && [ lang isEqualToString : _currentLanguage ] )
{
return ;
}
NSString * path = [ [ NSBundle mainBundle ] pathForResource : lang ofType : @ "lproj" ] ;
_currentLanguage = lang;
if ( path == nil )
{
[self resetLocalization ] ; // localization files were not found - reset _currentLanguage to nil and bundle to [NSBundle mainBundle]
}
else
{
bundle = [ NSBundle bundleWithPath : path ] ;
}
// here you can optionally send a notification about the successful change of locale.
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kLocalizationChangedNotification object : nil ] ;
}
- ( NSString * ) getLanguage
{
if ( ! _currentLanguage )
{
NSArray * languages = [ [ NSUserDefaults standardUserDefaults ] objectForKey : @ "AppleLanguages" ] ;
_currentLanguage = [ languages objectAtIndex : 0 ] ;
NSString * path = [ [ NSBundle mainBundle ] pathForResource : _currentLanguage ofType : @ "lproj" ] ;
if ( path == nil )
{
[ self resetLocalization ] ;
_currentLanguage = @ "en" ; // default language for our application.
}
}
return _currentLanguage;
}
And we define several macros for convenience:
#define MALocalizedString (key, comment)
[ [ MALocalizationSystem sharedLocalizationSystem ] localizedStringForKey : ( key ) value : ( comment ) ]
#define MALocalizationSetLanguage (language)
[ [ MALocalizationSystem sharedLocalizationSystem ] setLanguage : ( language ) ]
#define MALocalizationGetLanguage
[ [ MALocalizationSystem sharedLocalizationSystem ] getLanguage ]
With translation everything is more or less clear - we set the language using MALocalizationSetLanguage (“eo”), and everywhere we used MALocalizedString instead of NSLocalizedString, the installed language will be used. What about the resources: pictures, for example, and other files? And here begins ...
The third stage.
Apple has taken care of those who want to download the resource from a specific localization folder. Suppose if you want to load a list of currency names from an xml file, then this is usually done as follows:
NSString * pathToFile = [ [ NSBundle mainBundle ] pathForResource : @ "currencyNames"
ofType : @ "xml" ] ;
cachedCurrencyNames = [ NSMutableArray arrayWithContentsOfFile : pathToFile ] ;
But there is another way:
NSString * pathToFile = [ [ NSBundle mainBundle ] pathForResource : @ "currencyNames"
ofType : @ "xml"
inDirectory : nil
forLocalization : @ "ru" ] ;
cachedCurrencyCodes = [ NSMutableArray arrayWithContentsOfFile : pathToFile ] ;
Catch? :)
We summarize:
- instead of NSLocalizedString in the code, we use our macro MALocalizedString - it is better :)
- when loading a localized resource, you should do it with the indication of the current language: MALocalizationGetLanguage
- when changing user language, call MALocalizationSetLanguage
It remains only for each view controller to subscribe to the kLocalizationChangedNotification event and refresh localized resources / tags / pictures. To do this, it is convenient to collect all this stuff into one or several methods and call it (them) during awakeFromNib, as well as upon receipt of this very kind kLocalizationChangedNotification notification.
Instead of a conclusion.
I do not want iOS adherents to understand me wrong - I am impressed by Apple’s approach to minimizing user actions for convenient use of the application. At the same time, I do not consider that what I described above somehow gets out of this scheme. This is a normal approach when an application chooses a system language by default, after which the user in the settings is given the opportunity to change it "without noise and dust" (c).
Thanks for reading!
References
The basis was taken and slightly rewritten / added / corrected code
from here .