В интернете легко найти статьи по локализации iOS, где описываются все основные этапы. Проблема в том, что чаще нам на глаза попадается вариант ручного заполнения файла *.strings. Это довольно муторный подход и даже небольшая автоматизация в этом нам бы пригодилась. Ещё в iOS 8 Apple добавила возможность частичной автоматизации перевода приложения посредством экспорта и импорта локализованных строк через XLIFF-документ.


XLIFF (XML Localization Interchange File Format) — это обыкновенный XML, соотвествующий стандарту для обмена локализованными данными.


Я посчитал, что этот способ незаслуженно обходят стороной или упоминают его вскользь. А ведь он позволяет достать все строки для перевода из исходников (m, swift) и ресурсов (.storyboard, .xib) и объединить их в один файл *.xliff. А после может вставить перевод из него в проект. Остается лишь не забывать использовать NSLocalizedString.


NSLocalizedString


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


public func NSLocalizedString(key: String, tableName: String? = default, bundle: NSBundle = default, value: String = default, comment: String) -> String

Если же мы еще пишем на Objective-C, то нужно использовать си макросы:


#define NSLocalizedString(key, comment)
#define NSLocalizedStringFromTable(key, tbl, comment)
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment)
#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)

Они имеют одинаковые аргументы и идентичную функциональность.


  • key Ключ, по которому лежит переведенная строка.
  • tableName Таблица, в которой находится ключ. Соответствует имени файла с расширением tableName.strings. Не обязательный параметр. По умолчанию используется Localizable.strings.
  • bundle Бандл, в котором находится таблица с ключами и переводами. Не обязательный параметр. По умолчанию используется NSBundle.mainBundle().
  • comment Комментарий для переводчика. Обязательно пишите его! Он поможет вам в будущем сориентироваться в коде.
  • value Значение, возвращаемое, если локализованная строка для ключа не была найдена в таблице.

Аргументы NSLocalizedString соответствуют содержимому XLIFF-файла:


  • Таблицы с ключами, на основе которых Xcode создаст файлы типа *.strings. Имена для них берутся на этапе создания xliff-файла из параметра tableName.
  • Оригинальный текст из параметра key.
  • Текст для перевода, который нужно заполнить.
  • Комментарий для переводчика из параметра comment.

Дo iOS 8


В старые времена нам приходилось на несколько часов становиться секретаршами, чтобы пробежаться и сделать несколько монотонных вещей:


  • Вставить NSLocalizedString, если не сделали это сразу.
  • Придумать тег NSLocalizedString("TITLE", comment: "Заголовок первого экрана") и написать комментарий, если успеваем.
  • Скопировать этот тег в файл Localizable.strings.
  • Вставить перевод для тега. "TITLE" = "My App".
  • Скопировать новую строчку в отдельный документ (например, Google Docs), чтобы переводчику было удобнее перевести.

К этому алгоритму добавлялись условия, когда тег уже существует и нужно использовать его или придумать новый, когда переводы разбиты на разные файлы или бандлы. В этой последовательности рутинного копипаста было легко допустить ошибку или "уснуть". К тому же у нас есть .storyboard или .xib файлы, и в них приходится делать IBOutlet, чтобы перевести весь текст в них из кода.


После iOS 8


С использованием XLIFF наш воркфлоу немного изменился.


  • В коде, когда добавляем текст для UI, сразу пишем его в NSLocalizedString ("My App", comment: "Заголовок первого экрана"). Если язык разработки приложения Английский и вы его не меняли.
  • Когда настало время, чтобы перевести приложение, экспортируем XLIFF-документ. В итоге у нас получаются файлы, соответсвующие поддерживаемым языкам. Например: ru.xliff, de.xliff.
  • После того как переводы заполнены, импортируем их.

В результате Xcode сам создаст все необходимые файлы типа *.strings на основе xliff-файла.


А как же .storyboard и .xib ?


Строки из них Xcode также экспортирует в таблицы с именами как у их исходных файлов.


XLIFFy


Но остается одна проблема. Чем открыть файлы xliff? Когда Apple представила такую возможность, редакторов для этих файлов почти не было, а те что были, имели неудобный интерфейс. Сейчас Mac App Store полон ими на любой вкус. Но в то время я не нашел для себя подходящую программу и решил написать сам. XLIFFy


Пример


У нас есть демо-приложение с окном авторизации, в которое мы должны будем добавить русскую локализацию. По умолчанию Xcode создает проект с английским языком разработки. Это подразумевает, что весь ваш текст в UI будет на нем.


Добавим русский язык в проект.


  • Проект
  • Настроки проекта
  • Вкладка Info
  • + внизу списка Localization


После добавления нового языка. Будут сгенерированы файлы .strings для .storyboard или *.xib.



Начнем с того, что откроем ViewController.swift и взглянем на метод signInAction(:_).


class ViewController: UIViewController {
    @IBOutlet weak var usernameTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var signInButton: UIButton!

    @IBAction func signInAction(sender: AnyObject) {
        if usernameTextField.text == "user" && passwordTextField.text == "pass" {
            // success
        } else {
            // fail

            let alert = UIAlertController(
                title: "Error",
                message: "Username or Password is not correct",
                preferredStyle: .Alert
            )
            let okAction = UIAlertAction(
                title: "OK",
                style: .Cancel,
                handler: nil
            )
            alert.addAction(okAction)

            presentViewController(alert, animated: true, completion: nil)
        }
    }
}

У нас есть UIAlertController, который должен показать пользователю описание ошибки, если он ввел неправильный логин или пароль.


Переведем заголовок.


let alert = UIAlertController(
    title: NSLocalizedString("Error", comment: ""),
    message: "Username or Password is not correct",
    preferredStyle: .Alert
)

Часто в проекте повторяется одна строка, например “Error”, и было бы хорошо, чтобы и перевод для нее был один. В таком случае нам везде, где используется эта строка нужно вызывать метод с ней в качестве аргумента.


NSLocalizedString("Error", comment: "")

В результате перевод этой строки будет лежать в единственном экземпляре в файле Localizable.strings. Этот файл используется по умолчанию если не указывается имя другого.


Добавим сообщение об ошибке.


let message = NSLocalizedString(
    "Username or Password is not correct",
    tableName: "Auth",
    comment: "Сообщение о неверном логине или пароле"
)
let alert = UIAlertController(
    title: NSLocalizedString("Error", comment: ""),
    message: message,
    preferredStyle: .Alert
)

Сейчас мы уже добавили tableName, чтобы все строки, относящиеся к сценарию авторизации, лежали в отдельном файле Auth.strings, и комментарий, чтобы переводчику было понятнее, к какому контексту относится текст для перевода.


У нас еще много строк для перевода в Main.storyboard. Но мы не будем ничего с ними делать, кроме добавления комментариев. Чтобы добавить комментарий к элементу Interface Builder'а, выберем кнопку "Sign In" и в Identity Inspector найдем блок Document с разделом Notes и напишем "Кнопка авторизации".



Теперь можно экспортировать из нашего проекта файл для локализации.


  • Проект
  • В строке меню, Editor
  • В выпадающем меню Export For Localization...

Export For Localization


В результате у нас появился файл ru.xliff. Откроем его в редакторе XLIFFy или воспользуемся бесплатным аналогом из Mac App Store. Если вы выбрали XLIFFy, то справа будут перечислены имена таблиц переводов. Это и стандартный файл переводов Localizable.strings, и таблицы с именами, как у файлов .storyboard или .xib, из которых они были получены. Также есть таблица для info.plist, в которой можно перевести название приложения для разных стран. Есть и таблица Auth.strings, с которой мы связали в коде перевод теста ошибки.



После того, как у нас все переведено, импортируем в Xcode.


  • Проект
  • В строке меню, Editor
  • В выпадающем меню Import Localizations...

Import Localizations


Может появиться окно с предупреждениями, если некоторые строки остались без перевода. Особенно часто это встречается, из-за непереведенного info.plist. Во время импорта Xcode создает на основе таблиц переводов файлы *.strings, если их нет, и вставляет в них ключ, значение и комментарий. Лучше их не редактировать вручную, при неправильном форматировании может перестать работать export / import.



После иморта переводов самое время проверить, как отображается наше приложение на разных языках. Перезапускать его на симуляторе или на девайсе, конечно, нужно, но довольно долго. Куда быстрее это можно сделать в Interface Builder.
Откройте Main.storyboard, включите Assistant Editor и выберите в его выпадающем списке Preview. В этом режиме вы можете просмотреть, как будет выглядеть ваше приложение на разных девайсах и в разных локализациях.



Меняем Development Language


В очень редких случаях может понадобиться поменять Development Language, например, на Russian, потому что весь ваш дизайн сперва создается с русским текстом.
Вам нужно будет закрыть Xcode, открыть файл проекта в текстовом редакторе <project_name>.xcodeproj/project.pbxproj, найти пару строчек


developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
    en,
    Base,
);

и заменить их на


developmentRegion = Russian;
hasScannedForEncodings = 0;
knownRegions = (
    ru,
    Base,
);

Подробнее вы можете прочитать тут


Итого


Использование файлов xliff имеет как свои плюсы, так и минусы:


Плюсы:


  • Больше не нужно заниматься монотонным копипастом.
  • Удобный перевод .storyboard и .xib.
  • Весь менеджмент файлов *.strings берет на себя Xcode.
  • Вся работа по локализации сводится к использованию NSLocalizedString.

Минусы:


  • Ключом выступает не абстрактная строка, а текст на Development Language. Если меняется оригинальный текст, то его приходится заново переводить.
  • Для повторяющихся строк из .storyboard и .xib не получится добавить одного перевода для всех. Это сделано, потому что строки связаны с разными элементами UI, и один вариант перевода может оказаться слишком большим или неподходящим для контекста использования во втором случае.
  • Нет обработки числительных и единиц измерения. Для этого нужно создавать специальный файл *.stringsdict. Handling Noun Plurals and Units of Measurement

Дополнительная информация по локализации приложений:


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

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


  1. MANIAK_dobrii
    08.06.2016 21:31

    Самый главный минус XLIFF — это отсутствие нормальной поддержки плурализации («один конь», «два коня»). Этого вполне достаточно, чтобы окончательно закопать эту технологию для нормального использования. Вся суть в (ещё более) удобном отделении данных о локализации и супер удобное взаимодействие с локализаторами. XLIFF распространенный формат, для него есть куча софта и он используется на разных платформах. Получается, что вашим локализаторам прийдется часть ресурсов размещать в XLIFF, а часть в проприетарном замороченном apple формате .stringsdict. Естественно они делать этого не будут (или будут плохо) и все прелести автоматизации сходят на нет.

    Кстати, я решительно не согласен с предложенными вами способами именования ключей для строк («Auth», «Error»), в прошлом году я записал свои мысли на этот счет стандартных средств интернационализации тут.

    Так что я бы рекомендовал какое-то другое решение для управления локализациями.


    1. ignition_inc
      09.06.2016 10:39

      Самый главный минус XLIFF — это отсутствие нормальной поддержки плурализации («один конь», «два коня»).

      Да это минус, но у меня с ним не было больших проблем.


      Кстати, я решительно не согласен с предложенными вами способами именования ключей для строк («Auth», «Error»)

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


      1. MANIAK_dobrii
        09.06.2016 11:47

        Можно поподробнее о «нарушении формата»? Поковырял интеграцию Xcode с XLIFF ещё раз и немного сам стандарт. Не понял про «таблички из оригинальной строки и перевода к ней». Ключи на языке разработки — это как раз та практика, которую предлагает Apple (я её не поддерживаю). Xcode работает с файлами ресурсов (т.е. ключи даже не обязательно использовать из кода) и он использует и ключ и перевод.

        Например, для такой строчки:

        /* Testy Commenty! */
        "testy" = "ru Testy!";
        


        Получается такой кусок в одном из XLIFF:

        <trans-unit id="testy">
                <source>en Testy!</source>
                <target>ru Testy!</target>
        </trans-unit>
        


        Просто API работает так, что, если для ключа нет перевода, то NSLocalizedString + NSString возвращают ключ.
        Или может быть я что-то упустил?


  1. Krypt
    09.06.2016 06:57

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

    При локализация ксибов я использую такой подход:

    Для интерфейстых элементов создаю собственные сабклассы со свойством

    @proprerty (nonatomic, strong) IBDesignable NSString* locKey;
    

    В сеттере которого присваиваю текст, полученный из NSLocalizedString по присвоенному по свойству ключу.

    Это свойство видно в Interface Builder, в него и нужно прописать ключ локализации.


    1. egormerkushev
      09.06.2016 17:51

      Вот, точно! В категорию нельзя ли обернуть, кстати, это?


      1. Krypt
        09.06.2016 18:24

        Только что проверил: внезапно, работает. То есть, не нужен даже сабкласс.


        1. egormerkushev
          09.06.2016 18:36

          Замечательно!


          1. Krypt
            09.06.2016 18:42

            Вообще, я так запихиваю через Interface Builder многие вещи: шрифт из пресета, цвет placeholder'а у UITextField (который по умолчанию серый и не меняется в визуальном дизайнере), угол скругления, кастомный стиль кнопки и т.д. Практически всё, что обычно вручную делается в awakeFromNib можно реализовать таким образом.


            1. egormerkushev
              09.06.2016 18:45

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


              1. Krypt
                10.06.2016 02:53

                Вашу идею, кстати, я тоже реализовывал, до того как нашёл способ добавить свойство в Interface Builder.
                Там есть некоторые трудности с тем, что для некоторых контролов текст может быть выставлен для любых состояний (всех сочетаний получается 16 штук), причём непонятно, какие из них действительно выставлены.
                С другой стороны, если у вас для разных состояний тексты действительно отличаются — возможно лучше использовать именно этот путь.


                1. egormerkushev
                  11.06.2016 19:42

                  Сделал небольшой обзор на данный приём


      1. house2008
        11.06.2016 23:45

        Видел у ребят на соседних пару проектах это сделано через категории, они перенесли многие настройки UI компонентов (цвета, локализацию и что-то еще) в IB как раз через ibinspectable. Я не фанат такого, но оказалось реально удобно. Просто скетч проекта открываешь и копипастишь всё и в xcode, и код писать не надо.


    1. MANIAK_dobrii
      09.06.2016 19:57

      И как это работает, например, с UIButton? Плохая идея сабклассить класс кластеры. Но вообще, если использовать нибы, IBDesignable действительно удобный инструмент.


      1. Krypt
        09.06.2016 20:03

        UIButton — не кластер. Честно говоря, я не помню ни одного кластера среди наследников от UIView


        1. egormerkushev
          10.06.2016 10:27

          Когда речь заходит про паттерн кластер в Cocoa, то обычно приводят в пример NSArray и UButton. Хотя я лично не проверял, каких именно типов возвращаются объекты из метода + (instancetype)buttonWithType:(UIButtonType)buttonType; Но пишут, что это наследники UIButton для не Custom типа…


        1. MANIAK_dobrii
          10.06.2016 11:39

          Заглянул в доки — действительно, формально он не класс кластер (по крайней мере так не пишут прямо), так что тут я не прав. Но сабклассить его всё равно не стоит хотябы потому, что никто не гарантирует каким образом будет реализована иерархия классов и как API может измениться со временем.


          1. Krypt
            10.06.2016 22:21

            Отличить кластер можно по возвращаему типу, не совпадающему с оригинальным. Для [NSArray array], например, это __NSArray0.
            Конструкторы UIButton возвращает всегда UIButton. Так что даже если там внутри происходит какая-то «магия» — на наследование она не влияет.

            > Но сабклассить его всё равно не стоит
            В эппловской документации для таких классов содержится отдельное предупреждение (как, например, у UIWebView: «Subclassing Notes: The UIWebView class should not be subclassed.»)

            В случае же кнопки — некоторые вещи просто невозможно реализовать без наследования (модификация intrinsicContentSize, например)


            1. MANIAK_dobrii
              10.06.2016 23:31

              Да, всё верно, сейчас везде возвращается UIButton(по крайней мере -class возвращает его, не проверял все типы) и документация (обычно) явно описывает вопросы сабклассинга. Но, я помню времена, когда от типа зависел возвращаемый сабкласс. И достаточно много времени потратил на исследования различных хаков, которые применяет эппл под капотом. Сейчас не могу нагуглить, но я читал множество предостережений насчет UIButton и его отношения к class cluster, ребята из WWDC Labs советуют этого не делать.

              То что это работает сейчас не значит, что это продолжит работать завтра. Сабклассить UIButton и навешивать это всё через IB — скользкая опасная дорога. Сегодня UIButton можно достаточно гибко кастумизировать, и я бы предпочел использовать средства UIButton, или сабкласс UIControl.


              1. Krypt
                10.06.2016 23:44

                Ну, как уже выяснилось в соседней метке — IB видит в том числе и свойства, объявленные в категориях.

                По поводу UIButton: я работаю с платформой начиная с iPhone OS 3.1.3, и не разу не замечал у UIButton признаков кластера, в отличии от того же NSArray. Возможно — я был недостаточно внимателен. Возможно — вы что-то путаете.

                По поводу наследования: не вижу ни одного повода от него отказываться — это основная концепция ООП.
                К тому же нужно всего лишь следить за отсутствием конфликтов имён и не использовать приватные методы. Для того, чтобы при таком подходе что-то сломалось — Эппл должен переписать половину SDK, по пути сломав половину приложений из AppStore. Этого не случится никогда.


                1. MANIAK_dobrii
                  10.06.2016 23:51

                  Аналогичная ситуация, но признаки я встречал, поэтому сейчас немного удивлен их отсутствием.

                  Насчет опасности: я не про навешивание свойств в категории, это не будет работать, только если они выплюнут «бинарно совместимый» класс и засвиззлят -class и кучу всего ещё. Такое бывает, но редко и нам об этом никто не рассказывает. Такое действительно очень маловероятно.

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


  1. egormerkushev
    09.06.2016 17:56

    Чтобы быстро тестировать приложение под другой локалью, можно добавить в настройки схемы (или создать копий схемы под каждую локаль и язык) пару аргументов для Run. А именно:

    • выбрать схему
    • выбрать Edit Scheme в выпадающем меню
    • выбрать Run
    • выбрать Arguments
    • добавить -AppleLocale de_De
    • выбрать -AppleLanguages (de)

    Параметры в скобках соответствуют международным стандартам и легко гуглятся.