Мне бы хотелось рассказать об интересном опыте, приобретенном в процессе разработки приложения Smart Coin (бесплатный пока что конвертер валют), моего второго приложения в категории Finance. Первое, Money iQ, было написано во время работы в небольшой компании и даже успело побывать на 1м месте российского App Store. Небольшую dev story о создании Smart Coin я опубликую чуть позже и в другом блоге, если будет интересно, а в этой статье мне хотелось бы остановиться на такой проблеме как мгновенное изменение языка внутри приложения.
Собственно, проблема.
Наверное, многим приходилось сталкиваться с мультиязычными приложениями. Я говорю не только о приложениях под iOS, а вообще о приложениях, поддерживающих несколько языков. Во из них в сеттингах есть пункт «Language/Язык/Idioma», позволяющий установить язык, нужный пользователю.
В разных ситуациях эта опция работает по-разному. В каких-то приложениях для установки нового языка приходится их перезапускать. В каких-то все случается мгновенно. О том, как осуществить второй подход при написании приложений под iOS, в статье и пойдет речь.
Что предлагает Apple.
Строго говоря, Apple сторонники того, чтобы приложение использовало ту локаль, которая установлена дефолтной на телефоне. Это весьма разумно, поскольку русский человек вероятнее всего поставит русский язык, и ему захочется видеть приложения на русском.
Однако, это работает не всегда. В некоторых приложениях перевод бывает далек от идеала — слова не помещаются на кнопках, или выглядят переведенными с помощью неумирающего Prompt'а, или просто раздражает, что перевели текст, но не локализовали картинки. Хочется видеть приложение цельным, поставить английский язык и пользоваться в свое удовольствие. Некоторые предоставляют такую возможность, и я предлагаю разобраться, как именно они это делают.
Как приложение устанавливает локаль.
Тут все просто. Есть такое понятие как NSBundle — набор локализованных ресурсов приложения. Если приложение содержит директорию вида ru.lproj и локаль телефона установлена в ru_RU, то, допустим, вызов
[[NSBundle mainBundle] loadNibNamed:@"xib_name" ...]
вначале попробует найти соответствующий ресурс в директории ru.lproj, и толко если сделать этого не получилось, вернет дефолтный, находящийся в корне.
Далее. Приложения, поддерживающие несколько языков, скорее всего будут использовать NSLocalizedString. Эта конструкция — NSLocalizedString( @«string», @«comment» ) разворачивается в
[[NSBundle mainBundle] localizedStringForKey:@"string" value:@"" table:nil]
Что делать, если хочется поменять локаль на недефолтную? Относительно популярный способ решения этой задачи — после выбора пользователем языка убить приложение, а при последующем запуске подменить дефолтную локаль, с которой NSBundle будет загружать ресурсы. Что-то вроде этого:
main.m:
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObjects:@"ru_RU", nil]
forKey:@"AppleLanguages"];
[[NSUserDefaults standardUserDefaults] synchronize];
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([YourApp class]));
}
Здесь и далее, MA — префикс, расшифровывающийся как MyApp, потаенного смысла нет :)
Решение не из худших, ведь в итоге локаль меняется на заявленную, а то, что происходит это не очень user friendly — издержки производства.
Однако, я все чаще замечаю приложения, в которых локаль меняется без перезагрузки приложения, как, например, в прекрасном приложении от компании Booking.com (совсем не реклама — приложение и правда очень хорошее). В определенный момент, я задался целью выяснить как все это работает.
Подготовительная работа
Первый этап.
Не буду подробно останавливаться на подготовке к локализации приложения, на эту тему написано немало статей. Подчеркну лишь, что жизнь ваша станет намного легче, если на самом начальном этапе вы выставите правильно дефолтную локаль приложения и будете писать
NSLocalizedString( @"string", @"comment" )
вместо простого
@"string"
Для тех, кто не знает, второй параметр в NSLocalizedString — это комментарий, который автоматически добавляется в новый файл локализации, генерируемый командой genstrings. Очень полезно.
Второй этап.
Сделать как-то так, чтобы при вызове макроса локализации использовался бы не mainBundle, а некий «кастомный» бандл, в котором содержатся указанные нами ресурсы локализации.
Дла этого создаем новый синглтон MALocalizationSystem (реализацию синглтона на objc оставим гуглу и модному нынче dispatch_once ;)), в который добавляем методы:
+ (MALocalizationSystem *) sharedLocalizationSystem;
- (NSString *) localizedStringForKey:(NSString *)key value:(NSString *)comment;
- (void) setLanguage:(NSString *) language;
- (NSString *) getLanguage;
Реализация методов проста, как тапок:
static MALocalizationSystem *_sharedLocalizationSystem = nil; // инстанс синглтона
static NSBundle *bundle = nil; // текущий бандл. Инициализируем со значением [NSBundle mainBundle] в методе init
static NSString *_currentLanguage = nil; // текущий язык
- (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]; // файлы локализации не были найдены - сбрасываем _currentLanguage в nil и bundle в [NSBundle mainBundle]
}
else
{
bundle = [NSBundle bundleWithPath:path];
}
// тут по желанию можно посылать нотификейшн об успешной смене локали.
[[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"; // дефолтный язык для нашего приложения.
}
}
return _currentLanguage;
}
И определяем несколько макросов для удобства:
#define MALocalizedString(key, comment)
[[MALocalizationSystem sharedLocalizationSystem] localizedStringForKey:(key) value:(comment)]
#define MALocalizationSetLanguage(language)
[[MALocalizationSystem sharedLocalizationSystem] setLanguage:(language)]
#define MALocalizationGetLanguage
[[MALocalizationSystem sharedLocalizationSystem] getLanguage]
С переводом все более-менее понятно — устанавливаем язык с помощью MALocalizationSetLanguage(«eo»), и везде, где мы использовали MALocalizedString вместо NSLocalizedString, будет использоваться установленный язык. Что же с ресурсами: картинками, например, да и прочими файлами? А вот тут начинается…
Третий этап.
Компания Apple все же позаботилась о тех, кто хочет загружать ресурс из определенной папки локализации. Допустим, если хочется загрузить список названий валют из xml-файла, то обычно это делается следующим образом:
NSString* pathToFile = [[NSBundle mainBundle] pathForResource:@"currencyNames"
ofType:@"xml"];
cachedCurrencyNames = [NSMutableArray arrayWithContentsOfFile:pathToFile];
Но есть и другой способ:
NSString* pathToFile = [[NSBundle mainBundle] pathForResource:@"currencyNames"
ofType:@"xml"
inDirectory:nil
forLocalization:@"ru"];
cachedCurrencyCodes = [NSMutableArray arrayWithContentsOfFile:pathToFile];
Улавливаете? :)
Резюмируем:
- в коде вместо NSLocalizedString используем наш макрос MALocalizedString — он лучше :)
- когда загружаем локализованный ресурс, стоит сделать это с указанием текущего языка: MALocalizationGetLanguage
- при смене языка пользователем, вызываем MALocalizationSetLanguage
Остается только в каждом view controller'е подписаться на событие kLocalizationChangedNotification и рефрешить локализованные ресурсы/метки/картинки. Для этого удобно собрать все это добро в один или несколько методов и вызывать его (их) во время awakeFromNib, а так же при получении этого самого доброго нотификейшена kLocalizationChangedNotification.
Вместо заключения.
Не хочу, чтобы адепты iOS поняли меня неправильно — мне импонирует подход Apple по минимизации действий юзера для удобного использования приложения. В то же время, я не считаю, что то, что я описал выше как-то выбивается из этой схемы. Это нормальный подход, когда приложение по-дефолту выбирает системный язык, после чего юзеру в сеттингах предоставляется возможность его поменять «без шуму и пыли» (с).
Спасибо, что прочитали!
Ссылки.
За основу был взят и немного переписан/дополнен/исправлен код отсюда.
Автор: kovpas