Мгновенное изменение языка приложения

    Мне бы хотелось рассказать об интересном опыте, приобретенном в процессе разработки бесплатного пока что конвертера валют, моего второго приложения в категории Finance. Первое, Money iQ, было написано во время работы в небольшой компании и даже успело побывать на 1м месте российского App Store. Небольшую dev story о создании приложения я опубликую чуть позже и в другом блоге, если будет интересно, а в этой статье мне хотелось бы остановиться на такой проблеме как мгновенное изменение языка внутри приложения.

    Собственно, проблема.


    Наверное, многим приходилось сталкиваться с мультиязычными приложениями. Я говорю не только о приложениях под 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 по минимизации действий юзера для удобного использования приложения. В то же время, я не считаю, что то, что я описал выше как-то выбивается из этой схемы. Это нормальный подход, когда приложение по-дефолту выбирает системный язык, после чего юзеру в сеттингах предоставляется возможность его поменять «без шуму и пыли» (с).

    Спасибо, что прочитали!

    Ссылки.


    За основу был взят и немного переписан/дополнен/исправлен код отсюда.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 33
    • –2
      Однако, это работает не всегда. В некоторых приложениях перевод бывает далек от идеала — слова не помещаются на кнопках, или выглядят переведенными с помощью неумирающего Prompt'а, или просто раздражает, что перевели текст, но не локализовали картинки. Хочется видеть приложение цельным, поставить английский язык и пользоваться в свое удовольствие. Некоторые предоставляют такую возможность, и я предлагаю разобраться, как именно они это делают.

      а не проще ли сразу сделать перевод не Prompt'ом, размер кнопок нормальным, а картинки локализованными?
      • +1
        Это юзеру решать — мне, например, какие-то программы проще воспринимать на русском, хоть язык системы установлен английский.
        • 0
          Не проще. Автор врядли сможет повлиять на всех девелоперов. :) Да и проблемы установки рускоязычного приложения на англоязычный телефон с автоматическим определением локализации это не решит.

          Пример из жизни. У меня айфон с английской локализацией. Устанавливаю детскую игру из русского апстора и получаю все инструкции и картинки на английском языке. На русский она переключается только при смене языка в настройках телефона, что неудобно для меня. Английский язык пока еще неподходит для дочки. Почти тупиковая ситуация.
          • +1
            А мы вот постарались в своих книгах устранить эту проблему и сделали переключение языка внутри приложения. Хотя у нас система попроще (или посложнее, как посмотреть), чем у автора этой статьи.
            • 0
              Молодцы! Мы пошли другим путем. Решили сделать 2 отдельных приложения. В нашем случае это оказалось более логичным решением. :)
        • +1
          Я в свое время использовал такую функцию ( было давно )

          Не очень правильно было использовать число для идентификации языка но тем не менее код нормально работал

          static NSBundle* saved_language_bundle = nil;
          static int saved_language = -1;

          NSString* languageSelectedString(NSString* x1)
          {
          NSUserDefaults* prefs = [NSUserDefaults standardUserDefaults];
          NSString * result = nil;
          int language = [[prefs valueForKey:@"uoslanguage"] integerValue];
          if (language == 2) // это проверка того что все строки локализуются - включашеь язык и на тех формочках где нге то что тебе надо не появляется тест
          {
          return @"test";
          }
          if ( language == saved_language ) // бандл грузится только 1 раз
          {
          result = [saved_language_bundle localizedStringForKey:x1 value:@"" table:nil];
          } else
          {
          saved_language = language;
          if ( language == 1)
          {
          NSString* path = [[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"];
          saved_language_bundle = [NSBundle bundleWithPath:path];
          result = [saved_language_bundle localizedStringForKey:x1 value:@"" table:nil];
          }
          else if ( language == 0)
          {
          NSString* path = [[NSBundle mainBundle] pathForResource:@"ru" ofType:@"lproj"];
          saved_language_bundle = [NSBundle bundleWithPath:path];
          result = [saved_language_bundle localizedStringForKey:x1 value:@"" table:nil];
          } else
          result = @"error";
          }
          return result;
          }

          • 0
            какая то проблема с форматированием
            gist.github.com/1922569
            • 0
              Ну да, по факту то же самое, скомпанованное в одну функцию :)
              • 0
                Да, вдруг кому поможет или будет удобнее
          • +2
            Это, конечно, не совсем по теме статьи, но в интерфейсах приложения smart coin, которое вы упомянули в статье, странноватый английский: choose currency from a complete list — э?..
            Ну и таких ляпов много, даже на скриншотах приложения, выложенных в айтюнсе.
            • 0
              Очевидно, Ваш английский лучше моего :)).

              Не затруднит прислать список «странных» фраз в личку? Хотя бы тех, что сразу бросаются в глаза.
              • +2
                Не затруднит, конечно. Отправил в личку.
                • 0
                  может и сюда — как было/стало? чтоб знать :)
                  • +1
                    «Сразу оговорюсь, что моего английского достаточно, чтобы видеть неточности, которые скорее всего заметит носитель языка, но недостаточно, чтобы предложить абсолютно верный, «нативный» вариант перевода. Тем не менее, попытаюсь предложить более правильные варианты перевода. Если у вас есть под рукой носитель языка — лучше прогнать тексты через него.

                    Add a favorite currency -> add currency to favouries
                    to choose a favourite currency, pick and place it -> pick and place this currency to favourites
                    update rates -> update exchange rates
                    choose currency from a complete list -> browse full list of currencies»
                    • 0
                      *favouriTes, конечно же.
                      В общем, еще и через word тексты прогоните)
                      • 0
                        Спасибо!
              • 0
                А как теперь из кода строчки выдернуть? Макросы мы заменили и genstrings видимо поломается?
                • 0
                  man genstrings:

                  -s routine
                  Substitutes routine for NSLocalizedString. For example, -s MyLocalString will catch calls to MyLocalString and MyLocalStringFromTable.


                  ;)
                  • 0
                    Ага, написал и уже потом подумал, что подобное умеет эта тулза. Спасибо.
                • 0
                  Этот подход будет ну очень актуален в приложения с локализацией на языки, локаль которых не поддерживается в iOS в принципе, на пример латышский или азербайджанский.

                  Сам лично как раз подошел к этой проблеме в своем проекте, огромное спасибо автору.
                  • 0
                    Да, получается, что это бесплатный бонус :)
                  • 0
                    Есть такое понятие как NSBundle — набор локализованных ресурсов приложения.

                    Это неправильное определение понятия Bundle.
                    • 0
                      An NSBundle object represents a location in the file system that groups code and resources that can be used in a program. NSBundle objects locate program resources, dynamically load and unload executable code, and assist in localization.
                      • 0
                        Вот именно, бандл это совокупность всех ресурсов приложения, если разговор идет про iOS.
                        • 0
                          во-первых, я нигде не говорил про то, что мое определение отличается 100% точностью
                          во-вторых, я вел разговор про локализацию и в контексте этого разговора определение правильное
                          в-третьих, мне совершенно непонятна ваша придирка к этой мелочи, которая на суть повествования вообще никак не влияет.
                          • 0
                            Я всего лишь указал на неточность в хорошей статье с целью помочь автору сделать её еще лучше и точнее.
                            • 0
                              Если бы вы посоветовали как лучше или правильнее написать, то я бы с радостью принял такую помощь. В первом же комменте вы просто кинули «это неправильное определение», но никак не «определение неточное, вот так было бы лучше ...». Это, как мне кажется и есть разница между придиркой и указанием на неточность с целью помочь.
                    • 0
                      Было бы здорово увидеть код как вьюшик рефрешатся при получении нотификейшена…

                      Неужели ручками для каждого вью реализацию писать и обновлять все контролы с текстом?
                      • 0
                        В моем случае лейблов было не сильно много, посему, да, обновлял контролы ручками. Если текстовых элементов на экране много, то можно что-то другое придумать.
                        • 0
                          вот и хотелось бы узнать, если что что-то другое…

                          Было бы здорово если бы такое обновление всего текста во вьюхах делалось самой Cocoa — тем более что вроде есть стандартный эпловский notification оповещающий о смене локали.

                          Ведь есть же функция итерации по всем subwindow, может ее как-то можно использовать…
                          • 0
                            Вряд ли изменение локали будет затрагивать уже отображенные UILabel'ы, им надо обязательно сделать setText и у меня есть большие соменния в том, что система сделает это за вас.

                            Касательно функции итерации по вьюшке не знаю, ее можно легко написать самому. Вопрос где хранить айдишник строки для данного label'а и во время итерации, соответсвтенно, делать [… setText:MALocalizedString(blah, blah)]
                      • 0
                        Вот очень странно, я не увидел самого ожидаемого комментария: у все этой идеи есть проблема — алерты системы. К примеру, с просьбой ввести логин и пароль при покупке. Они будут локализованы языком системы. И вы никак не решаете эту проблему.
                        • 0
                          Наверное потому, что эту проблему без перезапуска решить невозможно :)

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.