Ваше приложение рассчитано больше чем на одну страну? Тогда вам будет необходима локализация (перевод вашего приложения на другие языки).
Как устроена локализация в Android
Пару слов теории. Строки в Android проекте обычно хранятся в файликах strings.xml, которых может быть несколько. Количество зависит от поддерживаемых языков.

Папка values содержит значения по умолчанию. Значения именно из нее будет показаны, если нет доступных переводов в других папках. values-XX будут использованы, если XX удовлетворяет текущему модификатору устройства. Это может быть как локаль, так и размер экрана и т.д.
На значения из строковых файлов можно ссылаться прямо из верстки
android:text="@string/my_key_for_string"
либо же из кода
context.getString(R.string.my_key_for_string
Синхронизация разработчика с переводчиком
На больших проектах переводчик и разработчик — не один и тот же человек. Переводчику не очень удобно работать с исходниками, а разработчику — получать от него Excel со строками. Для этой задачи существуют разные сервисы и локальные программы.
Из ряда опробованных мной, больше всего понравился POEditor, в него загружаются ключи для перевода. Потом, при создании нового языка в проекте, останется только добавить переводы и выгрузить в проект.
Для разработчика удобно, что можно подключить Github и выкачивать туда-обратно ключи и строки. Но! Можно сделать еще круче, если есть сервер, на котором вы собираете приложения. (А в идеале он должен быть, как настроить все это дело писал в отдельной статье).
Настройка автоматической локализации на CI сервере
Можно настроить, чтобы перед каждой сборкой (как и на CI, так и просто локально) каждый раз выкачивались все переводы. А уже после этого можно и собирать приложение. Самый простой способ — сделать это с помощью gradle task. Вот пример, как сделать это с помощью API от POEditor. По большому счету можно использовать любой сервис, который может отдать переводы в нужном формате.
task('downloadTranslations') {
ext.downloadPOTranslation = { stringsFile, projectId, lang ->
def f = new File("app/src/main/res/" + stringsFile)
if (f.exists()) {
println("Delete file: " + f.absolutePath)
f.delete()
}
println("Get lang: " + lang + " for project: " + projectId)
def requestArray = ["curl",
"-X", "POST",
"-d","api_token=API_TOKEN",
"-d", "id=" + projectId + "",
"-d", "language=" + lang + "",
"-d", "type=android_strings",
"https://api.poeditor.com/v2/projects/export",]
println(requestArray)
def request = requestArray.execute()
request.waitFor()
def response = request.text
if (response.isEmpty()) {
def errorResponse = request.err.text
println("Error response from server:")
println(errorResponse)
} else {
println("Response from server:")
println(response)
}
def json = new JsonSlurper().parseText(response)
def transFile = json['result']['url']
println("Download file: " + transFile)
new URL(transFile).withInputStream { i -> f.withOutputStream { it << i } }
// From here we assume that our main project contains android-user-sdk
// as an our own SDK that need to be localized as well
def sdkFile = new File('android-user-sdk/sdk/src/main/res/' + stringsFile)
if (sdkFile.exists()) {
println("Delete file: " + sdkFile.absolutePath)
sdkFile.delete()
} else {
println("File: " + sdkFile.absolutePath + " does not exist")
}
println("Copy file to SDK lib into folder:")
println(sdkFile.absolutePath)
f.withInputStream { i -> sdkFile.withOutputStream { it << i } }
}
doLast {
downloadPOTranslation("values/strings.xml", "250751", "en-gb")
downloadPOTranslation("values-fr/strings.xml", "250751", "fr-ch")
downloadPOTranslation("values-de/strings.xml", "250751", "de")
downloadPOTranslation("values-it/strings.xml", "250751", "it-ch")
// Download strings from different POeditor project.
downloadPOTranslation("values/strings_zoom_en.xml", "298821", "en")
downloadPOTranslation("values-fr/strings_zoom_fr.xml", "298821", "fr-ch")
downloadPOTranslation("values-de/strings_zoom_de.xml", "298821", "de")
downloadPOTranslation("values-it/strings_zoom_it.xml", "298821", "it-ch")
downloadPOTranslation("values/strings_idenfy_en.xml", "294287", "en")
downloadPOTranslation("values-fr/strings_idenfy_fr.xml", "294287", "fr-ch")
downloadPOTranslation("values-de/strings_idenfy_de.xml", "294287", "de-ch")
downloadPOTranslation("values-it/strings_idenfy_it.xml", "294287", "it-ch")
}
}
Пару уточнений по конфигу.
- В примере блок из подгрузки 4 языков повторен 3 раза. Это нужно, т.к. в проекте используются 3 POEditor проекта, который отвечает за разные части
- Приложение из примера состоит из основного модуля + собственный SDK как подмодуль, по-этому реализовано копирование переводов в папку подпроекта.
- Задача
downloadTranslationsзапускается не автоматически, ее нужно принудительно запустить как отдельный шаг сборки. С поправкой на OS это./gradlew app:downloadTranslations
(Updated) Использование Lokalise. Переезд.
Существуют множество разных инструментов для локализации, одно время мы использовали POEditor вот переехали на lokalise. Как оказалось он дешевле и мощнее. Переезд оказался простым. С продуктовой точки зрения это означало выгрузку переводов из POEditor и загрузку в Lokalise. С технической же точки зрения поменялся немного скрипт выгрузки:
task('downloadTranslations') {
ext.downloadPOTranslation = { Void ->
println("Download translations for project")
def requestArray = ["curl",
"-X", "POST",
"-H","x-api-token: API_KEY",
"-H", "content-type: application/json",
"-d", "{\"format\":\"xml\",\"original_filenames\":true, \"all_platforms\":true, " +
"\"include_pids\": [\"ADDITIONAL_PROJECT_1\", \"ADDITIONAL_PROJECT_2\"]}",
"https://api.lokalise.com/api2/projects/PROJECT_KEY/files/download",]
println(requestArray)
def request = requestArray.execute()
request.waitFor()
def response = request.text
if (response.isEmpty()) {
def errorResponse = request.err.text
println("Error response from server:")
println(errorResponse)
} else {
println("Response from server:")
println(response)
}
def json = new JsonSlurper().parseText(response)
def transFile = json['bundle_url']
println("Download file: " + transFile)
def tempFile = new File("translationZip.zip")
new URL(transFile).withInputStream { i -> tempFile.withOutputStream { it << i } }
def resourceDir = new File("app/src/main/res/")
println("Unzip file: " + tempFile.absolutePath)
println("Unzip to: " + resourceDir.absolutePath)
copy {
from(zipTree(tempFile.absolutePath))
into(resourceDir.absolutePath)
}
def sdkFile = new File('android-user-sdk/sdk/src/main/res/')
println("Unzip to SDK: " + sdkFile.absolutePath)
copy {
from(zipTree(tempFile.absolutePath))
into(sdkFile.absolutePath)
}
println("Remove temp file: " + tempFile.absolutePath)
tempFile.delete()
}
doLast {
downloadPOTranslation()
}
}
Lokalise возвращает не просто файл, а архив целиком. Получаем, распаковываем… Профит!
Загрузка строк из нескольких проектв
Оказалось очень удобно, что одним запросом можно выгрузить скомпонованный результат из нескольких проектов. Добавив параметр include_pids сервис сам смержит необходимые проекты.
Вот. Теперь все довольны. Переводы всегда попадают в последнюю сборку, а разработчику не надо каждый раз синхронизировать код с табличками в Excel. Эта статья, кстати, неплохо дополняет список инструментов для Android разработчика. А если говорить о еще большей кастомизации интерфейса, а не только про перевод на другой язык, то можно затронуть смену темы в приложении.

