LocoLaser: переводим приложения в Google Sheets



    На практике часто приходится выпускать приложения сразу на 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". Необязательный параметр. Однако, если вы хотите быть уверенным, что конфигурационный файл и утилита работаю с одним и тем же типом источника, вам стоит указать его тип.
    Platform
    Как я говорил ранее, платформа задает ряд параметров специфичных для нее. Эти параметры можно изменить, если определить платформу не в виде строки, как делалось выше, а в виде 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 варианта:
    • remove_platform — Удаляет локальные ресурсы платформы полностью заменяя их ресурсами из Google Sheets.
    • keep_new_platform — Сохраняет строки платформы если они отсутствуют в Google Sheet.
    • export_new_platform — Сохраняет новые строки платформы и загружает их на Google Sheet.
    Значение по умолчанию 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.1.+'
    }
    

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

    1. Создать новый проект, если для вашего приложения он еще не создан.
    2. Открыть список Сервисных аккаунтов. Если будет предложено выбрать проект, выберите проект своего приложения.
    3. Кликнуть Создать сервисный аккаунт.
    4. В окне «Создание сервисного аккаунта» ввести имя нового сервисного аккаунта, включить флаг «Создать новый закрытый ключ», выбрать тип ключа 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
    Пример таблицы
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.