Ваше приложение рассчитано больше чем на одну страну? Тогда вам будет необходима локализация (перевод вашего приложения на другие языки).
Как устроена локализация в 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 разработчика. А если говорить о еще большей кастомизации интерфейса, а не только про перевод на другой язык, то можно затронуть смену темы в приложении.