Добро пожаловать! Если ваше приложение поддерживает разные языки, то вы наверняка сталкивались с проблемами, связанными с локализацией: ошибки в написании ключей, пропущенные значения языков, необходимость пересборки приложения в случае экстренных правок перевода.
Не самые приятные моменты разработки, правда? В этой статье речь пойдет о том, как работает локализация в Vivid.Money: поговорим о том, какой инструмент локализации мы выбрали, с какими проблемами столкнулись и как их решили.
Для лучшего понимания специфики проекта вы можете ознакомиться с этот материал, и мы предлагаем начать эту статью без лишних слов.
Какой метод локализации вы выбрали?
Наши требования к локализации включают следующее:- Синхронизация между платформами (iOS, Android, Backend) для единого источника истины;
- Проверка правильности написания используемых ключей при компиляции для исключения возможности допущения опечатки в названии ключа;
- Разработчикам не нужно самостоятельно внедрять локализации для разных языков, чтобы разработчики могли тратить больше времени на то, что им следует делать — реализацию функций;
- Простота взаимодействия с переводчиками;
- Возможность изменения значений ключей без пересборки приложения.
.
строки И .
stringsdict .
Файлы .
строки используется для хранения строковых данных в виде ассоциативного массива («ключ»: «значение»), поддерживаются заполнители.
Файлы .
stringsdict используются для хранения форм множественного числа значений ключей и представления списка.
Для каждого из поддерживаемых платформой языков имеются свои копии таких файлов с переводом, а локализация в коде осуществляется с помощью функции NSLocalizedString .
Вы можете просмотреть стандартный процесс локализации Apple по адресу: связь .
Недостатки такого подхода очевидны, если вспомнить наши требования к локализации: нет единого источника истины между платформами, нет проверки правильности используемых ключей на этапе компиляции, обновление ключей возможно только на стороне клиента , разработчики должны сами делать локализации для разных языков.
А если для решения последнего указанного недостатка можно использовать формат экспорта локализации XLIFF (локализуемый стандарт обмена данными на основе языка разметки XML), то другие проблемы не могут быть решены стандартными средствами.
А использование экспорта локализации не будет особо полезным в большом проекте, поскольку нет централизованного хранилища ключей локализации и придется как-то автоматизировать экспорт.
Уже из первого и последнего требований к локализации становится понятно, что нам нужен какой-то бэкенд для хранения и обновления локализации, и чтобы не изобретать велосипед в виде собственного сервиса управления переводами, мы решили использовать уже имеющиеся существующие на рынке.
Для себя мы определили критерии, которые важны для нас при использовании сервисов по локализации, и на их основе сравнили некоторые сервисы локализации, таблица которых представлена ниже:
Локализовать | Фраза | OneSky | POEditor | |
Разделение ключей по платформам | + | - | - | - |
Поддержка тегов | + | + | - | + |
Поддержка спецификаторов множественного числа и формата.
| + | + | +/- | + |
Мобильный SDK | + | + | + | - |
Удобный интерфейс | + | - | - | - |
Найти/объединить дубликаты | + | - | - | - |
Находим ошибки в тексте.
| + | - | - | - |
Поддержка машинного перевода | + | +/- | - | - |
Цены | https://lokalise.com/pricing | https://phrase.com/pricing/ | https://www.oneskyapp.com/pricing/ | https://poeditor.com/pricing/ |
В настоящее время на рынке существует множество сервисов для решения проблемы локализации, но большинство из них имеют более-менее одинаковый базовый функционал, и мы в первую очередь обращали внимание на простоту использования и интуитивную понятность интерфейса.
Исходя из этого, мы решили использовать Lokalise, так как он показался нам наиболее удобным из сравниваемых, что имело решающее значение.
Кроме того, у него есть приятные бонусы, такие как проверка орфографии и встроенный машинный перевод от разных провайдеров, что весьма удобно на этапе разработки функций, до появления реальных переводов.
Чтобы начать работать с lokalise, вам необходимо создать новый проект на сервисе, затем загрузить существующие файлы или добавить новые ключи вручную и разослать приглашения членам команды.
Для интеграции с проектом доступны следующие варианты:
- Скачайте с сервиса архив с локализациями и добавьте его в свой проект;
- Используйте скрипт Fastlane, предоставляемый сервисом;
- Используйте API/CLI;
- Используйте SDK.
Как мы оптимизировали работу с локализацией
Всегда удобнее использовать нативные инструменты, поэтому изначально мы хотели использовать SDK.Но у этого подхода есть один неприятный момент: после обновления переводов в сервисе пакет локализации необходимо сгенерировать и опубликовать вручную, чтобы он стал доступен на клиенте с помощью SDK.
Кроме того, у нас есть сильная привязка к конкретному сервису без абстракции, что в будущем может усложнить переход на другой сервис локализации.
Чтобы решить эти две проблемы, мы использовали прокси-API, который автоматически загружает локализации из Lokalise при каждом изменении ключа, и мобильные клиенты обращаются к нему за пакетами.
Это также позволило нам использовать обработку платформы и пользовательскую логику при создании пакетов.
Таким образом, мы не можем использовать Lokalise SDK, и нам нужно скачать пакет локализации вручную.
Мы делаем это в двух случаях: во время сборки и при каждом запуске.
Кстати, Lokalise SDK делает это только при запуске, и есть вероятность, что загрузка прервется и пользователь увидит вместо локализованных строк названия ключей.
Скрипт для скачивания локализации перед сборкой
Одним из этапов сборки приложения является загрузка архива бандла с локализациями с параметризованной средой (отладка/релиз), в зависимости от которой генерируется ссылка на скачанный бандл.После скачивания бандла с локализациями проверяем .
строки И .
stringsdict файлы на валидность с помощью утилиты plutil (утилита списка свойств), которая проверяет синтаксис: сбалансированы ли кавычки и т. д. Ниже приведен код этого скрипта на Ruby:
def self.valid_bundle?(path)
puts 'Validating localization bundle.'
strings = Dir["#{path}/Contents/Resources/*.
lproj/*.
strings"]
stringsdict = Dir["#{path}/Contents/Resources/*.
lproj/*.
stringsdict"]
is_valid = true
(strings + stringsdict).
each do |path|
stdout, stderr, status = Open3.capture3("plutil -lint #{path}")
unless status.exitstatus == 0
is_valid = false
line = stderr.strip[/on line ([0-9]*)/, 1]
puts "***********************************************".
red
puts "Found the invalid string in file at path: #{path}".
red
puts "The invalid string: #{File.readlines(path)[line.to_i-1].
strip}".
red
end
end
puts "***********************************************".
red unless is_valid
is_valid
end
Далее файлы локализации сохраняются в ресурсах основного комплекта модуля локализации и запускается скрипт для формирования структуры локализации, о которой речь пойдет ниже.
Загрузка локализации при запуске
Чтобы избежать необходимости пересборки проекта при обновлении файлов локализации, мы загружаем вышеупомянутые пакеты вместе с файлом конфигурации приложения и некоторыми фундаментальными бизнес-объектами каждый раз при запуске приложения.Сорт ЛокализацияFetcher загружает метаданные о бандле: ссылку на сам бандл и номер версии.
Если версия метаданных кэшированной локализации совпадает с только что полученной, мы ничего не делаем — у нас есть текущая версия локализации.
Если версия в кеше не совпадает с полученной, то скачайте архив с бандлом, разархивируйте его и сохраните локализации в каталог Documents, сохранив новые метаданные о локализации.
Чтобы обновленные файлы локализации можно было использовать сразу, а не после перезапуска приложения, нам необходимо сделать недействительным кеш для Локализовать.
bundle .
Делается это следующим образом: после сохранения пакета локализации в каталог Documents, циклически перебираем все каталоги локализации.
.
lproj в ресурсах бандла и в каждом меняем имена файлов Локализуемые.
строки И Локализуемый.
stringsdict на Локализуемый.
nocache.strings И Localizable.nocache.stringsdict , соответственно.
Тогда остается только указать «Локализируемый.
nocache» как параметр имя_таблицы при вызове метода localizedString (forKey: значение: таблица:) в пакете локализации, загруженном при запуске.
Скрипт для генерации кода
Нам часто приходилось сталкиваться со стандартными проблемами, возникающими при работе с локализацией: об отсутствующих локализациях ключей мы могли узнать уже во время выполнения, а от ошибок в написании самих ключей никто не был застрахован, поэтому для получения проверок при компиляции необходимо автозаполнение и строгой типизации мы решили сгенерировать неизменяемую структуру на основе бандла с локализациями.
R.swift для этих нужд не подошел, поскольку, во-первых, он может генерировать только то, что уже есть в корне основного проекта (а локализации мы скачиваем при сборке и добавляем в комплект модуля локализации), во-вторых, Размер файла при использовании он оказывается довольно большим и содержит много сгенерированного контента, который нам не нужен.
По этим причинам мы решили написать собственный сценарий.
Скрипт генерации структуры с локализацией достаточно прост: файл определяется как выходной Локализация.
swift в модуле локализации, после чего файлы локализации Локализуемые.
строки И Локализуемый.
stringsdict анализируются и записываются в выходной файл в виде статических констант и методов.
Как строки локализуются в приложении?
Константы и методы, генерируемые скриптом, выглядят так: public static let localization_var = "localization_key".
localized
public static func localization_method(_ value1: String) -> String {
"localization_method_key".
localized(with: value1)
}
Вычисляемое свойство локализованный следующее: var localized: String {
let localLocalisation = Self.localLocaliseBundle?.
localizedString(
forKey: self,
value: nil,
table: nil
)
let serverLocalisation = Self.makeServerLocaliseBundle()?.
localizedString(
forKey: self,
value: localLocalisation,
table: “Localizable.nocache”
)
return serverLocalisation ?? localLocalisation ?? self
}
- Сначала мы пытаемся сгенерировать локализованную строку для ключа из локализации в основном комплекте модуля локализации;
- Затем локализованную строку из пакета локализации, хранящегося в Документах, если таковой имеется;
- Возвращаем полученное значение, либо сам ключ, если он отсутствует как в локальной, так и в серверной связке.
func localized(with parameters: CVarArg.) -> String {
let correctLocalizedString: String = {
if localized.starts(with: "%#@") {
return localized
}
return localized
.
replacingOccurrences(of: "%s", with: "%@")
.
replacingOccurrences(
of: "(%[0-9])\\$s",
with: "$1@",
options: .
regularExpression
)
}()
guard !correctLocalizedString.isEmpty else {
return self
}
return String(format: correctLocalizedString, arguments: parameters)
}
Вот что происходит: - Для форм множественного числа просто возвращается локализованная строка;
- В противном случае спецификаторы формата, указанные для строки в Lokalise, заменяются собственными;
- Выполняется проверка, чтобы убедиться, что строка не пуста;
- И, наконец, строка возвращается после интерполяции.
stringsdict
собственные спецификаторы формата уже присутствуют, поэтому нет необходимости их заменять; во-вторых, что более важно, если вы попытаетесь сделать это в нынешнем виде, то локализация множественного числа перестанет работать.Дело в том, что при вызове localizedString (forKey: значение: таблица:) для формы множественного числа, когда данные взяты из Локализуемый.
stringsdict, возвращает объект внутреннего типа __NSLocalizedString .
Это необходимое условие для корректной инициализации возвращаемой строки в случае форм множественного числа.
В свою очередь, используя опцию .
regularExpression при звонке replaceOccurities(of:with:options:) приводит к созданию новой строки с типом Нить в процессе замены вхождений шаблона регулярного выражения, и метод вернет именно это, а не строку типа __NSLocalizedString .
В итоге общая схема работы с локализацией выглядит так:
- Поставлена задача разработать фичу;
- Разработчики добавляют в Lokalise необходимые для макетов ключи с базовым переводом на английский, аналитики отправляют их на перевод на другие языки;
- Разработчик, запустив функцию в работу, запускает скрипт обновления проекта, в ходе которого загружаются файлы локализации для среды отладки, из которых генерируется файл.
Локализация.
swift ;
- Фича доработана, код проверен, отправлен на тестирование и исправлены все найденные ошибки;
- Аналитики добавляют полученные переводы на другие языки в Lokalise;
- После того как функция протестирована и готова к выпуску, ключи вручную развертываются в производственной среде Lokalise аналитиками из команды, ответственной за эту функцию;
- При сборке релизной версии приложения значения ключей загружаются из производственной среды Lokalise;
- Если существующие ключи впоследствии обновляются, пакет локализации загружается при каждом запуске приложения.
Проблемы и их решения
Несмотря на все преимущества удаленной локализации, она не лишена проблем.
Разная локализация на iOS и Android
Проблема в том, что некоторые вещи, касающиеся локализации, в iOS и Android выполняются по-разному.Например, в iOS заполнитель для строкового аргумента записывается как %@ , тогда как в Android это похоже на %s .
Для таких случаев, исходя из данных об ожидаемом распределении пользователей по платформам, было принято решение сначала локализовать Android в Lokalise, а на клиенте заменить его на iOS.
Критические изменения в файле локализации
Часто возникали ситуации, когда кто-то допускал ошибки в локализованной строке.Например, я использовал двойные кавычки без экранирования.
В этом случае файл локализации становился недействительным и локализация вообще не работала.
Нас такая ситуация не устроила и было решено после скачивания проверить файлы локализации, код для которых был приведен выше.
Это позволяет нам не только узнать, что локализация недействительна, но и идентифицировать файл и строку с ошибкой.
Локализация не была перенесена
Как говорилось ранее, у нас есть 2 «экземпляра» локализации — отладочная и продакшн.Изменения в отладочную версию могут вносить те, у кого есть к ней доступ, а в продакшене локализация происходит посредством миграции — специального процесса, передающего необходимые ключи из отладочной в продакшн.
Иногда бывают ситуации, когда мы делаем релизную сборку, но в рабочей копии еще нет некоторых ключей.
Поскольку мы не хотим попасть в ситуацию, когда у пользователей нет локализации, нам нужно как-то проверить наличие необходимых ключей.
В этом нам помогла генерация кода из предыдущей главы: при загрузке в проект локализации для релизной версии приложения генерируется новый файл Локализация.
swift , а при сборке просто получаем ошибку компиляции со списком недостающих ключей.
Решение может быть спорным, но оно гарантирует надежную синхронизацию используемых ключей и их значений из требуемой среды.
Заключение
И вот, в итоге, для локализации мы выбрали сервис Lokalise, используя наш прокси-сервер для генерации и загрузки пакета локализации.Мы скачиваем его при сборке и на его основе генерируем неизменяемую структуру с ключами, а при необходимости также скачиваем ее при каждом запуске приложения.
Конечно, были некоторые проблемы, такие как платформозависимый синтаксис плейсхолдеров, ошибки в файле локализации и синхронизации отладочной и рабочей версий локализаций, но нам удалось с ними справиться.
Возможно, это не последние проблемы, с которыми мы столкнемся при дальнейшем развитии проекта, но тем интереснее их решать.
Спасибо за уделенное время, надеемся, что данная статья оказалась полезной и позволила вам узнать что-то новое или породила новые мысли в решении проблемы локализации.
Вот и все, ребята! Теги: #iOS #разработка iOS #Разработка мобильных приложений #Swift #скрипты #локализация продукта #локализация #vivid.money #lokalise
-
Microsoft Купила Novell Ext2/3/4
19 Dec, 24 -
Социальные Ботнеты
19 Dec, 24