Всем доброго времени суток! Меня зовут Николай, я iOS-Lead в компании Touch Instinct. В процессе разработки часто приходится иметь дело с проектами, которые должны работать на нескольких языках. Расскажу, к какому подходу мы пришли при работе с локализацией.


Минусы базовых подходов


Есть несколько основных подходов для локализации iOS-приложения. Сперва стоит определиться, разрабатывается приложение с использованием storyboards или нет.


С использованием storyboards


Можно локализовывать строки напрямую в storyboard. Однако, при таком подходе есть ряд минусов:


  • в случае наличия большого количества storyboards, локализованные строки разбросаны по проекту;
  • невозможность использования атрибутных строк, а также строк, которые состоят из нескольких составных частей;
  • вам всё равно придется часть строк локализовывать в коде. Это ведет к еще большему разбросу в приложении;
  • фактически отсутствует возможность что-то проверить другому разработчику при проведении code review.

Без storyboards


В этом случае локализуем всё в коде. Однако и тут есть ряд минусов. Дело в том, что файлы со строками локализации localizable.strings — магические. При изменении таких файлов очень велика вероятность возникновения ошибки из-за человеческого фактора. Изменения нельзя отследить, пока ошибка не будет найдена в процессе тестирования.


Таким образом, хотя для локализации уже есть готовые механизмы в iOS SDK, они имеют существенные минусы. Более подробно смотрите здесь.



Разбираемся с целями


Если стандартные решения не справляются, не стоит сразу бросаться делать свой «велосипед». Сформулируйте цели, которые нужно достичь.

Основные критерии, которым должно удовлетворять будущее решение:


  • объединение ресурсов локализации для всех мобильных платформ, которые поддерживаются на проекте. В мобильной разработке чаще всего делаем проект сразу под несколько платформ, обычно iOS+Android. Одно и то же приложение на разных платформах будет использовать одни и те же ресурсы для локализации. При этом должна быть возможность иметь специфические строки для каждой отдельной платформы.
  • автоматическое добавление новых строк локализации в проекте без участия разработчика. Если приложение использует несколько языков, то ресурсы будет заполнять либо переводчик, либо посредник между переводчиком и проектом. Посредником может быть менеджер или другие участники команды. Давать доступ к коду нетехническим специалистам — моветон. Лучше всего держать ресурсы локализации где-то в стороннем месте. При этом необходимо, чтобы разработчики не добавляли каждый раз эти ресурсы «руками».
  • обнаружение ошибок в проекте при изменении ресурсов локализации на этапе компиляции. Стандартный подход, при котором используется NSLocalizedString, предполагает, что если сама строка поменяется в localizable.strings, а изменений в коде не произойдет, то в продакшен выйдет приложение с ошибкой. Самым первым ситом по поиску ошибку должен являться процесс билда проекта. Изменения строк локализации, а также ошибки возникающие вследствие изменений, необходимо получать именно на этом этапе.
  • отсутствие магических строк в проекте. Любое магическое значение в проекте это зло. Убирать их можно разными способами. Есть путь комментариев к коду или вынесения магических значений в константы с говорящими именами.

Локализация в Touch Instinct


Создаем репозиторий для строк


Для начала необходимо создать отдельный репозиторий в используемой вами системе контроля версий. Что должно храниться в данном репозитории? Всё просто. Здесь хранятся json файлы вида common_strings_eng.json/common_strings_ru.json/etc. Под каждый язык создается отдельный json файл.
Рассмотрим содержание таких файлов. Json представляет из себя текстовый формат описания данных в виде пар key-value, поэтому мы создаем ключ-наименование для каждой локализованной строки и используем его во всех файлах. Value же является локализованным значением и будет отображаться в самом приложении.
В нашей компании есть styleguide, чтобы унифицировать ключи во всех приложениях и сделать их консистентными в рамках каждого проекта.


У внимательного читателя может возникнуть несколько вопросов:


1) При заполнении файлов json можно допустить ошибку и, к примеру, забыть заполнить value для какого-то ключа в определенном языке?


Да, это верно. Однако здесь ответственность эскалируется уже на уровень переводчика или одного-единственного человека. При этом с легкостью можно создать скрипт, который пройдет по всем файлам json и покажет, где и чего не хватает в файлах.


2) В данной системе используется «старинная» система для работы с Plural. Удобно ли это?


Небольшой ликбез, что такое plural. Фактически, это формы существительного, зависящие от его количества. Пример: 1 день, 2 дня, 5 дней. Действительно, очень часто можно услышать от разработчиков, что работать с plural сложно. Однако, мы пришли к выводу, что таких ресурсов крайне мало в приложениях. Получая другие преимущества, этим можно пренебречь. Про работу стандартного механизма для работы с plural в виде словаря вы можете посмотреть здесь.



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


Пример:


{
    "common_global_error": "Ошибка",
    "common_notifications_no_new_notifications": "У вас нет новых уведомлений",
}

Локализуем приложение


Репозиторий со строками необходимо подключить как submodule к вашему проекту. Что это даёт? Если вам не нужна последняя версия строк, вы просто не обновляете версию submodule, а переводчик/дизайнеры/другие заинтересованные лица могут спокойно создавать ресурсы для следующих версий.


В процессе разработки наших проектов решили использовать локализацию в коде, даже если используются storyboards.


После подключения submodule, нам всё равно как-то необходимо получить строки в приложении, чтобы их использовать. Для этого создаём script и добавляем его в build phases как run script.
Для самого script’a будем использовать php. Однако его вы можете переписать на любой другой скриптовый язык.


Положим данный скрипт к нам в проект. Теперь добавляем в наш run script.


php ./common_strings/import_strings.php

Предварительно называете его, к примеру, «Localization». Очень часто приходится видеть сторонние проекты, в которые есть run scripts, которые никак не названы. Поэтому понять, что там происходит, сходу нельзя.




Заранее создайте в проекте пустые файлы localizable.strings, а также String+Localization.swift. После первого build (cmd+B) у нас в проекте есть заполненные файлы localizable.strings, а также файл String+Localization.swift. Если скрипт не выполняется или выполняется неправильно, убедитесь, что данные файлы у вас были заранее созданы, так как скрипт отвечает только за заполнение.



Пример готового файла Localizable.string (Russian)



Пример готового файла String+Localization.swift


Кроме этого, для простоты локализации создадим extension для String.


Теперь мы можем использовать строки из String+Localization в проекте. К примеру:


emptyLabel.text = .commonNotificationsNoNewNotifications

При этом, это уже не магическая строка. При наведении на неё можно также увидеть перевод слова на языке, который понятнее всего разработчикам проекта.




Если в проекте используется строка .commonNotificationsNoNewNotifications, а затем переводчик убирает её в новой версии, то у разработчика высветится ошибка компиляции. Потому что данной строки уже не существует в проекте и её нужно поправить. Мы предиктивно получили ошибку об изменении ресурса и не выпустили это в production/отдали тестировщикам/etc.


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

Поделиться с друзьями
-->

Комментарии (24)


  1. slutsker
    26.04.2017 03:04

    спасибо за ваше решение!
    Статья в очередной раз вызвала у меня вопрос — ведь это же максимально распространенная задача: удобная кроссплатформенная локализация с теми фичами, о которых вы пишите. Разработка мобильных приложений появилась не вчера. Почему же нет удобного и массового решения этой задачи? Или я просто не знаю о нем?


    1. niklnd
      26.04.2017 11:58

      Вопрос скорее риторический. Хотя iOS появилась далеко не вчера, многие вопросы до сих пор остаются нерешенными и неудобными, так как над системой работают такие же люди как и мы.


      1. slutsker
        27.04.2017 02:06

        А для Android что-то подобное уже существует?


        1. KamiSempai
          27.04.2017 10:29
          +1

          Могу предложить свое решение: LocoLaser: переводим приложения в Google Sheets
          Работает с Android и iOS. Ресурсы находятся в гугл таблицах. Умеет генерировать Swift и Obj-C классы. Есть плагин для Gradle.


  1. Tereks
    26.04.2017 05:29

    А как в вашей системе разработчику добавить строку, чтобы ее увидели в команде локализации? Закоммитить в submodule?


    1. niklnd
      26.04.2017 11:53

      Да, всё верно.


      1. Tereks
        26.04.2017 12:00

        А как у вас происходит согласование строк? Если все будут коммитить в один репозиторий, то случится дублирование строк. Вы договариваетесь до коммита? К


        1. niklnd
          26.04.2017 12:09

          У нас есть code-review абсолютно для всех репозиториев. Как правило такие коммиты смотрят разработчики, которые участвуют в проекте, поэтому дублирование находится на этом этапе, так как pull request просто реджектится для исправления.


  1. Lailore
    26.04.2017 07:42

    Почему нельзя создать подкласс локализованного лейбла, который будет по ключу в шаблоне брать нужные строки? Или это и есть через storyboard? Если да, то ваших минусов можно избежать. Кроме первого пункта, но это не минус. В коде все так же разбросанно, а если все собрать в одном месте, то это глобальные переменные и свалка. Или нет?


    1. niklnd
      26.04.2017 12:03

      Создание сабклассов под эту задачу смысла вообще не имеет. В сториборде имеется в виду, что вы локализуете конкретно каждый сториборд, есть такая нативная фича, что под сторибордами создаются файлики с элементами и текстами. Но опять же есть куча минусов помимо указанных выше. Самый главный — нельзя вставить динамический текст без написания это в коде.
      Что же касается разброса локализации в коде, то скорее это огромный минус чем плюс. При появлении нового языка, вам вместо одного места, в которое нужно добавить новые данные, необходимо искать все места, где нужно что-то менять.


      1. Lailore
        26.04.2017 13:23

        Так я говорю про саб класс не просто так. в сторе борде будет не строка, а шаблон типа: {Today}: {$0} {Items}
        И все динамическое на месте. Просто шаблонизатор


  1. svanichkin
    26.04.2017 12:03
    -2

    Вторая статья на тему локализаций за последний месяц и опять со своим велосипедом )) Я например использую нативные методы, локализация storiboard + локализация в строках и ничем меня это не коробит.


    1. niklnd
      26.04.2017 12:07

      Любое решение имеет место быть.
      Плюсы нашего в отличии от нативного указаны в статье.
      Главные из них:
      — кроссплатформенность
      — ошибки при изменении сразу находятся на этапе компиляции, а не в продакшене у юзера
      — отсутствие магических строк в сторибордах и коде.

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


    1. KamiSempai
      26.04.2017 17:01

      Работа со строками в iOS сделана таким образом, что невозможно иметь общую строковую базу на несколько платформ и не городить при этом велосипедов. Очевидно, вы разрабатываете только под iOS и не сталкивались с этой проблемой.

      PS: хотел третью статью написать. Но видимо придется повременить. :)


  1. budnikovv
    26.04.2017 12:53

    замена магических строк на константы которые проверяет компилятор, схожий подход в статье https://habrahabr.ru/company/tinkoff/blog/271459/


    1. niklnd
      26.04.2017 12:55

      Не совсем схожий, так как у них в итоге генерируется в конце
      NSLocalizedString(LocKey_main_continue, @"")

      А у нас просто String.lockLeyMainContinue

      Но идея в целом похожа


      1. KamiSempai
        26.04.2017 16:54

        Не приведет ли такое количество констант к увеличению времени запуска приложения?


        1. niklnd
          26.04.2017 17:20

          К вопросу про время компиляции — безусловно увеличивается, ровно как и любой run script в билд фазе. Однако, в сравнении с swiftlint статическим анализатором, то время на порядок меньше занимает.

          Что касается увеличение времени запуска, то опять же смотря какого. Если имеется в виду холодный запуск приложения, то естественно да, так как все строка хранятся в static memory. Но при замерах, которые мы проводили разница в проекте с 1000 строк просто микроскопическая что находится в пределах статистической погрешности.

          Да, и если интересует жестко вопрос оптимизации времени запуска приложения, особенно на свифте, то рекомендую посмотреть видео-запись доклада с прошедшей конференции мобиуса (видео будут через неделю), там это детально расписано и получаются конкретные цифры, а не абстрактные. (Не знаю можно ли в комментах рекламировать видео или нет :)


  1. KamiSempai
    26.04.2017 17:11

    Как по мне, так отдавать переводчикам JSON — неблагодарное дело. Обязательно что-то поломают. Код-ревью в этом случае выглядит как костыль.
    Также, замечу, тема локализации Storyboard не раскрыта. Или вы готовите еще одну статью?


    1. niklnd
      26.04.2017 17:25
      +1

      Даже если что-то поломают, то у нас проект просто не соберется. Именно в этом прелесть данного подхода.
      Про локализацию storyboard, также указано, что эту проблему мы решаем протянув аутлеты в код, вместо того, чтобы локализовывать сами сториборды. Причины также указаны почему так сделано.
      Но в целом, естественно всегда присутствует человеческий фактор) Вопрос лишь к сведению его к минимуму


      1. KamiSempai
        26.04.2017 17:34

        Возможно вам стоит взглянуть вот на этот подход: Удобная локализация iOS приложений в Interface Builder
        Это та самая «первая» статья из двух за месяц. В ней я рассказал как можно избавится от аутлетов.


        1. niklnd
          26.04.2017 17:45

          Что происходит, если один и тот же label в сториборде может иметь разный текст? Как решается это в данном подходе?
          Именно поэтому мы и решили сделать систему консистентной, а тут как говорится «лес рубят, щепки летят».


          1. KamiSempai
            26.04.2017 17:59

            Если какой-то компонент нужен вам в коде, вы делаете на него аутлет. И это нормально.
            В статье описана ситуация с надписями которые не меняются в процессе работы программы. Тянуть ссылки на них в код, только чтобы поставить текст, мне кажется немного топорным решением.


    1. Johan
      27.04.2017 17:30

      На самом деле, есть ряд сервисов для групповой работы с переводами, которые дадут тот самый GUI для переводчиков. Суть затеи при этом не поменяется.


      Кодогенерация — зачетная мысль.