Домой Android разработкаОрганизация кода и DevOps Локализация Android приложения

Локализация Android приложения

by dilix
Локализация Android приложений

Ваше приложение рассчитано больше чем на одну страну? Тогда вам будет необходима локализация (перевод вашего приложения на другие языки).

Как устроена локализация в Android

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

Строки файлов в Android приложении

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

Хочешь обсудить Android разработку?
Заходи к нам Вконтакте, на Facebook и в Телеграм!

Добавить комментарий

Может быть интересно

Этот сайт использует Cookie файлы для улучшения вашего пользовательского взаимодействия. Используя данный сайт вы соглашается с этим. Принять Читать

Политика конфиденциальности и Cookies