Это большой ультимативный гайд по локализации. Если вы только начинаете изучить локализацию - рекомендуем читать по порядку. Все инструменты в статье редакция выстрадала опытом и временем.
Основы
Начнём с основ — добавим язык и переведём слова.
Добавление языка
Чтобы добавить новый язык, перейдите в Настройки проекта -> Info. Потом найдите секцию Localizations, нажмите на кнопку `+` и выберите новый язык.
Локализация строки
Чтобы перевести строку, используем макрос NSLocalizedString. Он принимает 2 параметра — ключ и комментарий, а возвращает уже локализованную строку.
let localisedString = NSLocalizedString(
"label text", // уникальный ключ, связан со строкой
comment: "Пример комментария для ключа" // комментарий для переводчика, можно оставить пустым
)
Теперь добавим перевод. Создайте файл Localizable.strings. Файл можно перевести на языки, которые поддерживает проект. Необязательно переводить на все языки. В инспекторе справа вы увидите, какие языки поддерживает файл. Чтобы перевести строки на новый язык, поставьте рядом с ним галочку.
Локализация заполняется в формате "ключ" = "значение";. Перейдите в файл и добавьте строки:
/<i> Пример комментария для ключа </i>/
"label text" = "Localised Text";
Ключ локализован - теперь по ключу label text вернётся локализованное значение Localised Text. Заполните по аналогии другие языки.
Структура папок на диске будет отличаться. Xcode создаст папку с идентификатором локали en.lproj и файлы локализации будет помещать в неё. Так в папке может быть несколько файлов одной локализации.
Передача параметра в строку
Возможность пригодится, если хотите поприветствовать пользователя. Например, написать Привет, Имя! или отобразить время Осталось X минут. В NSLocalizedString можно передавать параметры — строки или числа. Для этого нужны спецификаторы - Xcode заменит их на значения:
- %@ - для значений String;
- %d - для значений Int;
- %f - для значений Float;
- %ld - для значений Long;
Весь список спецификаторов находится на сайте Apple Developer.
Перейдём к примеру. Давайте создадим объект String с инициализатором format:
let parametrString = "Parametr Example" // параметр, который будем передавать
let localisedString = String(
format: NSLocalizedString(
"label text", // ключ локализации
comment: "" // комментарий
), parametrString // переменная
)
Теперь локализуем ключ с параметром. Перейдём в Localizable.strings и добавим:
"label text" = "Localised Text with %@";
Мы использовали спецификатор для параметров с типом String - %@, он заменится значением. Теперь при выводе ключа label text получим label text with Parametr Example.
Порядок параметров
Если в строке находятся два спецификатора одинакового типа, то значения отобразятся в том порядке, в котором мы их передадим. Давайте создадим переменную localisedString, принимающую 3 параметра:
let parametrString = "Make Apple"
let secondParametrString = "great again"
let parametrInt = 941
let localisedString = String(
format: NSLocalizedString("label text", comment: ""),
parametrString, secondParametrString, parametrInt
)
Локализуем ключ в strings-файле:
"label text" = "Lets %1$@ a true %2$@ at %3$d o’clock";
// %1$@ - для первого текстового значения и так далее.
// %3$d - для первого числового значения.
Нумерацию параметров можно не указывать, тогда строка будет такая:
"label text" = "Lets %@ a true %@ at %d o’clock";
Теперь при выводе переменной localisedString мы получим текст: Lets Make Apple a true great again at 941 o'clock. Если изменить порядок элементов, то изменится их порядок при выводе. Например, если создадим localisedString так:
let parametrString = "Make Apple"
let secondParametrString = "great again"
let parametrInt = 941
let localisedString = String(
format: NSLocalizedString("label text", comment: ""),
secondParametrString, parametrString, parametrInt // меняем parametrString и secondParametrString местами
)
При выводе получим Lets great again a true Make Apple at 941 o'clock
Локализация Info.plist
Info.plist — системный файл проекта, который содержит информацию о бандле, имени приложения, ключах разрешений и т. д. Мы можем локализовать имя приложения и ключи разрешений. Создаём файл InfoPlist.strings и в инспекторе выбираем поддерживаемые языки.
Чтобы локализовать название приложения, добавим в файл CFBundleName в формате "ключ" = "значение";:
"CFBundleName" = "App name";
Когда добавляете в Info.plist разрешения, например, для использования камеры, объясните, зачем оно нужно приложению. Локализуем это сообщение.
Список ключей можно глянуть здесь. Вставляем ключ и локализуем:
/<i> Privacy - Camera Usage Description </i>/
"NSCameraUsageDescription" = "We use the camera to take pictures.";
Экспорт и импорт локализации
Экспорт и импорт локализации автоматизирует действия, добавляет ключи. Экспорт помогает передать файлы переводчику, не передавая весь проект целиком. Переводчик видит имя ключа и комментарии к нему.
Перейдём в Product. Тут мы видим кнопки Export Localizations... и Import Localizations....
После экспорта создаются файлы xcloc — в них находится информация для переводчика:
Внутри xcloc находятся 3 папки и файл:
- Папка Localized Contents содержит локализуемые ресурсы, включая файл xliff. В нём находятся локализуемые строки.
- Папка Notes содержит дополнительную информацию для переводчиков: скриншоты, видео или текстовые файлы.
- Папка Source Contents хранит исходные strings-файлы и контекст для переводчиков: файлы интерфейса и другие ресурсы.
- Файл contents.json хранит метаданные о каталоге: регион разработки, язык, номер версии Xcode, а также номер версии каталога.
Переведём приложение через экспортированный файл. У Xcode есть встроенная IDE для редактирования файла. Откройте xcloc-каталог.
На сайдбаре увидите 2 файла — InfoPlist и Localizable. В первой колонке ключ, во второй переводим его, а в третьей находится комментарий. Сохраните файл после перевода. Чтобы импортировать локализацию, перейдите в Product -> Import Localizations.
Здесь выбираем каталог и загружаем в проект. В файле Localizable.strings импортированного языка появятся переведённые ключи:
/<i> No comment provided by engineer. </i>/
"key a" = "Буква А";
/<i> No comment provided by engineer. </i>/
"key b" = "Буква Б";
/<i> No comment provided by engineer. </i>/
"key c" = "Буква С";
/<i> No comment provided by engineer. </i>/
"key d" = "Буква Д";
/<i> No comment provided by engineer. </i>/
"key e" = "Буква Е";
Встроенный переводчик удобно использовать для быстрой правки локализации.
Poedit
Это альтернативная IDE для редактирования xсloc-каталогов. Она покажет ошибки в переводе, отсутствующие строки и автоматически переведёт ключи на другой язык.
Poedit умеет читать только xliff-файлы, поэтому открываем xcloc-каталог правой кнопкой и переходим в содержимое пакета.
Итак, нас интересует папка Localized Contents. Внутри будет xliff файл, откройте его через Poedit.
Здесь содержатся все ключи списком. Выберите нужный. После этого внизу появится исходный ключ и поле для ввода перевода. Справа есть варианты перевода, ключ и комментарий. После перевода сохраните файл и импортируйте xсloc в проект.
BartyCrouch
Это консольный инструмент и встраиваемый плагин. Он автоматизирует локализацию и генерацию ключей, обновляет strings-файлы, удаляет неиспользуемые ключи и сортирует ключи по алфавиту.
Установка
Как установить BartyCrouch:
- Откройте терминал и установите Homebrew```
<ol class="ol"><li>Введите в терминал <mark>brew install bartycrouch</mark></li><li>Создайте дефолтный конфиг, для этого вставьте в терминал:</li></ol>```
bartycrouch init
В папке появится скрытый файл .bartycrouch.toml.
Это стандартная конфигурация.
Настройка
Прописываем paths и codePaths чтобы файлы локализации нашлись быстрее:
// Указывайте путь к файлам в вашем проекте, например:
paths = ["App/Localisations/"] // <mark>strings</mark>-файл
codePaths = ["App/Data/"] // Файл с <mark>enum</mark>, если используете <mark>supportedLanguageEnumPaths</mark>
Что пригодится для задачи interfaces:
- subpathsToIgnore = ["."] — пути к файлам, которые нужно игнорировать.
- defaultToBase = true — добавляет значение от стандартного языка к новым не локализованным ключам.
- ignoreEmptyStrings = true — запрещает создание view для пустых строк.
- unstripped = true — сохраняет пробелы в начале и конце strings-файлов.
Что пригодится для задачи normalize:
- separateWithEmptyLine = false — создаёт пробелы между строками.
- sourceLocale = "." — переопределяет основной язык.
- harmonizeWithSource = true — синхронизирует ключи с остальными языками.
- sortByKeys = true — сортирует ключи по алфавиту.
Опций больше, весь список смотрите в документации.
После настройки конфига запустите проверку:
bartycrouch update
BartyCrouch проверит ключи, добавит их в strings-файлы и избавится от ненужных строк. Команды, которые будут выполнятся через update, можно настроить. Например:
[update]
tasks = ["interfaces", "normalize", "code"]
Теперь при вызове отработают только 3 задачи. Ещё есть lint-задача, которая по умолчанию делает поверхностную проверку. Она ищет повторяющиеся ключи и пустые строки.
Встроить в Xcode
Чтобы не вызывать Bartycrouch вручную, можно встроить его в Xcode — проверка будет запускаться при каждом билде. Переходим в таргет проекта -> Build Phase, нажимаем на плюсик и создаём новый скрипт:
Вставляем код:
if which bartycrouch > /dev/null; then
bartycrouch update -x
bartycrouch lint -x
else
echo "warning: BartyCrouch not installed, download it from https://github.com/FlineDev/BartyCrouch"
fi
Теперь Bartycrouch делает проверку автоматически.
Плюрализация
Пригодится, если захотим локализовать количество, например:
- У Тима нет наушников;
- У Тима 1 наушник;
- У Тима 2 наушника;
- У Тима 7 наушников;
Создаём функцию:
func headphonesCount(count: Int) -> String {
let formatString: String = NSLocalizedString("headphones count", comment: "Don't localise, will localise in stringsdict")
let resultString: String = String.localizedStringWithFormat(formatString, count) // передаем count
return resultString // возвращаем нужный текст
}
Создаём новый файл. В поиске пишем strings и выбираем Stringsdict File. Называем Localizable и добавляем в проект.
Переходим в файл и видим структуру:
- Localised String Key — ключ локализации - headphones count.
- Localised Format Key — параметр, значение которого войдёт в строку результата. В нашем случае только один: count.
- NSStringFormatSpecTypeKey — указывает единственный возможный тип перевода NSStringPluralRuleType. Он значит то, что в переводе встречается множество имён существительных (то, что мы хотим локализовать). Его не трогаем.
- NSStringFormatValueTypeKey — строковый спецификатор формата числа. Например, `d` для целых чисел. Полный список тут.
- zero, one, two, few, many, other — различные формы множественного числа для языков. Обязательное other — оно будет использовано, если переданное число не удовлетворит ни одному из перечисленных условий. Остальные можно убрать, если они не используются.
Заполняем файл:
Видим, что two, few, many и other повторяются. Обязательно только other, поэтому остальные убираем.
Файл заполнен, но при вызове функции headphonesCount(count: 1) вместо перевода мы получим ключ headphones count.
Чтобы локализовать stringsdict, перейдём в инспектор -> кнопка Localize
Затем выбираем языки, для которых нужно создать stringsdict-файлы.
Локализовать .stringsdict можно прямо в созданном файле. Выбираем Localizable (Russian) в левом меню.
Заполняем строки, добавляем few для корректного перевода числа на русском.
Что получим:
headphonesCount(count: 0) // У Тима нет наушников
headphonesCount(count: 1) // У Тима 1 наушник
headphonesCount(count: 2) // У Тима 2 наушника
headphonesCount(count: 7) // У Тима 7 наушников
Если нужно локализовать другое слово, создайте новое значение в stringsdict-файле. Например, посчитаем яблоки. Для этого создаём функцию с новым ключом:
func applesCount(count: Int) -> String {
let formatString: String = NSLocalizedString("apples count", comment: "")
let resultString: String = String.localizedStringWithFormat(formatString, count)
return resultString
}
Переходим в stringsdict, создаём новое значение apples count. Настраиваем так же, как в прошлых шагах.
Новое значение всё ещё можно локализовать прямо в файле, но в этот раз для перевода используем другой способ и экспортируем локализацию через Product -> Export Localizations.... Открываем нужный xcloc-каталог:
Переводим и импортируем в проект через Product -> Import Localizations.... В stringsdict-файле русского языка осталось лишнее значение many. Удаляем его.
Проверяем:
applesCount(count: 0) // У Тима нет яблок
applesCount(count: 1) // У Тима 1 яблоко
applesCount(count: 7) // У Тима 7 яблок
applesCount(count: 131) // У Тима 131 яблоко
applesCount(count: 152) // У Тима 152 яблока
Локализация SPM-пакетов
Чтобы локализовать SPM-пакет, создадим папку внутри пакета с идентификатором языка. Например, en.lproj. У каждого языка есть свой идентификатор, весь список можно глянуть по ссылке. В папке создаём файл Localizable.strings.
Повторяем процедуру для каждого нужного языка.
В файле Package выставляем defaultLocalization — этот язык будет использоваться по умолчанию. Указываем папку с файлами локализации в resources.
В файле Localizable.strings каждого языка должны храниться пары ключ-значение NSLocalizedString, которые мы используем в пакете. Например:
NSLocalizedString("first key", bundle: .module, comment: "")
Указываем bundle: .module в инициализаторе NSLocalizedString. Так мы указываем, что строку нужно искать в пакете. А в Localizable.strings локализуем как обычно:
/<i> No comment provided by engineer. </i>/
"first key" = "First key";
Экспорт и импорт
Чтобы экспортировать пакет, перейдите в Product -> Export Localisations и выберите пакет. Выше мы как раз рассмотрели способы локализации экспортированных файлов.
Локализация специальных данных
Она пригодится, если захотите локализовать данные в правильном формате в зависимости от выбранного языка. Например, сумму 3 000,00 ₽, дату 24 апр. 2022 г. или процент 54 %.
В английском языке процент пишется слитно с числом 61%, а в немецком - раздельно 61 %. Что бы значения автоматически изменялись правильно - существуют форматтеры, подробнее о них в нашей статье по форматтерам.
Идентификаторы языка
Чтобы получить идентификатор локали, вызовите Locale.current.identifier. Вернётся значение языкприложения_ЯЗЫКРЕГИОНА, например, en_US. Полный список таких идентификаторов найдёте по ссылке
Локализация изображений
Представим, что нам нужно показывать флаг страны по локализации приложения. Переходим в Assets -> Добавляем стандартное изображение. Потом переходим в инспектор -> Localize...
Выбираем языки, на которые хотим локализовать изображение. Добавляем нужные изображения в появившихся полях.
Проверяем, как отображается изображение на разных языках.
Языки справа-налево RTL
В английском, русском, немецком и других языках текст пишется слева-направо. Но есть языки с направлением справа-налево, например иврит, персидский и арабский.
Направление текста и интерфейса можно определить для приложения и для каждой вью отдельно. Направление приложения определяется в объекте UIApplication:
if UIApplication.shared.userInterfaceLayoutDirection == .leftToRight {
// Код для LTR направления
} else {
// Код для RTL направления
}
Иногда направление конкретной вью не соответствует направлению приложения. Чтобы получить направление для конкретной вью, используйте effectiveUserInterfaceLayoutDirection:
if view.effectiveUserInterfaceLayoutDirection == .leftToRight {
// Код для LTR
} else {
// Код для RTL
}
Картинки
Картинки тоже подчинаются направлению интерфейса. Символы из SFSymbols отзеркаливаются из коробки.
Кастомные иконки нужно отзеркаливать вручную, используйте метод imageFlippedForRightToLeftLayoutDirection():
// Оригинальная иконка
let image = UIImage.init(named: "icon")
// Отзеркаленная
let flippedImage = image?.imageFlippedForRightToLeftLayoutDirection()
Текст
Для лейблов выставляем textAlignment = .natural. Так текст будет выравниваться согласно языку приложения: .left для LTR, .right для RTL.
Есть исключения. Например, ваш арабский текст включает абзац английского текста в одну или две строки. Тогда можно оставить направление справа-налево для обоих языков. Но если абзац больше 3 строк, то выравнивание должно соответствовать языку.
Фреймы
Создаем квадрат размером 100 на 100. Позицию `x` будем менять согласно направлению интерфейса:
let space = 30
squareView.frame = .init(x: 0, y: 0, width: 100, height: 100);
if squareView.effectiveUserInterfaceLayoutDirection == .leftToRight {
squareView.frame.origin.x = space
} else {
squareView.frame.origin.x = view.frame.width - space - squareView.frame.width
}
Теперь, если лейаут LTR - квадрат будет стоять в 30 поинтах от левого края. Если RTL - в 30 поинтах от правого:
Auto Layout
leftAnchor и rightAnchor это всегда левый и правый край соответственно. Даже в RTL это не меняется. Но если использовать leadingAnchor и trailingAnchor, то края поменяются местами для направления справа-налево. Для LTR это будет левый и правый край, для RTL наоборот - правый и левый.
Создаем квадрат размером 100 на 100. Указываем leadingAnchor и констрейнты:
squareView.topAnchor.constraint(equalTo: view.topAnchor, constant: 200).isActive = true
squareView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
squareView.widthAnchor.constraint(equalToConstant: 100).isActive = true
squareView.heightAnchor.constraint(equalToConstant: 100).isActive = true
Теперь в LTR направлении квадрат будет стоять в 30 поинтах от левого края, а в RTL - от правого. trailingAnchor работает так же, только для правого края.
Рекомендации
Теперь поделюсь советами по работе с локализацией, чтобы вы могли сэкономить время и избежать переиспользования кода.
Отдельный файл для ключей
Создаём файл, внутри делаем enum Texts. В нём создаём статические переменные, которые вернут NSLocalizedString. Его можно структурировать, создавая дочерние enum внутри других enum:
enum Texts {
enum FirstController {
static var title: String { NSLocalizedString("first controller title", comment: "") }
static var subtitle: String { NSLocalizedString("first controller subtitle", comment: "") }
static var action_button: String { NSLocalizedString("first controller action button", comment: "") }
static var cancel_button: String { NSLocalizedString("first controller cancel button", comment: "") }
}
enum SecondController {
static var title: String { NSLocalizedString("second controller title", comment: "") }
static var subtitle: String { NSLocalizedString("second controller subtitle", comment: "") }
static var action_button: String { NSLocalizedString("second controller action button", comment: "") }
static var cancel_button: String { NSLocalizedString("second controller cancel button", comment: "") }
}
}
В проекте получение строки будет выглядеть так:
titleLabel.text = Texts.FirstController.title
Если переменных много, можно создать несколько файлов и разгруппировать ключи.
Часто используемые слова
Функциональные слова, такие как ОК, Отменить, Удалить можно вынести в отдельный enum Shared и использовать по всему приложению, чтобы не дублировать локализации:
enum Shared {
static var ok: String { NSLocalizedString("shared ok", comment: "") }
static var cancel: String { NSLocalizedString("shared cancel", comment: "") }
static var delete: String { NSLocalizedString("shared delete", comment: "") }
}
Shared можно вынести в отдельный пакет, чтобы использовать для разных таргетов проекта.
Передача параметров в ключ
Чтобы красиво передать параметры в NSLocalizedString, создайте функцию:
static func fruitName(name: String) -> String {
return String(format: NSLocalizedString("fruit name %@", comment: ""), name)
}
Вызываем в коде:
fruitNameLabel.text = Texts.fruitName(name: "Apple")
Как называть ключи
NSLocalizedString принимает 2 параметра, которые будут видны при локализации — ключ и комментарий. Можно создать непонятный ключ и подробно описать в комментарии, зачем он нужен. Но лучше делать понятные имена. Например, секции в футере с фидбеком на экране настроек:
NSLocalizedString("settings controller table feedback section footer", comment: "")
Полезные инструменты
-
poedit.net
Poedit
Приложение для локализации xcloc-файлов. Поддерживает автоматический перевод всех строк на другой язык, обладает удобным интерфейсом.
-
github.com
BartyCrouch
Автоматизация локализаций. Удаляет неиспользуемые строки, сортирует по алфавиту — это настраивается.
Особенности
- Интерфейс должен быть динамическим. Заранее рассчитать ширину и высоту лейбла под текст не получится, потому что одни и те же слова занимают разное место в зависимости от языка. Обычное «Как ты?» переводится на французский как «Comment allez-vous?».
- В английском языке функциональные слова пишутся с большой буквы. Так, кнопка «Add new» пишется с двух заглавных «Add New». В русском языке такое встречается, но не часто.