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

http://engineering.linkedin.com/language-packs/decoupling-translation-source-code
  • Перевод
“Я хочу, чтобы после того, как программист добавил новую строчку в интерфейс, она сама перевелась на 19 языков и сама положила себя в SVN и была готова к релизу утром” — это мечта любого разработчика, вкусившего запретный плод локализации продукта на иностранные языки. В Alconost мы помогаем если не исполнить эту мечту, то хотя бы приблизиться к ней. Да, решение, похожее на описанное в статье существует не только для разработчиков 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.\n\n- {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.\n\n\n\n-{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.

Alconost занимается локализацией приложений, игр и сайтов на 60 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.

Мы также делаем рекламные и обучающие видеоролики — для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.

Подробнее: https://alconost.com

Alconost
134,00
Локализуем на 68 языков, делаем видеоролики для IT
Поделиться публикацией

Комментарии 26

    +2
    зачем придумали упаковывать properties в лишний jar?
      0
      Чтобы можно было обновлять ресурсы локализации независимо от других сборок. В первую очередь, от рабочего кода.
        +3
        а просто файл подложить не катит?
          0
          Видимо там не один файл properties. Удобнее обновлять сразу целый языковой пакет и отслеживать его версионность.
            0
            ну, хозяин — барин.
            мну использует спринговский ReloadableResourceBundleMessageSource
      +31
      Походу суть перевода за одну ночь не в технологии а в большом штате сотрудников.
        +1
        И в технологии упрощенного развертывания ресурсов локализации, не затрагивающего код.
          +14
          Я такую технологию юзал еще до гугла и линкедина… она логична. Есть еще как минимум gettext, которому 100 лет в обед. Посему расказ о приходе к технологии не ясен.
            +3
            Серьёзно??? Ну да наверное для сервиса где изначально не была заложена интернационализация это — маленькая победа.
            Но по сути так хранить локализации — очевидно.
          +3
          Помоему довольно очевидный способ, юзаю похожее уже лет 5. Кроме конечно упаковки в jar и т.д. в том числе и локализацию типа «hello, {username}» где в метки проставляются соответствующие элементы отправленного массива.
          Как уже выше описали секрет быстрой локализации — в количестве переводчиков.
          Я думал они там на ночь скрипт запускают, который с какого нить translate тянет переводы.
            +2
            Вся соль в том (следует из диаграммы), что:

            1. платформа для перевода забирает ресурсы из svn
            2. переводчики переводят новые и измененные строчки
            3. платформа для перевода кладет переведенные ресурсы назад в svn

            В платформе работают переводчики агентства. У агентства много переводчиков т.е. есть достаточный для быстрого перевода штат. Не нужно быть линкедином, чтобы построить такое решение, нужно выбрать правильное агентство.
          • НЛО прилетело и опубликовало эту надпись здесь
              +4
              > “Я хочу, чтобы после того, как программист добавил новую строчку в интерфейс, она сама перевелась на 19 языков и сама положила себя в SVN и была готова к релизу утром”

              Это, точно, мечта. Само, и еще и выложилось. И без косяков.

              Вы же описываете просто вынос ресурсов отдельно от кода: т.е. переводится не само, само же и не выкладывается (если бы jar-ы сами себя начали выкладывать… ого-го бы началось!), и от косяков не страхует вообще никак.

              Сколько раз читаешь файл с переводами — вроде все верно (ну, кроме случаев «смотри, мама, я Промт украл, и стал мегапереводчиком!»), а когда тот же перевод видишь в ПО — хочется плакать от того, что на выходе видишь. Предел для меня — перевод сообщений на смартфоне, где английское сообщение «Ring» (в смысле, звонок) перевели как «Кольцо» :)
                +1
                Согласен перевод без контекста это полная Ж. А вот как передать контекст переводчику это тот еще вопрос.
                  +2
                  Ну да. А над тем смартфоном (точнее, его хозяином) мы еще долго смеялись — «Властелин звонков» :)
                +1
                А gettext по прежнему самое оптимальное решение или нет? И как переводить при помощи gettext скрипты на javascript?
                Извините за глупый вопрос в тему.
                  +2
                  Самое. У нас в проекте долгое время использовалась доморощенная архитектура локализации, но после 5 лет мы поняли, что пишем велосипед. За неделю переделали на GETTEXT — профит! И девы, и документаторы счастливы.

                  На Javascript-е мы используем Gettext. На сервере генерим JSON из *.po файлов, и клиент подтягивет файл нужной локали. Быстро, удобно, неимоверно гибко (! — домены, контекст, плюралы).
                    0
                    Мы пошли немного другим путем. Что бы не тратить время на перевод мы генерим уже переведенные конфиги и просто юзаем их для нужной локали, тоже и для javascript. Gettext просто не сильно оптимальное решение для серверов которые не держат базу в памяти, например для php.
                  +3
                  Статья ни о чём :(

                  Все давно знают, что ресурсы нужно хранить отдельно от приложения.

                  К примеру, MSFT stack с его .NET: там ресурсы by design отдельно, и в любой момент можно использовать какой угодно язык.
                    0
                    MSFT stack поддерживает отделение кода от ресурсов, начиная с формата NE (1985 г.)
                    Угу, новинка .NET, а то как же.
                    0
                    С обычными приложениями существенно больше возни не с сообщениями (с ними-то как раз все просто), а со справкой и прочими текстами. Там возни на порядки больше, и формализовать процесс намного сложнее.
                      0
                      Да мне бы хоть на английском
                      Почините сервер пожалуйста.

                      503: Service Unavailable
                      вот скрин habrastorage.org/storage2/003/9e3/656/0039e3656c7f2e3104eff450cbecdd93.jpg
                        –1
                        Да-а, сегодня у линкеда Хабраэффект, похоже…
                        +5
                        О какой локализации может идти речь, если у них почти год неправильно приходили уведомления, содержащие русский текст, а сейчас поле образования у всех людей, у которых указан университет в профиле кириллицей выглядит примерно так (если зайти в редактирование профиля, то там все нормально)

                        image
                          +1
                          Мы ввели концепцию языкового пакета


                          А вы уверены, что это именно вы сделали? в том же Zend Studio (первое, что на ум пришло) это применяется лет 10 как, если не больше…
                            0
                            Инновации же!

                            У нас на Друпальных проектах так:
                            Function t.

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

                            Самое читаемое