На практике часто приходится выпускать приложения сразу на Android и iOS, а иногда и на Windows Phone. В этом случае некоторые разработчики решают проблему локализации напролом, переводя каждую платформу по отдельности. Согласитесь, не самый лучший поход. На момент, когда я задался решением этой проблемы, в сети уже можно было найти утилиты по загрузке строковых ресурсов из Google Sheets, но большая их часть выглядела как минимум топорно. Меня такой расклад категорически не устраивал. В результате на свет появился LocоLaser — простая в использовании, но очень умная утилита на Java. На протяжении пары лет я использовал её исключительно в своих рабочих проектах. За это время она успела обрасти достаточно богатым функционалом, и теперь готова быть представленной на суд общественности.
В этой статье я расскажу о том что из себя представляет LocoLaser и как как интегрировать его в свой проект. Для начала разберем возможности утилиты, а затем перейдем к более конкретным вещам, таким как Gradle плагин для Android и Bash скрипты для iOS. Я достаточно ленивый программист, чтобы из раза в раз делать одни и те же действия, поэтому все сделано так, что после первоначальной настройки ваша дальнейшая работа будет сводиться лишь к запуску нужной задачи, когда это потребуется. Для переводчиков же, перевод выглядит совсем просто, мы расшариваем для них гугл таблицу со строками и они вписывают перевод в соответствующие колонки. Если нужно добавить язык, просто добавляется еще одна колонка с новым языком. Google Sheets позволяет индивидуально настроить доступ к редактированию таблицы, поэтому каждый переводчик имеет доступ только к колонкам со своими языками.
Файл конфигурации
Но вернемся к утилите. Если чересчур упростить, сама она представляет из себя обычный jar файл. Скачать его можно с Bintray репозитория. Самым простым способом запуска утилиты будет запуск из консоли. Консольная команда может выглядеть следующим образом:
java -jar loco-laser-google.jar localization_config.json
Замечание №1
Для выполнения команды, на машине должна быть установлена Java.
Замечание №2После выполнения команды будут созданы соответствующие файлы ресурсов и разложены по нужным папкам. В качестве первого параметра должен быть передан путь к файлу конфигурации. В приведенном выше примере это файл
Еще раз замечу, для использования утилиты в своих проектах скачивать jar напрямую не нужно. Для этих целей есть готовые скрипты и плагины.
"localization_config.json"
. Именно он отвечает за настройку всех тонкостей импорта и экспорта. Файл представляет из себя текстовый JSON который может выглядеть, например, вот так:{
"platform": "android",
"source": {
"id":"1JZxUcu30BjxLwHg12bdHTxjDgsGFX9HA9zC4Jd8cuUM",
"column_key":"key",
"column_locales":["base", "ru"]
}
}
Этот пример содержит минимум параметров для запуска. Давайте разберем что же они означают.
Параметр
"platform"
указывает платформу для которой будет произведен импорт. Возможен один из 2-х вариантов: "android"
и "ios"
(Windows Phone пока не поддерживается, но это лишь вопрос времени). Помимо формата, платформа отвечает за расположение файлов с ресурсами, их имена, а также место для складирования временных файлов.Параметр
"source"
— это JSON объект. Его содержимое отвечает за то, откуда и как будут загружены строки. Минимальным набором параметров для него будет: "id"
, "column_key"
и "column_locales"
.В качестве
"source.id"
должен быть указан идентификатор Google таблицы. Для того чтобы найти этот идентификатор нужно взглянуть на URL к таблице. Он должен выглядеть следующим образом docs.google.com/spreadsheets/d/1JZxUcu30BjxLwHg12bdHTxjDgsGFX9HA9zC4Jd8cuUM. В данном примере идентификатором будет "1JZxUcu30BjxLwHg12bdHTxjDgsGFX9HA9zC4Jd8cuUM"
. Кстати, ссылка кликабельна и по ней вы можете увидеть реальный пример таблицы.С остальными параметрами все достаточно просто,
"source.column_key"
содержит имя колонки с ключами, а "source.column_locales"
массив имен колонок со значениями, где название колонки также является локалью. Для базовой локали зарезервировано слово "base"
. Тут стоит сделать небольшое отступление и определиться со структурой таблицы и заодно разобраться с тем, что такое имена колонок. Если просто, то LocoLaser воспринимает первую «не вычеркнутую» строку как строку с именами колонок. Вы спросите «Что еще за вычеркнутая строка?». Дело в том, что самая первая колонка в таблице используется для служебных целей. В настоящий момент можно «вычеркнуть» строку добавив в первой колонке символ
"-"
. «Вычеркнутая» строка полностью игнорируется и никак не участвует в локализации. Если посмотреть на таблицу по ссылке выше то вы увидите, что первая строка с «человеческими» заголовками «вычеркнута» и является лишь декоративным элементом таблицы. Также можно «вычеркивать» строки с ресурсами, например, когда строка нигде не используется, а удалять жалко.Полный перечень параметров
Вышеописанных параметров достаточно для работы утилиты. Однако, есть и другие. Я не буду подробно описывать каждый, а лишь приведу их полный перечень в виде таблиц.
Source
Параметр | Тип | Описание |
---|---|---|
id |
String | ID таблицы Google Sheet. URL к таблице содержит этот идентификатор (https://docs.google.com/spreadsheets/d/*sheet_id*). Обязательный параметр. |
column_key |
String | Имя колонки с ключами. Обязательный параметр. |
column_locales |
Strings array | Массив имен колонок со значениями, где название колонки также является локалью. Для базовой локали следует использовать колонку с именем "base" . Обязательный параметр. |
column_quantity |
String | Имя колонки с количественными значениями. Ячейки в таблице должны содержать одно из следующих значений: zero , one , two , few , many , other . Пустая строка воспринимается как other. Необязательный параметр. По умолчанию количественные значения не используются. |
column_comment |
String | Имя колонки с комментариями. Необязательный параметр. По умолчанию комментарии в ресурсный файл не записываются. |
worksheet_title |
String | Название листа в Google Sheet таблице. Необязательный параметр. По умолчанию используется первый лист. |
credential_file |
String | Путь к файлу содержащему идентификационные данные для OAuth авторизации. При использовании относительного пути, путь к файлу указывается относительно рабочей директории. Необязательный параметр. |
type |
String | Тип источника данных. Должен быть "googlesheet" . Необязательный параметр. Однако, если вы хотите быть уверенным, что конфигурационный файл и утилита работаю с одним и тем же типом источника, вам стоит указать его тип. |
Как я говорил ранее, платформа задает ряд параметров специфичных для нее. Эти параметры можно изменить, если определить платформу не в виде строки, как делалось выше, а в виде JSON объекта. Вот перечень возможных свойств этого объекта:
Параметр | Тип | Описание |
---|---|---|
type |
String | Тип платформы. Возможны следующие значения: "android" или "ios" . Обязательный параметр. |
res_name |
String | Имя ресурсного файла без расширения. Расширение выбирается в зависимости от типа файла. Необязательный параметр. Значения по умолчанию: Android — "strings" iOS — "Localizable" |
res_dir |
String | Путь к директории с ресурсами. Необязательный параметр. Значения по умолчанию: Android — "./src/main/res/" iOS — "./" |
temp_dir |
String | Путь к директории для хранения временных файлов. Необязательный параметр. Значения по умолчанию: Android — "./build/tmp/" iOS — "../DerivedData/LocoLaserTemp/" |
Важно!Прочие параметры
Все относительные пути указываются относительно рабочей директории. По умолчанию в качестве рабочей директории используется директория файла конфигурации.
Конфигурация может содержать еще несколько параметров, кроме платформы и источника. Все они могут значительно повлиять на полученный результат:
Параметр | Тип | Описание |
---|---|---|
work_dir |
String | Путь к рабочей папке. Все относительные пути указываются относительно этой папки. По умолчанию используется директория файла конфигурации. |
force_import |
Boolean | Утилита запоминает состояние ресурсов при последнем выполнении и старается не запускать импорт без лишней необходимости. Чтобы игнорировать это и выполнять импорт всегда в полном объеме установите force_import равным true . Значение по умолчанию false. |
conflict_strategy |
String | Определять способ разрешения конфликтов при объединении ресурсов платформы и ресурсов из Google Sheets. Возможны 3 варианта:
keep_new_platform . |
duplicate_comments |
Boolean | При duplicate_comments = false комментарий не будет добавлен в ресурсный файл, если этот комментарий равен локализованной строке. Значение по умолчанию false . |
delay |
Long | Время в минутах определяющее минимальное время до следующей локализации. Локализация не будет выполняться чаще, чем указано в параметре delay. Если используется force_import задержка игнорируется. |
Параметр
duplicate_comments
особенно полезен, если вы используете в качестве комментариев базовую локаль. В этом случае, в локализованных ресурсах всегда будет комментарий с текстом на базовом языке (обычно английский), а в базовом ресурсе комментарии будут отсутствовать, так как в этом случае они повторяют значения строки и являются излишними.Пример Android ресурсов при duplicate_comments = false
/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">LocoLaser example</string>
<string name="screen_main_app_description">This is example application of how to use the LocoLaser.</string>
<string name="screen_main_plural_example_title">Plural string examples</string>
</resources>
/values-ru/strings.xml<?xml version="1.0" encoding="utf-8"?>
<resources>
/* LocoLaser example */
<string name="app_name">LocoLaser пример</string>
/* This is example application of how to use the LocoLaser. */
<string name="screen_main_app_description">Это пример приложения по использованию LocoLaser.</string>
/* Plural string examples */
<string name="screen_main_plural_example_title">Примеры Plural строк</string>
</resources>
Пример кастомизированной конфигурации localization_config.json
{
"platform": {
"type":"android",
"res_name":"strings_intro"
},
"source": {
"type":"googlesheet",
"id":"1JZxUcu30BjxLwHg12bdHTxjDgsGFX9HA9zC4Jd8cuUM",
"column_key":"key",
"column_locales":["base", "ru"],
"column_comment":"base",
“worksheet_title”:”Strings intro”
},
"force_import":true,
"conflict_strategy":”keep_new_platform”,
"delay":60,
}
Консольные аргументы
При запуске локализатора из консоли можно изменить некоторые параметры конфигурации. Для этого добавьте соответствующие аргументы. Важно: путь к файлу конфигурации всегда указывается первым. Порядок остальных аргументов не имеет значения.
Аргумент | Описание |
---|---|
--force или --f |
Флаг. Устанавливает "force_import = true" |
-cs *string conflict strategy* |
Переопределяет свойство "conflict_strategy" |
-delay *long delay* |
Переопределяет свойство "delay" |
Пример команды
java -jar loco-laser-google.jar localization_config.json --f -cs keep_new_platform
Android и Gradle Plugin
При работе с LocoLaser в Android стоит использовать специальный Gradle плагин. Он добавляет в проект несколько задач объединеных в группу «localization». Есть 2 варианта подключения плагина, «классический» и «альтернативный».
Классический способ подключения плагина
Откройте файл
После этого откройте файла
"build.gradle"
корневого проекта и добавьте следующие строки:buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "gradle.plugin.ru.pocketbyte.locolaser:plugin:1.0.1"
}
}
После этого откройте файла
"build.gradle"
модуля приложения и добавьте одну строку в начало файла:apply plugin: "ru.pocketbyte.locolaser"
Альтернативный способ подключения плагина
Этот способ подходит для проектов с версией Gradle выше 2.1 и использует так называемый «incubating» функционал (используйте его на свой страх и риск, предупреждает нас команда Gradle). Для него не нужно ничего прописывать в корневой
Еще раз замечу строка должна быть в САМОМ верху. Ничего другого перед ней быть не должно. Иначе проект даже не синхронизируется.
"build.gradle"
. Откройте "build.gradle"
нужного вам модуля и в самом верху файла добавьте следующую строкуplugins { id "ru.pocketbyte.locolaser" version "1.0.1" }
Еще раз замечу строка должна быть в САМОМ верху. Ничего другого перед ней быть не должно. Иначе проект даже не синхронизируется.
После подключения плагина, одним из вышеописанных способов, в этом же файле, добавьте следующую зависимость:
dependencies {
...
localize 'ru.pocketbyte.locolaser:locolaser-mobile-googlesheet:1.0.+'
}
Теперь остается лишь положить файл конфигурации в папку модуля приложения и можно начинать работу. По умолчанию, в качестве файла конфигурации, плагин использует файла с именем
"localize_config.json"
. После синхронизации проекта в списке Gradle задач появится группа «localization», в ней 3 задачи.localize — Запускает LocoLaser со стандартными параметрами;
localizeForce — Запускает LocoLaser с флагом
"--force"
;localizeExportNew — Запускает LocoLaser с флагом
"--force"
и "conflict_strategy" = "export_new_platform"
.Если вы хотите, чтобы локализация запускалась при каждой сборке, добавите зависимость к задаче preBuild:
afterEvaluate {
preBuild.dependsOn project.tasks.localize
}
Пример Android проекта на GitHub: github.com/PocketByte/locolaser-android-example
Используется Gradle плагин. Локализация встроена в процесс сборки.
Локализация iOS приложений
В отличии от Android в iOS нет такой мощной системы сборки как Gradle, так что приходится использовать обходные методы, благо их предостаточно и они прекрасно вписываются в iOS идеологию. Файл конфигурации я предлагаю хранить в папке с исходниками, обычно название этой папки совпадает с названием проекта (это единственное расположение при котором есть возможность использовать значения по умолчанию).
Итак, файл конфигурации вы создали, и положили в папку с исходниками. Как же его использовать? Поскольку артефакты утилиты расположены на удаленном maven репозитории я пошел по пути написания bash скрипта, имитирующего систему управления зависимостями. Вот он:
localize.command
GROUP="ru/pocketbyte/locolaser"
ARTIFACT="locolaser-mobile-googlesheet"
VERSION="1.0.0"
CONFIG_FILE="localization_config.json"
cd "`dirname \"$0\"`"
ARTIFACTS_DIR="../DerivedData/LocoLaserTemp/artifacts/$GROUP/"
mkdir -p $ARTIFACTS_DIR
ARTIFACT_FILE="$ARTIFACTS_DIR$ARTIFACT-$VERSION.jar"
if [ -f $ARTIFACT_FILE ]
then
echo "Artifact already downloaded"
else
ARTIFACT_URL="https://bintray.com/pocketbyte/maven/download_file?file_path=$GROUP/$ARTIFACT/$VERSION/$ARTIFACT-$VERSION.jar"
echo "Loading: $ARTIFACT_URL"
curl -L -o $ARTIFACT_FILE $ARTIFACT_URL
if [ $? -eq 0 ]
then
echo "Artifact downloaded"
else
exit $?
fi
fi
java -jar $ARTIFACT_FILE $CONFIG_FILE
Этот скрипт скачивает jar файл с удаленного maven репозитория и помещает его в папку
"../DerivedData/LocoLaserTemp/artifacts/"
, после чего запускает на выполнение. В качестве файла конфигурации используется файл с именем "localize_config.json"
, как и в Android. Решение достаточно грубое, придется точно указывать версию используемого артефакта. Однако, со своей задачей оно прекрасно справляется и не нужно хранить в проекте jar файл утилиты. Как можно догадаться по относительным путям, скрипт следует сохранить в ту же папку что и файл конфигурации. У меня в проекте несколько таких скриптов: localize.command, localizeForce.command и localizeExportNew.command. Все эти команды повторяют задачи из Gradle плагина выше.Если вы хотите, чтобы локализация выполнялась при каждом билде, добавьте соответствующий Run scrip:
Run script должен идти сразу за фазой Target Dependencies. Это важно, так как если импорт новых строк произойдет, например, уже после фазы Copy Bundle Resources, нужный результат вы получите с запозданием, только в следующем билде.
Пример iOS проекта на GitHub: github.com/PocketByte/locolaser-ios-example
Используется bash скрипт. Локализация встроена в процесс сборки.
Windows Phone и другие платформы
К сожалению у меня совершенно нет опыта разработки под эту платформу, и в организации, в которой я в настоящий момент работаю, разработка Windows Phone приложений давно не практикуется. Я искренне извиняюсь за размещение статьи в хабе Windows Phone. Однако, я не нашел лучшего способа привлечь внимание Windows Phone разработчиков. Так что если вы разработчик под Windows Phone с большим опытом, и вам есть что мне рассказать по этой теме, пожалуйста, свяжитесь со мной. Также, если у кого-то из читателей появилась идея о применения LocoLaser в других платформах или сервисах, пишите не стесняйтесь, я открыт к предложениям.
Credential file
Для доступа к Google таблицам необходимо пройти OAuth авторизацию. Если в файле конфигурации не задан параметр
"source.credential_file"
, при запуске локализатора автоматически откроется браузер со страницей Google аутентификации. После успешной авторизации утилита продолжит свою работу. В следующий раз производить вход для этой таблицы не потребуется.Так как вышеописанный подход требует вмешательства пользователя, он не подходит для ситуаций, когда процесс локализации должен проходить в автоматическом режиме. Для того чтобы процесс всегда проходил автоматически, следует указать
credential_file
. В качестве credential файла подойдет файл Сервисного аккаунта Google. Для его создания в Google Developers консоли нужно проделать следующие действия:- Создать новый проект, если для вашего приложения он еще не создан.
- Открыть список Сервисных аккаунтов. Если будет предложено выбрать проект, выберите проект своего приложения.
- Кликнуть Создать сервисный аккаунт.
- В окне «Создание сервисного аккаунта» ввести имя нового сервисного аккаунта, включить флаг «Создать новый закрытый ключ», выбрать тип ключа JSON. Роль аккаунту выбирать не нужно. Кликнуть Create.
Сервисный аккаунт будет создан, после чего автоматически загрузится файл закрытого ключа. Путь к этому файлу следует указать в качестве параметра
"source.credential_file"
. Для того, чтобы сервисный аккаунт имел доступ к Google таблице, ее необходимо расшарить для созданного ранее сервисного аккаунта. При расшаривании в качестве Email укажите Идентификатор сервисного аккаунта. Этот идентификатор можно увидеть в списке сервисных аккаунтов, либо подсмотреть в скачанном файле ключа.Пример файла сервисного ключа
{
"type": "service_account",
"project_id": "myapp-1086",
"private_key_id": "b67c2edcc47c7053c035d8681c8eb7e9f4d90c09",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJUV49mSQB2NSO\nw+tfQWq4pP63U4t7W8V6O6E7FABbYS5N4g35nRzVEj5NciqI27shHSKVrsl7U5ji\nM0IIA+vi+dgHXwHfCPhS9d85xZ73fuqFaj29iru+pTq9tuNieLDl60L04oCc1qQKBgQDLsMMnX2r9qkQw01H5L2WRB9R6er+bO4DE\n8Ecpripfd/e7qq89WcGu1H8S+3Jy49dBmN709vPvVcsGQx8mDdYdm4P0WPkKbgTo\nt12OA07uiY6Zn54rW5CXrjdoscPXB94AS2ps4M3/xY5hHTxwtS8yJxoUgTVEfgNB\nDwzNrZSCVEMCoBAIYl6rWGITgNaR0+FLuP+kjZw\nHsLdkU8173J3nhuYhxo7N90BhO08lquIQ7bJAoGBAKLz\nVRxRdFlcdlMNK34K0dkVh4E4Y8K+9oQWqQeKIrHfWpuSr8CH5q+Dpek8qVGKPnFm\n567XRUzJuLLYzbl2xj1HZWf8KbeTTnALKYg8Jz\nxXXvLlZl2OJ8Frr9ry1DEszPkwWwTQJg5bRG7Z//QfpyEZ2PUvpCNVVpeRuMmUhv\nu5rSLa0G+C97/XIGz/O/1ME9WXU6ZNRwwDkSDw6L7AIrXY8V+8pIRL9e0ks4Uw/A\n6ACYrlYMYYAIl79MNrUrizvF5KwxLiohHJ5KVpThGuRZDaidCPp9BL/h8tfhXPel\nwQot9dM8P4CmQNR/fMpytQSVk7vv95B2JHrt6QmIsQKBgQD9BJ8gfZUVhlxtuaWO\nMKl1PjiD+YKhz4rmIZUKM84xphsGYUBhH29s1zb98u9vlEnlx3bGUtDakNnjTDQp\nagQv22+6STL+0s1haOxyfbi1jIzXvzh47yij6+v7WEIdNj45WV9kpcFTi2oUXURt\nEi3WskYTijYGbDNQmpG2kmY\nDz0KdeTFJxsnFstTT/VozEGvNIHf+8PhKv0123dBFuqSgBD5SFHDp3tQ2IzC81Jm\n5FJLldk3Hw1QRh6+WiEJBTX6nFU4DB5tVXKhbPOvhqYwI/CUYWWbVBQCQgqURcyr\nUdRxAHyDrxKhNrmXXKAEwR0rDz2uGTQCfJ0Zyk/Z1E7iDl/SDfYSSD70wAgGblH2\nAAIQzeoPAgMBAAECggEAb7Trswhft3qmb1V9LEzzN+OtxvHfqqKAkFO4Ijz1+b6R\n3/t4P7KTRhOqaHTZ7zjlu/kbsKzc9casRY+lqybp4/c4jNaGBklG6Vmu96E9wKBgEFPsRe88v/UaAV213Jlw0hdYE9I19yW6z7OSl+Q0dflDbLO5cRs\nTeHlh+9zhzThLVYf79vvwrO4klXm9Mv/7sa/uQ54GK7IkXVklSxUoZpThoOme5hT\n+8ScgJSnyqEpwFQjaslbNBUxtpc9IA2bseP1S7aCVDfZtEp7rHqOFZTvSgol5YD/AoGAdIMNKvg0IiS2xcIEbyHa\nPTNrUfpHeJk/P+Frr1cmDHhGe1l0FWNg9EDlhItAW5EP15ubZPdWQRrV7RuydJlc\ngWesc2RLPIG8+so1TpG1F62+tsQ9lGSF5imiew1x7sQ3H0VIpGtSfTvSMep6fuE7\niuSVbY0UnpxGnqzo9TBAYS4=\n-----END PRIVATE KEY-----\n",
"client_email": "localizator@myapp-1086.iam.gserviceaccount.com",
"client_id": "704739729071909788554",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/localizator%40myapp-1086.iam.gserviceaccount.com"
}
"client_email"
— это и есть идентификатор аккаунта.Заключение
Еще раз замечу, что архитектура утилиты разработана таким образом, что может быть легко расширена или дополнена, источник и платформа работают независимо. Например, при желании, можно дополнить список источников и в качестве source использовать базу данных или какой либо Web сервис, имеющий подходящее для этого API. Так что на Google Sheets свет клином тут не сошелся, и сторонники «антигуглизации» смогут найти для себя подходящее решение.
Также хотелось бы поблагодарить Инну(Foenix) за помощь в написании статьи. Без нее статья получилась бы не такой доходчивой и понятной. Инна, спасибо тебе за твои советы и замечания.
На этом закончу. Спасибо, что уделили время и ознакомились с моим творением. Буду рад познакомиться с вашим мнением, предложениями или критикой в комментариях. И не забываем жать стрелки вверх или вниз, для меня это очень важно, уж очень хочется знать как Хабр сообщество приняло мою работу.
Ссылки на исходники:
> Исходный код LocoLaser на GitHub
> Исходный код Gradle плагина на GitHub
> Пример для Android
> Пример для iOS
> Ссылка на Bintray
> Пример таблицы
Поделиться с друзьями