iOS Localization: XLIFF

  • Tutorial


В интернете легко найти статьи по локализации 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

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


Redmadrobot 81,33
№1 в разработке мобильных решений для бизнеса
Поделиться публикацией

Вакансии компании Redmadrobot

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

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

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

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


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

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

      • 0
        Можно поподробнее о «нарушении формата»? Поковырял интеграцию 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 возвращают ключ.
        Или может быть я что-то упустил?
    • 0
      > Ключом выступает не абстрактная строка, а текст на Development Language. Если меняется оригинальный текст, то его приходится заново переводить.

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

      Для интерфейстых элементов создаю собственные сабклассы со свойством
      @proprerty (nonatomic, strong) IBDesignable NSString* locKey;
      

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

      Это свойство видно в Interface Builder, в него и нужно прописать ключ локализации.
      • 0
        Вот, точно! В категорию нельзя ли обернуть, кстати, это?
        • 0
          Только что проверил: внезапно, работает. То есть, не нужен даже сабкласс.
          • 0
            Замечательно!
            • 0
              Вообще, я так запихиваю через Interface Builder многие вещи: шрифт из пресета, цвет placeholder'а у UITextField (который по умолчанию серый и не меняется в визуальном дизайнере), угол скругления, кастомный стиль кнопки и т.д. Практически всё, что обычно вручную делается в awakeFromNib можно реализовать таким образом.
              • 0
                Это я так же делаю, просто до упрощения локализации таким путем мысль еще не доходила.
                Была идея вставлять ключи от строк прямо в текстовые поля и заменять на локализованные строки при инстанцировании и т.п. Но то была не оформленная идея, которая казалось немного кривой. А ваш способ, по-моему, идеален.
                • 0
                  Вашу идею, кстати, я тоже реализовывал, до того как нашёл способ добавить свойство в Interface Builder.
                  Там есть некоторые трудности с тем, что для некоторых контролов текст может быть выставлен для любых состояний (всех сочетаний получается 16 штук), причём непонятно, какие из них действительно выставлены.
                  С другой стороны, если у вас для разных состояний тексты действительно отличаются — возможно лучше использовать именно этот путь.
        • 0
          Видел у ребят на соседних пару проектах это сделано через категории, они перенесли многие настройки UI компонентов (цвета, локализацию и что-то еще) в IB как раз через ibinspectable. Я не фанат такого, но оказалось реально удобно. Просто скетч проекта открываешь и копипастишь всё и в xcode, и код писать не надо.
        • 0
          И как это работает, например, с UIButton? Плохая идея сабклассить класс кластеры. Но вообще, если использовать нибы, IBDesignable действительно удобный инструмент.
          • 0
            UIButton — не кластер. Честно говоря, я не помню ни одного кластера среди наследников от UIView
            • 0
              Когда речь заходит про паттерн кластер в Cocoa, то обычно приводят в пример NSArray и UButton. Хотя я лично не проверял, каких именно типов возвращаются объекты из метода + (instancetype)buttonWithType:(UIButtonType)buttonType; Но пишут, что это наследники UIButton для не Custom типа…
              • 0
                Заглянул в доки — действительно, формально он не класс кластер (по крайней мере так не пишут прямо), так что тут я не прав. Но сабклассить его всё равно не стоит хотябы потому, что никто не гарантирует каким образом будет реализована иерархия классов и как API может измениться со временем.
                • 0
                  Отличить кластер можно по возвращаему типу, не совпадающему с оригинальным. Для [NSArray array], например, это __NSArray0.
                  Конструкторы UIButton возвращает всегда UIButton. Так что даже если там внутри происходит какая-то «магия» — на наследование она не влияет.

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

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

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

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

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

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

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

                        Насчет наследования сейчас просто начнется холивар, так что, думаю, не стоит его начинать, всем понятна противоположная точка зрения.
          • 0
            Чтобы быстро тестировать приложение под другой локалью, можно добавить в настройки схемы (или создать копий схемы под каждую локаль и язык) пару аргументов для Run. А именно:
            • выбрать схему
            • выбрать Edit Scheme в выпадающем меню
            • выбрать Run
            • выбрать Arguments
            • добавить -AppleLocale de_De
            • выбрать -AppleLanguages (de)

            Параметры в скобках соответствуют международным стандартам и легко гуглятся.
            • 0
              Возможно, пригодится — перевели статью о локализации iOs-приложений на основе XCode 7.3.1: https://habrahabr.ru/company/alconost/blog/322434/.
              • 0
                Xcode Version 8.3.3 (8E3004b).
                Для того, чтобы оставить приложению только русский язык теперь есть другой способ (возможно и был, не проверял на версиях старее).

                Итак, цель: Один язык в приложении кроме английского.
                1. Идем в настройки проекта (не Target), в списке ищем Localizations. Удаляем все лишние языки.
                2. Идем в Target проекта (или extensions) -> Info -> Localization native development пишем «ru» для русского, для других языков смотри список (Настройки проекта — Localizations -> "+" открывается список с названием языка и коротким названием).
                3.Чистим проект, удаляем приложение с девайса.

                Итого: всегда русский язык в приложении

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

                Самое читаемое