Babel и handlebars, пора бы и подружить

в 19:16, , рубрики: babel, handlebars, i18n, javascript, python, Веб-разработка, локализация, метки: , , , ,

Думаю многим известен такой пакет как Babel, либо PyBabel.
Отличный пакет для локализации, который базируются на gettext, как и все остальное ( по крайней мере мне известное) в современном мире.

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

Должен заранее оговориться, что у меня не было никакого ограничения в выборе технологий.
У нас в конечном счете для билда статики используется полный набор — ruby(compass), node(coffee,grunt,requirejs), python(бэкенд и основа всего и вся), шелл скрипты, в общем ограничения нет никакого.

Кстати если это кому-либо интересно могу подробно расписать о сборке билда отдельным постом, там requirejs+scss+все вышеперечисленное, на данный момент около 1000+ файлов входящих в билд + deployment на heroku и не-heroku одной кнопкой. На мой взгляд в целом процесс интересен, но не знаю на чем лучше заострить внимание

Т.е по сути все что необходимо было найти — кто способен:
a) Пройтись по шаблонам handlebars
b) Уметь работать по тому же принципу что и Babel — т.е строка это и есть ключ, а не константы, когда все хранится в отдельных файлах.
c) Подготовить из найденных строк .po файл

Дальше уже руки развязаны и возможностей хватает.

Потратив несколько часов на поиск вывод был неутешительный, такого почему то никто не сделал.
Верить в это категорически не хотелось, но все существующее было «не то» и «не так».

Что делали и умели существующие решения?
Они с переменным успехом выдирали регэксами строки. Не умели работать с ngettext, либо не умели работать по принципу «строка=ключ», а только константы.

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

1) Определить текст перевода в шаблонах, принцип «строка=ключ»
Так намного удобней в работе, чем когда ключ=имя константы. Цена ошибки так же меньше, максимум кто либо получит текст на английском, вместо перевода

2) Передать текст в babel, а так же номер строки
Это необходимо, т.к при обновлениях babel не сможет определить, что именно изменилось, если у него не будет номеров строк
3) Построить из строк .po файлы, собственно это задача babel
4) Из .po файлов после перевода сделать .json файлы и передать кому либо уже на сторону клиента. Конвертация — задача po2json
5) Сделать helper-ы для handlebars, как однострочные, так и блочные с поддержкой ngettext
Опять таки, разнообразие ради удобства повседневного
6) Оба варианта должны уметь работать с параметрами
7) Оба варианта должны передать текст для перевода «на верх», кому то, кто и держит у себя те самые .json файлы
8) Этот же кто-то должен подставлять полученные из шаблона параметры в финальную строку.

На 8й пункт был выбран Jed, отличная библиотека со встроенным sprintf, что сразу же решало проблему удобной передачи параметров.

Первый пункт был ключевым, его подсмотреть так нигде и не получилось.
Долго рассматривал регекспы и прикидывал свои — все не нравилось. Криво, недостаточно, ненадежно.
Еще дольше рассматривал скомпилированный в javascript код шаблонов. Оттуда получить строки было достаточно легко.
Но самая идея такого подхода ужасала.
К тому же — это не решало проблему, что нужно знать где строка была встречена в оригинальном шаблоне.

Пришлось смотреть исходники handlebars и тут снизошло озарение — его лексер написан с помощью jison и парсер jisona, использующий лексику непосредственно handlebars, зашит внутрь.
И вызывая метод Handlebars.parse(template) можно получить JSON структуру шаблона.
Казалось бы все замечательно — но номеров строк так и нет.

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

Дальше оставалось дело техники, собрать все воедино, прикрутить к babel в качестве extractor-а
Т.е:
Функция на python(extractor) которая вызывает node.js скрипт подгружающий пропатченный handlebars.js.
Далее этот скрипт рекурсивно проходит по структуре, собирает строки и возвращает в экстрактор в нужном ему формате. Вся логика babel остается неизменной. Логика извлечения строк — так же родная от handlebars.js. Все это не может не радовать.

Само собой этот пост не имел бы смысла, не будь последующих строк. Думаю, что как минимум время затраченное на поиски правильного направления для экспорта строк я кому либо сэкономлю.
— pip install pybabel-hbs
github.com/tigrawap/pybabel-hbs

Пакет устанавливается вместе с патченным handlebars, а в исходниках есть примеры реализации helper-ов (правда на coffee)
Само же использование в темплейтах получилось лично для меня идеальным, 4 хелпера на гитхабе, тут самый «толстый», для наглядности

{{#ntrans num_to_check_aganst param_1="something" num=num_to_check_against}}
    Some text to be translated with %(param_1)s and %(num)s
{{else}}
    Some plural text to be translated with %(param_1)s and %(num)s
{{/ntrans}}

В конфиг же babel-а нужно добавить

[hbs: path/to/project/**.hbs] 

Для работы необходимо чтоб в окружении был node.js.
В планах оптимизация, чтоб node.js не запускался каждый раз для каждого файла отдельно, а один раз за все время жизни основного процесса babel.
Это реализовано в версии 0.2.0, пока пост был на модерации.
Само собой ускорение получилось неплохое, пачка из 150 шаблонов стала обрабатываться за ~минуту.
До этого занимало пару минут, что было совсем неприемлимо.
Но и минута для такого колва слишком много.

Существенно(еще в 4 раза) получилось оптимизировать за счет того, что изначально передавал данные в node.js из python через pexpect, теперь же (версия 0.2.1) передаю только название файла и уже node.js его считывает. (тут конечно минус в том, что оба открывают файл по очереди)
Сейчас обработка 150 файлов занимает меньше 15 секунд. Есть еще простор для ускорения процесса, но меня пока что устраивает.
С учетом того, что запускать часто не нужно — good enough результат.

Мораль сей басни такова, прежде чем часами писать и тестировать регулярные выражения, выискивать окольные пути для разбора какого либо языка — стоит посмотреть как он разбирает сам себя. Возможно так проще.
В конечном счете, вся проблема решалась несколькими строчками вписанными в сам handlebars.js, а то что сегодня предлагается на просторах интернета это… как минимум не удобно.

Автор: TigraSan

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js