“Я хочу, чтобы после того, как программист добавил новую строчку в интерфейс, она сама перевелась на 19 языков и сама положила себя в SVN и была готова к релизу утром” — это мечта любого разработчика, вкусившего запретный плод локализации продукта на иностранные языки. В Alconost Translations мы помогаем если не исполнить, то хотя бы приблизиться к этой мечте. Да, решение, похожее на описанное в статье существует не только для разработчиков LinkedIn, но и для простых смертных.
О том, как процесс построен в LinkedIn — в этой статье (внимание — Java).
Интернационализация оказывает критическое влияние на деятельность и развитие сети 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). Это приводило к определенным проблемам:
- Добавление переводов означало пересборку и перезапуск всего сервиса.
- Если в переводе случалась ошибка, нельзя было просто вернуться к старой версии текста: нужно было откатывать код приложения к предыдущей версии.
- Переводы могут использоваться во многих сервисах — и все их в случае чего приходилось пересобирать и перезапускать.
По-новому
Новая система собирает и выкладывает properties-файлы отдельно от кода приложения. Мы ввели концепцию языкового пакета — это JAR-файл, содержащий весь переведенный контент для конкретного языка. Обновленные версии таких языковых пакетов могут выкладываться на веб-сервер в любой момент. Их также можно в любой момент откатить назад, если будут обнаружены ошибки.
Мы добавили новую библиотеку загрузки ресурсов, которая определяет доступность новых языковых пакетов и начинает использовать обновленные переводы — все это без перезапуска сервиса. Если библиотека не находит перевод, она использует исходные англоязычные строки.
Процесс
Внедрение новых переводов — лишь часть большой картины: нам также нужно было найти способ быстро находить новые и свежеизмененные строки, доставлять их переводчикам и включать результат перевода в языковой пакет. Вот так выглядит наш процесс полностью:
- Инженер отправляет новые или обновленные англоязычные строки в систему контроля версий.
- Сервер локализации сканирует систему контроля версий на предмет изменений раз в день и издает запрос на перевод всех новых и измененных строк.
- Раз в час сервер локализации собирает готовые переводы. Он проверяет новый контент и, затем, публикует полный языковой пакет со всеми переводами для конкретного языка в Хранилище.
- Система внедрения раз в час направляет обновленные языковые пакеты на тестирование и подгружает в работающий сервис дважды в день.
- На случай, если команде локализации понадобится изменить перевод в срочном порядке, есть возможность подгружать переводы вручную в любое время в один клик.
Обратная совместимость
Благодаря тому, что система динамической подгрузки языков отделяет локализуемые тексты от кода, появилась возможность выкладывать в работающий сервис новые строки контента раньше, чем код приложения, который их использует. Поэтому теперь все наши интернационализационные ресурсы должны быть обратно совместимы с предыдущими версиями, чтобы обеспечить уверенность в том, что ничего не сломается при внедрении новых строк.
В этом контексте обратная совместимость означает, что добавление новых строк всегда безопасно, но если вы изменяете существующую, то должны сохранить неизменным количество и тип переменных. Например, изначально у нас была такая строка:
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