Preferred languages and iOS 9
In iOS 9 Apple introduced some changes at how NSLocale +preferredLanguages returns languages. It returns strings like en-US, ru-RU instead of en, ru as it used to. Some (looks like a lot) of developers got confused by that, they reported that behavior as a bug, it even broke some codebases. The sad truth is that if this change breaks your codebase, you used it wrong making false assumptions.
As I’ve noted in my previous article, +preferredLanguages returns language IDs. In iOS and OS X Language ID is a combination of:
- Required language designator, like
enorru. - Optional script designator, like
HansorHant. - Optional region designator like
USorRUorGB.
joined by hyphen -. These are valid language IDs: en, ru, en-US, en-RU, ru-RU, zh-Hans, zh-Hans-US, zh.
Even before iOS 9
+[NSLocale preferredLanguages]always returned list of validlanguage IDs, the only thing changed is that since iOS 9 they are most likely include region designator, for exampleen-US,ru-RUvsen,ru.
That is, if you expected it to return not language IDs but language designator-s such as en or ru (which are at the same time wide range language IDs without region designator specified), you, probably, did it wrong even before iOS 9.
To work around this issue or make some fast fixes you may use +componentsFromLocaleIdentifier: from NSLocale, here is an example of how it could be done:
NSString *currentLocaleID = [NSLocale currentLocale].localeIdentifier;
NSArray *preferredLanguages = [NSLocale preferredLanguages];
NSLog(@"[NSLocale currentLocale] = %@", currentLocaleID);
NSLog(@"[NSLocale preferredLanguages] = %@", preferredLanguages);
// extracted language designators will go here
NSMutableArray *extractedLanguageDesignators = [NSMutableArray array];
for(NSString *languageID in preferredLanguages)
{
// extract components
NSDictionary *components =
[NSLocale componentsFromLocaleIdentifier:languageID];
// get language designator
NSString *languageDesignator = components[NSLocaleLanguageCode];
// it will never be nil for a valid language-id, but i'm paranoid
if(languageDesignator != nil)
{
[extractedLanguageDesignators addObject:languageDesignator];
}
}
NSLog(@"extracted language designators: %@", extractedLanguageDesignators);
And that what it returns with preferred languages set to Russian, Africaans, Chinese (Simplified), Italian, English and region set to United States (region language left automatic, i.e. Russian in this case):
// iOS 9
> [NSLocale currentLocale] = ru_US
> [NSLocale preferredLanguages] = (
"ru-US",
"af-US",
"zh-Hans-US",
"it-US",
"en-US"
)
> extracted language designators: (
ru,
af,
zh, <----- see, it misses script!
it,
en
)
// iOS 8
> [NSLocale currentLocale] = ru_US
> [NSLocale preferredLanguages] = (
ru,
af,
"zh-Hans",
it,
en
)
> extracted language designators: (
ru,
af,
zh, <--- yepp, no script again
it,
en
)
Last one is what you wanted, right? Wrong. Last one is a list of language designators, not language IDs. For example zh-Hans got truncated to zh, which is pretty huge loss of semantics. Of course you can crutch that too and make use of other keys:
NSLog(@"zh-Hans-US components: %@",
[NSLocale componentsFromLocaleIdentifier:@"zh-Hans-US"]);
// zh-Hans-US components: {
// kCFLocaleCountryCodeKey = US;
// kCFLocaleLanguageCodeKey = zh;
// kCFLocaleScriptCodeKey = Hans;
// }
But that is, probably, a crutch again. The point is that it is conceptually better to rely on language IDs than just language designators, API was built with that in mind. Unless you have a great reason or specific task, you really should stick with NSBundle API and let it decide which localization is the best fit.
This changes were made to enhance language fallback logic. You can read more on that here: Language Identifiers in iOS 9.
Tweet