BabelFish — полиглот в мире JavaScript

BabelFish Интернет приносит в нашу жизнь глобальность. И многие веб-ресурсы не ограничиваются аудиторией, живущей в одной стране и разговаривающей на одном языке. Однако, поддержка нескольких языковых версий сайта вручную — затея малоприятная и, начиная с определённого масштаба, вряд ли реальная.

Например, в REG.RU на сегодня в словарях более 15000 фраз, из которых порядка 200 используют склонение, и более 2000 используют подстановку переменных. Каждый день добавляется не менее 10 фраз. И это при том, что мы пока только начали локализацию сайта и впереди планы на новые языки.

Хотя задачи интернационализации и локализации программного обеспечения (в том числе в веб) не новы, и, в целом, довольно стандартны, хороших универсальных инструментов для их решения не так много. И подобрать такой инструмент для конкретного стека клиентских и серверных технологий не всегда просто, особенно если хочется использовать один и тот же инструмент и там, и там.

DON’T PANIC.

Недавно был опубликован пакет BabelFish 1.0, предназначенный для интернационализации JavaScript-приложений.

Идеи, лежащие в его основе, настолько пришлись нам по душе, что мы даже перенесли их на Perl в виде CPAN-модуля Locale: Babelfish, и используем это для Perl-приложений. Но вернёмся к JavaScript-реализации.

Обзор imageВ чём же особенности этой библиотеки? Очень удобный и компактный синтаксис для склонений и подстановок. Возможность работы как на сервере, так и на клиенте (для старых браузеров потребуется пакет поддержки es5-shim). Автоматическое приведение структур с данными к «плоскому» виду. Возможность хранения и отдачи сложных структур вместо текста. Рассмотрим возможности модуля на примерах. Типичная фраза выглядит так: В небе #{cachalotes_count} ((кашалот|кашалота|кашалотов)): cachalotes_count.

Также поддерживается точное совпадение и возможность вложенной интерпретации вхождений переменных. Типичный пример — когда мы вместо »0 кашалотов» хотим написать «нет кашалотов», вместо »1 кашалот» просто «кашалот», при этом оставив написание »21 кашалот»:

((=0 нет кашалотов|=1 кашалот|#{count} кашалот|#{count} кашалота|#{count} кашалотов))

Отметим, что если используется переменная с именем count, то её имя через двоеточие в конце фразы можно опустить.

Babelfish API предлагает метод t (локаль, ключ, параметры) для разрешения ключа в конкретной локали в готовый текст или структуру данных. Вызов выглядит так:

babelfish.t ('ru-RU', 'some.complex.key', { a: «test» }); babelfish.t ('ru-RU', 'some.complex.key', 17); // переменные count и value будут равны 17 Чтобы упростить читаемость кода и меньше печатать обычно создается метод такого вида (coffee): window.t = t = (key, params, locale) → locale = _locale unless locale? babelfish.t.call babelfish, locale, key, params Здесь локаль перемещается в конец списка аргументов и становится опциональной. Теперь можно писать кратко: t ('some.complex.key', { a: «test» });

// обе записи ниже равнозначны: t ('some.complex.key', 17); t ('some.complex.key', { count => 17, value => 17 }); Обратная сторона лаконичности синтаксиса — переводчикам (персоналу, работающему со словарями и шаблонами) к синтаксису нужно привыкать, хоть он и несложен.Решением проблемы является предоставление интерфейса для переводчиков, где, помимо фразы для перевода, предлагаются сразу контекст фразы, фикстуры с типичными данными, используемыми при её формировании, и область просмотра результатов.

Также полезно предоставление сниппетов, которые вставляют уже готовые конструкции для склонения и подстановки переменных.

Рассмотрим процесс интеграции Babelfish в ваше приложение на стороне браузера.

Установка Babelfish доступен как в виде пакета npm, так и в виде пакета bower. Если вам нужно работать одновременно и с Node.JS, и с браузерами, рекомендуем использовать npm-пакет + browserify (пример есть в babelfish demo), но большинству разработчиков проще будет использовать bower.Здесь мы предполагаем, что текущая локаль определена как window.lang:

# assets/coffee/babelfish-init.coffee do (window) → «use strict»

BabelFish = require 'babelfish'

locale = switch window.lang when 'ru' then 'ru-RU' when 'en' then 'en-US' else window.lang

window.l10n = l10n = BabelFish ()

l10n.setFallback 'by-BY', [ 'ru-RU', 'en-US' ]

window.t = t = (args…) → l10n.t.apply l10n, [ locale ].concat (args)

null Хранение и компиляция словарей Внутренний формат Словари формируются во внутреннем формате Babelfish, который позволяет привязать к ключу не только текст, но и другие структуры данных. Механизм сериализации и десериализации словарей в JSON прилагается (stringify/load).Фактически, можно добавлять фразы в словари так:

babelfish.addPhrase ('ru-RU', 'some.complex.key', 'текст ключа'); babelfish.addPhrase ('ru-RU', 'some.complex.anotherkey', 'текст другого ключа'); Или так: babelfish.addPhrase ('ru-RU', 'some', { complex: { key: 'текст ключа', anotherkey: 'текст другого ключа' } }); При добавлении сложных структур данных можно указать параметр flattenLevel (false или 0), после: babelfish.addPhrase ('ru-RU', 'myhash', { key: 'текст ключа', anotherkey: 'текст другого ключа' }, false); И тогда при вызове t ('myhash') мы получим объект с ключами key и anotherkey. Это очень удобно при локализации внешних библиотек (например, для предоставления конфигураций для плагинов jQuery UI).Единственное требование при сериализации таких данных — возможность их представления в формате JSON.

Обратите внимание, что для разбора синтаксиса Babelfish использует ленивую (отложенную) компиляцию. То есть для фраз с параметрами при первом использовании будут сгенерированы функции, а при следующих вызовах результат получится быстро. С одной стороны это сильно упрощает сериализацию, с другой — может стать проблемой, если вы используете параноидальные CSP-политики (запрещающие выполнение eval и Function () в браузере). Автор пакета не против реализовать режим совместимости, так что если Вам это действительно потребуется — просто создайте тикет в трекере проекта.

Формат YAML Для большинства применений больше подходит формат YAML, который также поддерживается «из коробки». Я бы рекомендовал хранить данные в этом формате, компилируя их во внутренний формат перед использованием. В частности, словари можно комбинировать друг с другом и отдавать клиенту в виде обычного JavaScript.При этом вложенные ключи YAML преобразуются в плоскую структуру:

some: complex: key: «Some text at least of #{count}» преобразуется в ключ some.complex.key.Кстати, Babelfish умеет автоматически, без прямого указания, распознавать в словарях не просто фразы, но и списки (как сложные структуры данных). Так, если указать

mylist:  — british  — irish То при вызове t ('mylist') мы получим [ 'british', 'irish' ]. Это нам пригодится чуть позже.Преобразования фраз локализации imageОбычно нам требуется перед компиляцией фраз выполнить дополнительные преобразования над ними. В их число у нас входят такие, как: преобразование из формата Markdown в HTML; типографика; добавление классов и атрибутов, специфичных для нашей реализации БЭМ. Автоматическое типографирование полезно всем, а использование формата Markdown упрощает как чтение текста, так и взаимодействие с переводчиками.Оригинальные словари мы кладём в каталог assets/locales, преобразуя их далее в готовые к использованию в config/locales.

Понятно, что ваш стек преобразований скорее всего будет отличаться от нашего.

А вот пример компиляции словарей в формате YAML во внутренний формат Babelfish с преобразованием через Markdown-процессор (grunt):

# Gruntfile.coffee # нужны пакеты glob, marked, traverse marked = require 'marked' traverse = require 'traverse'

grunt.registerTask 'babelfish', 'Compile config/locales/*..yaml to Babelfish assets', → fs = require 'fs' Babelfish = require 'babelfish' glob = require 'glob' files = glob.sync '**/*.yaml', { cwd: 'config/locales' } reFile = /(^|.+\/)(.+)\.([^\.]+)\.yaml$/

# do not wrap each line with

renderer = new marked.Renderer () renderer.paragraph = (text) → text

for file in files m = reFile.exec (file) continue unless m [folder, dict, locale] = [m[1], m[2], m[3], ''] b = Babelfish locale translations = grunt.file.readYAML «config/locales/#{folder}#{file}»

# md traverse (translations).forEach (value) → if typeof value is 'string' @update marked (value, { renderer: renderer })

b.addPhrase locale, dict, translations

res = »// #{file} translation\n» res += «window.l10n.load (» res += b.stringify locale res += »);\n» resPath = «assets/javascripts/l10n/#{folder}#{dict}.#{locale}.js» grunt.file.write resPath, res grunt.log.writeln »#{resPath} compiled.» Теперь готовые скрипты можно склеивать и подключать к вашему приложению любым удобным вам образом.Выбор локали Для выбора локали на серверной стороне наиболее корректным способом является парсинг заголовка Accept-Language. В этом нам поможет npm-модуль locale. Также можете посмотреть исходный код nodeca.core.Откат на другую локаль Babelfish поддерживает список правил отката на другие локали в случае, если нужной фразы нет в текущей локали.Например, мы хотим, чтобы для белорусской локали данные брались в порядке приоритета из белорусской, русской и английской локалей:

babelfish.setFallback ('by-BY', [ 'ru-RU', 'en-US' ]); Локализация Помимо интернационализации перед нами стоит также задача по локализации приложения. В частности, мы должны уметь, например, форматировать валюты, даты, диапазоны времени с учётом локали.Локализация дат Воспользуемся слегка модифицированными данными для форматирования дат из Rails: # config/locales/formatting.ru-RU.yaml date: abbr_day_names:  — Вс  — Пн  — Вт  — Ср  — Чт  — Пт  — Сб abbr_month_names: -  — янв.  — февр.  — марта  — апр.  — мая  — июня  — июля  — авг.  — сент.  — окт.  — нояб.  — дек. day_names:  — воскресенье  — понедельник  — вторник  — среда  — четверг  — пятница  — суббота formats: default: '%d.%m.%Y' long: '%-d %B %Y' short: '%-d %b' month_names: -  — января  — февраля  — марта  — апреля  — мая  — июня  — июля  — августа  — сентября  — октября  — ноября  — декабря order:  — day  — month  — year time: am: до полудня formats: default: '%a, %d %b %Y, %H:%M:%S %z' long: '%d %B %Y, %H:%M' short: '%d %b, %H:%M' pm: после полудня # assets/coffee/babelfish-init.coffee strftime = require 'strftime'

l10n.datetime = (dt, format, options) → return null unless dt && format

dt = new Date (dt * 1000) if 'number' == typeof dt

m = /^([^\.%]+)\.([^\.%]+)$/.exec format format = t («formatting.#{m[1]}.formats.#{m[2]}», options) if m

format = format.replace /(%[aAbBpP])/g, (id) → switch id when '%a' t («formatting.date.abbr_day_names», { format: format })[dt.getDay ()] # wday when '%A' t («formatting.date.day_names», { format: format })[dt.getDay ()] # wday when '%b' t («formatting.date.abbr_month_names», { format: format })[dt.getMonth () + 1] # mon when '%B' t («formatting.date.month_names», { format: format })[dt.getMonth () + 1] # mon when '%p' t ((if dt.getHours () < 12 then "formatting.time.am" else "formatting.time.pm"), { format: format }).toUpperCase() when '%P' t((if dt.getHours() < 12 then "formatting.time.am" else "formatting.time.pm"), { format: format }).toLowerCase()

strftime.strftime format, dt Теперь мы имеем хелпер: window.l10n.datetime (unix timestamp or Date object, format_string_or_config). Аналогично можно построить хелперы для валют и других локализуемых значений.Другие реализации Парсер Babelfish построен на PEG.js. С некоторыми доработками можно использовать его грамматику и в других PEG-парсерах. Учитывая отсутствие привязки синтаксиса к JavaScript и удобство использования, можно полагать, что будут опубликованы реализации Babelfish и для других платформ.Как я уже упоминал выше, мы реализовали диалект Babelfish 1.0 для языка Perl.

Заключение Для иллюстрирования возможностей Babelfish мы опубликовали небольшой демонстрационный проект с использованием marked и jade.Надо сказать, что в процессе использования в нашем проекте некоторые возможности Babelfish существенно расширились именно в результате наших запросов. Например, хранение сложных структур данных фактически перекочевало в Babelfish из нашего Perl-проекта.

Как это обычно и бывает у nodeca, они выпустили продуманную, качественную и перспективную библиотеку. Просто напомню, что ими были разработаны такие хиты, как js-yaml, mincer, argparse, pako и remarked.

© Habrahabr.ru