Заключительная статья об интеграции Serge+Smartcat. В этой статье я расскажу, как мы масштабируем Serge на всю компанию, рассмотрю 4 нестандартных интеграции и, в качестве бонуса, расскажу о 2 фичах которые могут упростить вам жизнь.

Предыдущие статьи:

20 проектов, 20 языков, срок вчера
20 проектов, 20 языков, срок вчера. Часть 2

Масштабируемость


В прошлой статье я рассказывала, как настроить Serge для одного репозитория. В нашей компании мы имеем несколько десятков репозиториев, которые нуждаются в переводах, поэтому был выделен отдельный сервер для локализаций. Файловая структура и окружение на нем полностью идентичны тому, что описано в предыдущей статье. Для каждого репозитория используется свой инстанс Serge. Чтобы не выполнять команды вручную, каждому инстансу соответствует крон, который последовательно запускает команды Serge: получение новых строк из репозитория, получение новых переводов, парсинг, отправка новых строк в Smartcat и отправка новых переводов в Gitlab.

Варианты интеграций


Два набора языков в одном репозитории


Начнем с самого простого случая. Представьте, что в вашем репозитории есть несколько наборов ресурсных файлов. Например, строки для клиента и API приложения хранятся в одном репозитории, но в разных директориях. Клиент переводится на 20 языков, API — на 6.

Задача: организовать независимую поставку переводов в каждую из директорий.
Решение:

  1. Настроить 2 проекта в Smartcat: на 6 языков и на 20.
  2. Настроить 2 проекта на сервере локализаций.
  3. В первом проекте в файле project1.cfg добавить строку our $unmerged_branch_mask = '^(translateAPI-)'; # process unmerged branches matching this mask, где “translateAPI-” это префикс названия ветки. Префикс будет указывать Serge, что в данной ветке нужны переводы в директории API.
  4. В файле project1.serge.tmpl в параметре source_dir указать путь до ресурсных файлов в директории API.
  5. Аналогично, для второго проекта в файле project2.cfg добавить строку our $unmerged_branch_mask = '^(translateCLIENT-)'; # process unmerged branches matching this mask, где “translateCLIENT” это префикс для веток этого проекта. Префикс будет указывать Serge, что в данной ветке нужны переводы в директории Client.
  6. В файле project2.serge.tmpl в параметре source_dir указать путь до ресурсных файлов в директории CLIENT.

Обратите внимание, префиксы должны быть уникальными среди всех проектов, настроенных для одного репозитория.

Итого, мы имеем 2 проекта в Smartcat и 2 соответствующих им проекта на сервере локализаций. Оба проекта смотрят на один репозиторий в Gitlab, но в разные директории. Serge по префиксу ветки понимает, какие именно строки ему надо отправить на перевод. Для вычисления diff-а используется одна и та же ветка base-translate.

Локализация Swagger


В нашей компании все продукты, включая документацию, локализованы. Сейчас мы внедряем автогенерацию документации из swagger, и перед нами встала необходимость его локализовать.
Задача: локализовать swagger с минимальными усилиями.

Решение: В файле myproject.tmpl.serge в объект parser добавляем объект data и перечисляем в нем те поля, значение которых надо извлечь и отправить на перевод:

parser {
            plugin                  parse_json
            data
            {
             path_matches          \/(summary|description)$
            }
        }

Аналогичная задача: надо переводить тексты из файла, но не все, а только юридические. Прочие тексты поставляет команда маркетологов. Чтобы не усложнять структуру, и не создавать дополнительный файл для юридических текстов, ключи всех юридических строк получили префикс “legal”:

parser {
            plugin                  parse_json
             data
            {
             path_matches          ^\/legal\..*
            }
        }

Тонкости юридических переводов


Еще один интересный кейс. У нас есть юридический документ, условия которого разнятся в зависимости от страны. Но, тем не менее, это одно приложение и ресурсные файлы лежат в одной директории.

Задача: в рамках одного проекта перевести несколько документов, причем каждый документ должен быть переведен на один конкретный язык.

Что было сделано:

  1. Для каждой страны была создана соответствующая директория, внутри которой лежал релевантный этой стране исходный файл на английском языке.
  2. Путь для переменной source_dir указан до общей директории с ресурсными файлами.
  3. Включаем поиск ресурсных файлов во всех вложенных директориях: source_process_subdirs YES
  4. Добавляем в список вызываемых плагинов новый плагин, который позволяет отправлять каждый конкретный ресурсный файл на нужный язык. В качестве ориентира используем название директории, где он лежит:

callback_plugins {
            :feature_branch {
                plugin              feature_branch
 
                data {
                    master_job      job.base-translate
                }
            }        
            :limit_languages
            {
                plugin              limit_languages
                data
                {
                    # all rules are processed top to bottom; each rule can add or remove languages
                    # so the most priority rules are placed at the bottom
                    if
                    {
                        # by default, don't localize
                        file_matches                 .
                        then
                        {
                            exclude_all_languages    YES
                        }
                    }
 
                    if
                    {
                        file_matches                 de-au\/
 
                        then
                        {
                            include_languages        de-AT
                        }
                    }
 
                    if
                    {
                        file_matches                 li-LI\/
 
                        then
                        {
                            include_languages        li
                        }
                    }
 
                    if
                    {
                        file_matches                 pt\/
 
                        then
                        {
                            include_languages        pt-BR
                        }
                    }
 
                    if
                    {
                        file_matches                 zh-Hans\/
 
                        then
                        {
                            include_languages        zh-Hans
                        }
                    }
 
                    # and so on..
                }
            }

Локализация при хранении строк в БД


В нашей системе есть часть кода, которая хранит переводы в БД, и по ряду причин переехать на ресурсные файлы в репозитории не может. Тем не менее, нам необходимо иметь возможность быстро и в автоматическом режиме поставлять переводы.

Задача: Организовать процесс непрерывных локализаций, если строки хранятся не в репозитории, а в БД.

Решение:

  1. Создать репозиторий, в нем собрать и сгруппировать по удобному нам принципу (по количеству языков перевода или по продуктам) все строки из БД.
  2. Создать проект в Smartcat.
  3. Запустить стандартный цикл непрерывных локализаций.
  4. Ветки переводов мержить в ветку base-translate.
  5. По крону проверять значение хеша последнего коммита в base-translate. Если хеш изменился, то есть, были смержены новые переводы, парсить diff между старым и текущим хешом, и отправлять новые/измененные строки в БД.

Бонусные фичи


Оповещения


Базовые оповещения Smartcat нам не подходили, так как каждая команда хочет получать уведомления только о своих ветках и только о полной готовности переводов во всех ресурсных файлах продукта.

Было принято решение отталкиваться от наличия всех переводов в репозитории и, если они полностью готовы, отправлять оповещения в корпоративный мессенджер, в нашем случае это Google Chat.

Задача: организовать оповещения в репозитории, куда могут коммитить 8 команд, дублировать все оповещения в канал отдела технического документирования.

Решение:

  1. Договориться с каждой из команд, что название веток должно содержать название команды. По-прежнему использовать префикс translate- для обозначения веток, нуждающихся в переводе.
  2. Создать pipeline, который запускается только для веток с префиксом translate-.
  3. В pipeline определять какой команде принадлежит ветка, проверять наличие строк с пустым value, и, если их нет, отправлять уведомления о готовности в соответствующий канал. Так как код достаточно объемный, то я его вынесла в скрипт.

CI


check-translations:
  stage: check-translations
  image: node:8.14.0
  tags:
  - devops
  script:
    - chmod +x ./notification.sh
    - ./notification.sh
  only:
      - base-translate
      - /^translate.*$/
  when: always

Скрипт оповещений


#!/bin/bash
hangouts(){
  curl -X POST --max-time 180 -H "Content-Type: application/json; charset=UTF-8" --data "{
\"cards\": [{\"header\": {\"title\": \"LOCALIZATION  IS READY\",\"subtitle\": \"REPOSITORY NAME\",\"imageUrl\": \"https://avatanplus.com/files/resources/mid/5775880ee27f8155a31b7a50.png\"},\"sections\": [{\"widgets\": [{\"keyValue\": {\"topLabel\": \"Translation is finished in the branch\",\"content\": \"$1\"}}]},{\"widgets\": [{\"buttons\": [{\"textButton\": {\"text\": \"SEE COMMIT\",\"onClick\": {\"openLink\": {\"url\": \"https://gitlab.loc/common/publisher-client/commit/$2\"}}}}]}]}]}]}" "$3" || true
}
cd app/translations
if echo "$CI_COMMIT_REF_NAME" | grep "commandname1";
  then
      grep -rl '\:\s\"\"' *.json >> result.file
    if [ -s network.file ];
      then
        echo "Translations are not ready";
        cat result.file
      else
        hangouts $CI_COMMIT_REF_NAME $CI_COMMIT_SHA $HANGOUTS_NOTIFICATIONS_COMMAND_NAME_1
        hangouts $CI_COMMIT_REF_NAME $CI_COMMIT_SHA $HANGOUTS_NOTIFICATIONS_DOC
    fi
  fi
if echo "$CI_COMMIT_REF_NAME" | grep "commandname2";
  then
        grep -rl '\:\s\"\"' *.json >> result.file
    if [ -s result.file ];
      then
        echo "Translations are not ready";
        cat result.file
      else
        hangouts $CI_COMMIT_REF_NAME $CI_COMMIT_SHA $HANGOUTS_NOTIFICATIONS_COMMAND_NAME_2
        hangouts $CI_COMMIT_REF_NAME $CI_COMMIT_SHA $HANGOUTS_NOTIFICATIONS_DOC
    fi
  fi
...
if echo "$CI_COMMIT_REF_NAME" | grep "commandname8";
    then
        grep -rl '\:\s\"\"' *.json >> result.file
    if [ -s result.file ];
      then
        echo "Translations are not ready";
        cat result.file
      else
        hangouts $CI_COMMIT_REF_NAME $CI_COMMIT_SHA $HANGOUTS_NOTIFICATIONS_COMMAND_NAME_8
        hangouts $CI_COMMIT_REF_NAME $CI_COMMIT_SHA $HANGOUTS_NOTIFICATIONS_DOC
    fi
  fi

Назначения переводчиков через Smartcat API




Так выглядит наш менеджер по локализации, когда настает время назначить все ветки на перевод.

В среднем, у нас в работе каждый день находится более 10 веток. В Smartcat каждая языковая пара – это отдельный документ, и на каждый такой документ необходимо назначить переводчиков. Вручную. Представьте: 40-60 назначений каждый день. Для упрощения этого процесса мы сделали назначение через API, и тоже положили в pipeline. Запуск этого job-а производится по кнопке. Резонный вопрос: почему бы не сделать назначения автоматическими, при отправке переводов, и не разместить вызов методов в плагине Smartcat, а не в pipeline?

Причин такого решения несколько:

  1. Человеческий фактор. Несмотря на то, что мы выстраиваем процессы и стараемся их придерживаться, невычитанные строки или строки без контекста регулярно попадают в Smartcat. Автоматическое назначение в данном случае означало бы для нас дополнительные траты, так как некоторые строки были бы отправлены на перевод дважды: до и после редактуры.
  2. Распределение ролей. Настройкой и управлением проектами на уровне сервера локализаций занимается инженер по локализации или технический писатель проекта. Назначениями и общением с переводчиками занимается менеджер по локализации. Таким образом, назначения должны быть управляемы, прозрачны и доступны через GUI.

Решение: когда менеджер по локализации считает, что строки в данной ветке готовы к переводу, она нажимает на кнопку в Gitlab. На данную ветку назначается вся команда переводчиков. Задачу забирает тот переводчик, кто откликнулся первым.

CI


assignee:
  stage: assignee
  image: node:8.14.0
  tags:
  - devops
  script:
    - chmod +x ./assignee.sh
    - ./assignee.sh
  only:
    - base-translate
    - /^translate.*$/
    - assignee
  when: manual

Скрипт назначений


#!/bin/bash

if echo "$CI_COMMIT_REF_NAME" | grep "translate-";
then
node -pe "JSON.parse(process.argv[1]).documents.forEach(function(elem){ if(elem.name.indexOf(\"$CI_COMMIT_REF_NAME\") !== -1) { console.log(elem.id) } });" "$(curl -XGET -H "Authorization: Basic $SMARTCAT_API_KEY" -H "Content-type: application/json" "https://smartcat.ai/api/integration/v1/project/$SMARTCAT_PROJECT_ID")" >> documents
fi
sed '$d' documents > documents.list
while read LINE; do bash -c "curl -XPOST -H 'Authorization: Basic $SMARTCAT_API_KEY' -H "Content-type:application/json" -d '{"documentIds":[\""$LINE"\"],"stageNumber": 1}' 'https://smartcat.ai/api/integration/v1/document/assignFromMyTeam'";done < documents.list

На этом я завершаю цикл статей об интеграции и настройке непрерывных локализаций. Буду рада ответить на любые ваши вопросы.

Комментарии (3)


  1. afan
    20.05.2019 21:11

    > невычитанные строки или строки без контекста регулярно попадают в Smartcat. Автоматическое назначение в данном случае означало бы для нас дополнительные траты, так как некоторые строки были бы отправлены на перевод дважды: до и после редактуры.

    В моей голове бытует мнение, что стоимость «лишнего» перевода может оказаться дешевле стоимости работы менеджера по локализации; а внедрение в процесс лишней кнопки может приводить к задержкам в работе (переводчики теряют день или больше, ожидая проверки строк) и накладкам (менеджер по локализации забыл что-то оправить в работу и потерял на этом еще день-два). Посему вопрос: не рассматривали ли вы возможность, хотя бы в порядке эксперимента, автоматической отправки на перевод всего и вся с последующими замерами (насколько быстрее проходит перевод, сколько вы реально переплачиваете за повторный перевод с учетом всех совпадений TM, и сколько вы экономите времени менеджера по локализации)?


    1. AnastasiaMyasnikova
      22.05.2019 13:20

      Добрый день! Я тот самый менеджер по локализации.
      Тут дело даже не столько в том, что мы не хотим переплачивать и боимся, что в перевод уйдут невычитанные тексты. Помимо назначения переводчиков в Smartcat, я также передаю им описание задачи и контекст (в виде текста-пояснения или скриншотов) в Skype. Поскольку мы по большей части переводим интерфейсные тексты, скриншоты и подробности необходимы, иначе перевод будет некорректным.
      Есть способ передавать ссылки на скриншоты в комментариях к ресурсному файлу, но не все наши ресурсные файлы поддерживают возможность комментирования. В таком случае, автоматическое назначение означало бы, что переводчики получают задачу на перевод сразу, как только соответствующая ветка попадает в Smartcat, а значит, без моего сообщения и без необходимого контекста. Тогда некоторые из них начнут писать мне в личку и требовать скриншоты, а некоторые переведут «как поняли», и хотя мы этого и не заметим, качество переводов заметно снизится.


      1. afan
        22.05.2019 23:31

        Ну у нас все то же самое — нужно обеспечивать контекстом переводчиков и все такое, но мы пошли от обратного: переводы выгружаются как можно раньше (какие-то с комментариями, какие-то без, тоже в зависимости от формата файлов и от проекта), и переводчики начинают переводить строки с тем контекстом, что есть. А вот когда у них появляются вопросы, мы уже им отвечаем, выборочно добавляем комментарии и скриншоты. Автоматическая выгрузка строк на перевод позволяет еще и как можно раньше обнаруживать проблемы с исходными строками, причем не только силами менеджера по локализации, но и переводчиками, а также позволяет интегрировать переводы раньше и делать QA в CI-сборках продукта (а QA в финальном продукте и подправка переводов, как мне кажется, намного более продуктивен, чем просто перевод, пусть даже и с дополнительными комментариями).

        Поэтому я и подвожу вас размышлению: может быть выгрузка «сразу и как есть» с последующим выборочным добавлением контекста — это не только минус, но и плюс? И если у вас есть возможность реально оценить качество переводов, то это был бы отличный эксперимент (хотя бы на одном проекте каком-то). Вполне может оказаться, что плюсов от такого подхода больше, чем ожидаемых минусов.