Как LinkedIn делает локализации на 19 языков за 1 ночь

“Я хочу, чтобы после того, как программист добавил новую строчку в интерфейс, она сама перевелась на 19 языков и сама положила себя в SVN и была готова к релизу утром” — это мечта любого разработчика, вкусившего запретный плод локализации продукта на иностранные языки. В Alconost Translations мы помогаем если не исполнить, то хотя бы приблизиться к этой мечте. Да, решение, похожее на описанное в статье существует не только для разработчиков LinkedIn, но и для простых смертных.

О том, как процесс построен в LinkedIn — в этой статье (внимание — Java).

image


Интернационализация оказывает критическое влияние на деятельность и развитие сети LinkedIn, которая сегодня доступна на 19 разных языках. Чтобы ускорить работу с локализованными текстами, международная инженерная команда разработала систему динамического внедрения переведенных строк контента в работающие сервисы.

Новая система позволяет нам оперативно модифицировать контент: вносить быстрые правки и изменения без привлечения разработчиков, пересборки и перезапуска сервиса.

Введение

Весь контент изначально пишется на английском нашими разработчиками и продакт-менеджерами. Обычно текст, нуждающийся в переводе, содержится в properties-файле:

add_to_network__send_invitation=Send invitation
add_to_network__cancel=Cancel
add_to_network__add_message=Add a personal message
add_to_network__user_message=I'd like to add you to my professional network.nn- {0}

Затем наша штатная команда локализации переводит контент на разные языки. Вот, например, тот же properties-файл, переведенный на итальянский:

add_to_network__send_invitation=Invia un invito
add_to_network__cancel=Annulla
add_to_network__add_message=Aggiungi un messaggio personale
add_to_network__user_message=Vorrei aggiungerti alla mia rete professionale.nnnn-{0}

Чтобы текст отобразился на странице, мы используем в шаблонах функции интернационализации, которые обращаются по заданному ключу к properties-файлу для пользовательской локали.

<form>
  <label for="message">${i18n('add_to_network__add_message')}</label>

  <textarea id="message">${i18n('add_to_network__user_message', fullname)}</textarea>

  <input type="submit" value="${i18n('add_to_network__send_invitation')}"/>
  <input type="button" value="${i18n('add_to_network__cancel')}"/>
</form>

По-старому

До внедрения динамической подгрузки языков система собирала все properties-файлы в один артефакт вместе с кодом приложения (WAR). Это приводило к определенным проблемам:

  1. Добавление переводов означало пересборку и перезапуск всего сервиса.
  2. Если в переводе случалась ошибка, нельзя было просто вернуться к старой версии текста: нужно было откатывать код приложения к предыдущей версии.
  3. Переводы могут использоваться во многих сервисах — и все их в случае чего приходилось пересобирать и перезапускать.

По-новому

Новая система собирает и выкладывает properties-файлы отдельно от кода приложения. Мы ввели концепцию языкового пакета — это JAR-файл, содержащий весь переведенный контент для конкретного языка. Обновленные версии таких языковых пакетов могут выкладываться на веб-сервер в любой момент. Их также можно в любой момент откатить назад, если будут обнаружены ошибки.

Мы добавили новую библиотеку загрузки ресурсов, которая определяет доступность новых языковых пакетов и начинает использовать обновленные переводы — все это без перезапуска сервиса. Если библиотека не находит перевод, она использует исходные англоязычные строки.

Процесс

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

image

  1. Инженер отправляет новые или обновленные англоязычные строки в систему контроля версий.
  2. Сервер локализации сканирует систему контроля версий на предмет изменений раз в день и издает запрос на перевод всех новых и измененных строк.
  3. Раз в час сервер локализации собирает готовые переводы. Он проверяет новый контент и, затем, публикует полный языковой пакет со всеми переводами для конкретного языка в Хранилище.
  4. Система внедрения раз в час направляет обновленные языковые пакеты на тестирование и подгружает в работающий сервис дважды в день.
  5. На случай, если команде локализации понадобится изменить перевод в срочном порядке, есть возможность подгружать переводы вручную в любое время в один клик.

Обратная совместимость

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

В этом контексте обратная совместимость означает, что добавление новых строк всегда безопасно, но если вы изменяете существующую, то должны сохранить неизменным количество и тип переменных. Например, изначально у нас была такая строка:

accept_invite__hello_connect=Hello {0}, would you like to connect with {1}?

Мы спокойно можем поменять некоторые слова:

# This change is backwards compatible
accept_invite__hello_connect=Hi {0}, would you like to add {1} to your professional network?

Но удаление, добавление или изменение типа переменных нарушит обратную совместимость, так как код приложения будет предоставлять значения только для прежних переменных:

# This change is NOT backwards compatible!
accept_invite__hello_connect=Hello {0}, would you like to connect with {1}, a coworker at {2}?

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

/**
 * Verify that translation isn't missing indexes used with the 
 * original as well as does not specify additional indexes not
 * present in the original.
 */
private boolean verifyPlaceholderIndexMatch(String originalMessageTemplate, 
                                            PlaceholderInfo originalPlaceholderInfo,
                                            PlaceholderInfo placeholderInfo)
{
  if(originalPlaceholderInfo.keySet().equals(placeholderInfo.keySet()) == false)
  {
    // remove all index numbers in the translation from the index 
    // set in the original to determine what's missing in the 
    // translation
    Set<String> missingIndexSet = new HashSet<String>(originalPlaceholderInfo.keySet());
    missingIndexSet.removeAll(placeholderInfo.keySet());

    // remove  all index numbers in the original from the set of
    // indexes in the translation to determine what extra index
    // we have in the translation
    Set<String> extraIndexSet = new HashSet<String>(placeholderInfo.keySet());
    extraIndexSet.removeAll(originalPlaceholderInfo.keySet());
    return false;
  }
  return true;
}

19 языков внедрены, впереди — все остальные!

Новая система ускорила процесс перевода в LinkedIn: попадание новых строк в работающий сервис стало значительно быстрее и легче, а мы получили возможность постепенно выкатывать переводы и откатывать их назад в случае необходимости. И самое важное: наша система интернационализации теперь может масштабироваться с учетом растущего количества приложений, языков и участников.

Переведено в Alconost Translations.

Автор: alconost

Источник

Оставить комментарий