- PVSM.RU - https://www.pvsm.ru -
Мне бы хотелось рассказать об интересном опыте, приобретенном в процессе разработки приложения Smart Coin [1] (бесплатный пока что конвертер валют), моего второго приложения в категории Finance. Первое, Money iQ, было написано во время работы в небольшой компании и даже успело побывать на 1м месте российского App Store. Небольшую dev story о создании Smart Coin я опубликую чуть позже и в другом блоге, если будет интересно, а в этой статье мне хотелось бы остановиться на такой проблеме как мгновенное изменение языка внутри приложения.
Наверное, многим приходилось сталкиваться с мультиязычными приложениями. Я говорю не только о приложениях под iOS, а вообще о приложениях, поддерживающих несколько языков. Во из них в сеттингах есть пункт «Language/Язык/Idioma», позволяющий установить язык, нужный пользователю.
В разных ситуациях эта опция работает по-разному. В каких-то приложениях для установки нового языка приходится их перезапускать. В каких-то все случается мгновенно. О том, как осуществить второй подход при написании приложений под iOS, в статье и пойдет речь.
Строго говоря, 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 (совсем не реклама — приложение и правда очень хорошее). В определенный момент, я задался целью выяснить как все это работает.
Не буду подробно останавливаться на подготовке к локализации приложения, на эту тему написано немало статей [2]. Подчеркну лишь, что жизнь ваша станет намного легче, если на самом начальном этапе вы выставите правильно дефолтную локаль приложения и будете писать
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];
Улавливаете? :)
Резюмируем:
Остается только в каждом view controller'е подписаться на событие kLocalizationChangedNotification и рефрешить локализованные ресурсы/метки/картинки. Для этого удобно собрать все это добро в один или несколько методов и вызывать его (их) во время awakeFromNib, а так же при получении этого самого доброго нотификейшена kLocalizationChangedNotification.
Не хочу, чтобы адепты iOS поняли меня неправильно — мне импонирует подход Apple по минимизации действий юзера для удобного использования приложения. В то же время, я не считаю, что то, что я описал выше как-то выбивается из этой схемы. Это нормальный подход, когда приложение по-дефолту выбирает системный язык, после чего юзеру в сеттингах предоставляется возможность его поменять «без шуму и пыли» (с).
Спасибо, что прочитали!
За основу был взят и немного переписан/дополнен/исправлен код отсюда [3].
Автор: kovpas
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/2538
Ссылки в тексте:
[1] Smart Coin: http://itunes.apple.com/app/smart-coin/id502182129
[2] статей: http://pyobjc.ru/2008/09/02/lokalizaciya-prilozhenij-v-mac-os/
[3] отсюда: http://aggressive-mediocrity.blogspot.com/2010/03/custom-localization-system-for-your.html
Нажмите здесь для печати.