I18n, l10n and string interpolation JavaScript library
T.js берёт на себя все задачи, связанные с выводом локализованного текста:
Ниже — подробнее.
Для использования библиотеки её необходимо подключить, а затем добавить в неё необходимые локализации.
Добавление локализации производится вызовом T.define()
и его имеет смысл вынести в отдельные файлы lang.ru.js
, lang.en.js
и т.д. (чтобы их можно было подключать по отдельности). Если локализация небольшая, все переводы можно объединить и в один файл (lang.js
). Файл с текущим перевод также следует подключить обычным способом (или загрузить и выполнить асинхронно).
После загрузки библиотеки необходимо указать текущий язык:
T.lang('ru');
Пример файла перевода:
T.define({
ru: {
$aux: {
your: {
ins: { $plural: { one: 'вашей', other: 'вашими' } },
acc: { $plural: { one: 'вашу', other: 'ваших' } },
},
uploaded: { $plural: { one: 'загрузил', other: 'загрузили' } },
liked: { $gender: { m: 'оценил', f: 'оценила' } },
users: { $plural: { one: '{} пользователь', few: '{} пользователя', other: '{} пользователей' } },
photos: {
nom: { $plural: { one: 'фотография', few: 'фотографии', other: 'фотографий' } },
gen: { $plural: { one: 'фотографии', few: 'фотографий', other: 'фотографий' } },
dat: { $plural: { one: 'фотографии', few: 'фотографиям', other: 'фотографиям' } },
acc: { $plural: { one: 'фотографию', few: 'фотографии', other: 'фотографий' } },
ins: { $plural: { one: 'фотографией', few: 'фотографиями', other: 'фотографиями' } },
abl: { $plural: { one: 'фотографии', few: 'фотографиях', other: 'фотографиях' } },
},
codes: {
200: 'OK',
300: 'перенаправление',
400: 'не существует',
500: 'ошибка',
},
},
ok: 'OK',
cancel: 'отмена',
online: 'онлайн {>users}',
newsfeed: {
uploaded: '{>users users} {>uploaded users} {>photos.acc photos}',
press_ok: 'нажмите «{>ok}»',
status: 'статус {code}: {>codes code}',
},
},
});
Подробнее его структура рассмотрена ниже.
Перевод строки с ключом newsfeed.uploaded
:
el.innerHTML = T('newsfeed.uploaded', { users: 5, photos: 21 });
Результат:
5 пользователей загрузили 21 фотографию
JS-объект, передаваемый в функцию T.define()
, должен содержать один или несколько полей, название каждого из которых соответствует двухбуквенному коду языка (ru
, en
), а значение является объектом, который описывает перевод на указанный язык. Внутри такого объекта содержатся либо непосредственно пары ключ-значение, либо вложенные объекты, либо специальные поля. Вложенные объекты могут быть любой глубины (и к ним можно обращаться через точечную нотацию), но рекомендуется использовать только один уровень вложенности — на верхнем уровне общие для всего приложения строки, остальные сгруппированы по разделам.
Каждому ключу в переводе соответствует строка, которая может содержать два вида подстановок: данные и другие строки перевода.
Данные передаются в виде объекта вторым параметром в функцию T
. Чтобы подставить это значение целиком, используйте фигурные скобки: {}
. Для того, чтобы обратиться к какому-то из внутренних полей, в фигурных скобках пропишите «путь» к нему: {user.name}
.
Кроме того, внутрь строк можно подставлять другие строки из того же перевода. Делается это так:
{>key param1 param2 name=param3}
Здесь key
– подставляемая строка, param1
, param2
и param3
– поля данных, которые нужно передать в неё (при этом значение поля param3
станет доступно как name
). По умолчанию в «дочернюю» строку передаются те же данные, что были переданы родительской.
Часто возникает необходимость сделать строку зависящей от переданного ей числа. В этом случае различные варианты строки оборачиваются в структуру $plural
:
users: {
$plural: {
one: '{} пользователь',
few: '{} пользователя',
other: '{} пользователей'
}
}
Поля zero
, one
, two
, few
, many
и other
являются зарезервированными и соответствуют значениям, описанным в документе
http://www.unicode.org/cldr/charts/27/supplemental/language_plural_rules.html
Кроме того, можно указать варианты и для конкретных значений.
В данный момент в библиотеку встроены правила для русского и английского языка, но для конкретного перевода можно написать собственную функцию определения категории по числу и поместить её в виде поля $plural
в корне объекта, описывающего перевод.
При обращении к ключу, содержащему внутри себя структуру $plural
, будет автоматически выбран соответствующий вариант на основании либо поля n
, если передан объект, либо, если передано одно число, самого числа.
TODO: ordinal + range
Аналогично зависимости от числа, можно помечать строку как зависящую от пола при помощи поля $gender
:
liked: {
$gender: {
m: 'оценил',
f: 'оценила'
}
}
Если у входного объекта поле g
(или сам объект) начинается с буквы f
или имеет значение 1, будет выбран вариант, соответствующий женскому полу, в противном случае — мужскому.
Иногда удобно записать одну строку в разных падежах чтобы использовать её внутри других строк (в составе предложений). Для этого можно просто использовать вложенный объект:
your: {
nom: { $plural: { one: 'ваша', other: 'ваши' } },
gen: { $plural: { one: 'вашей', other: 'ваших' } },
dat: { $plural: { one: 'вашей', other: 'вашим' } },
acc: { $plural: { one: 'вашу', other: 'ваших' } },
ins: { $plural: { one: 'вашей', other: 'вашими' } },
abl: { $plural: { one: 'вашей', other: 'ваших' } },
}
Тогда значение, естественно, можно получить как {>your.acc}
. Однако иногда возникает ситуация, когда нужно просклонять входные данные, а не строки перевода. Чаще всего это необходимо для имён собственных (например, для фразы «Олегу понравились 2 ваших фотографии»). Для этого случая доступны специальные функции $name
, $surname
и $patronym
. Обращаться к ним следует так, словно это обычные подставляемые строки: {$name.dat user.firstName} {$surname.dat user.lastName}
. В данный момент реализована поддержка только русских имён, фамилий и отчеств (разумеется, для английского и многих других языков такой проблемы не стоит).
При желании, функции $name
, $surname
и $patronym
можно переопределить (аналогично $plural
) – объявив их в корне объекта, описывающего перевод. Первым параметром в них передается имя, вторым — падеж, указанный после точки.
Кроме того, можно определять и собственные функции, которые будут вызываться аналогичным образом. Если название функции не начинается с $
, при обращении к ней нужно ставить знак >
в начале (как при подстановке любой другой строки), в противном случае он опционален. Первый параметр это всегда данные, «прокинутые» из родительской строки; второй, третий и так далее — то, что стоит после первой, второй (и так далее) точки за именем функции. То есть, подстановка {>testFunc.optA.optB val1 val2 xxx=val3}
превратится в вызов testFunc({ 0: val1, 1: val2, xxx: val3 }, 'optA', 'optB')
.
Для форматирования даты и времени доступно три функции: $date
, $time
, $rdate
.
Функции $date
и $time
вызываются сходным образом, принимая одну опцию — формат вывода: $date.DDMMYYYY
или $time.HHmmss
. Всё отличие между ними заключается только в форматах по умолчанию, которые используются, если этот параметр опущен. В этом случае $date
выводит день, месяц и год, а $time
— часы и минуты.
Поддерживаемые компоненты даты и времени:
TODO: добавить Do/Mo/do (ordinals).
Для описания формата нужно перечислить (без разделителей) компоненты, которые необходимо использовать при выводе, в том порядке, в котором их нужно вывести. Названия месяцев и дней недели, а также разделители будут выбраны на основе текущего языка.
Функция $rdate
предназначена для вывода относительных дат. Она выбирает наиболее подходящий формат в зависимости от того, насколько скоро наступит (или насколько давно наступила) указанная дата:
3 фев 1989
(дата без времени)1 апр в 19:40
(дата без указания года + время)вчера в 7:32
(слово вчера
или сегодня
+ время)два часа назад
(число часов словами)37 минут назад
(число минут)три минуты назад
(число минут словами)48 секунд назад
(число секунд)только что
через несколько секунд
через 23 секунды
(число секунд)через минуту
(число минут словами)через 22 минуты
(число минут)через три часа
(число часов словами)сегодня в 21:45
(слово сегодня
или завтра
+ время)19 сен в 8:21
(дата без указания года + время)25 мая 2017
(дата без времени)Эти строки можно переопределить, указав ключ $rdate
(а внутри него — объект $date
, который и отвечает за выбор нужной опции в зависимости от входных данных):
$rdate: { $date: {
in_hours: { $plural: { 1: 'in an hour', 2: 'in two hours', 3: 'in three hours' } },
in_minutes: { $plural: { 1: 'in a minute', 2: 'in two minutes', 3: 'in three minutes', one: 'in {} minute', other: 'in {} minutes' } },
in_seconds: { $plural: { one: 'in {} second', other: 'in {} seconds' } },
in_moment: 'in a moment',
moment_ago: 'just now',
seconds_ago: { $plural: { one: '{} second ago', other: '{} seconds ago' } },
minutes_ago: { $plural: { 1: 'a minute ago', 2: 'two minutes ago', 3: 'three minutes ago', one: '{} minute ago', other: '{} minutes ago' } },
hours_ago: { $plural: { 1: 'an hour ago', 2: 'two hours ago', 3: 'three hours ago' } },
tomorrow: 'tomorrow at {$time.Hmm}',
today: 'today at {$time.Hmm}',
yesterday: 'yesterday at {$time.Hmm}',
year: '{$date.MMMD} at {$time.Hmm}',
other: '{$date.MMMDYYYY}',
} },
Обратите внимание, что в строки in_hours
, in_minutes
, in_seconds
(а также hours_ago
, minutes_ago
, seconds_ago
) передается не дата, а целочисленное количество часов, минут и секунд (относительно текущего времени) соответственно. Не забывайте про зависимость от числа в таком случае. Для малых величин рекомендуется прописать числительные словами (а для единицы опустить его полностью, если язык допускает).
Перечисленные выше ключи, входящие в структуру $date
, являются наиболее полным списком, но при переопределении нет необходимости указывать их все: единственной обязательной строкой является other
. В отсутствие строки in_moment
будет взято значение moment_ago
— и наоборот (это удобно, если дата заведомо должна находиться в прошлом или в будущем, но из-за неточно выставленного времени может оказаться «по другую сторону» от текущего момента). Если отсутствуют оба варианта — будут выбраны строчки in_seconds
и seconds_ago
соответственно. В свою очередь, если нет строк in_seconds
/in_minutes
/in_hours
или seconds_ago
/minutes_ago
/hours_ago
— будут использована подходящая строка из today
/tomorrow
/yesterday
. Если подходящей не будет (и дата относится к текущему году) — year
. Наконец, если даже этот вариант не указан, алгоритм «откатится» к other
.
TODO: форматирование интервалов.
Для форматирования целых и дробных чисел используется внутренняя функция $num
(которая, разумеется, может быть переопределена). По умолчанию она доступна для русского (в качестве десятичного разделителя используется запятая, в качестве разделителя разрядов — тонкий пробел) и английского (десятичный разделитель — точка, разделитель разрядов — запятая) языков.
Функция имеет следующие опции: {$num.+t3.5}
. Знак «плюс» означает, что перед положительными числами следует выводить знак (по умолчанию выводится только знак «минус» перед отрицательными). Символ «t» — использовать разделитель тысяч (тонкий пробел для русского, запятая для английского). Число «3» до точки означает, что если целая часть оказалась короче трёх символов, то её следует дополнить слева нулями до трёх разрядов. Число «5» после точки — вывести не более 5 знаков после запятой. Чтобы вывести в точности 5 знаков (дополнив их нулями справа при необходимости), перед их количеством нужно поставить 0: {$num..03}
выведет число ровно с тремя знаками после запятой. Обратите внимание, что здесь нет дополнительных опций, касающихся целой части, поэтому в описании формата стоят две точки подряд.
TODO: форматирование размеров файлов и градусов/минут/секунд.
http://www.artlebedev.ru/kovodstvo/sections/62/ (и Ководство в целом)
https://github.com/typograf/typograf/blob/dev/docs/RULES.ru.md
Библиотека не берёт на себя ответственность за непосредственно создание и изменение переводов, но предоставляет два интерфейса, упрощающих эти процессы.
Вызов T.showAllKeys(path)
откроет попап-окно со списком всех строк в текущем переводе. Опциональным параметром path
можно отфильтровать список, показав только ключи, находящиеся по указанному пути. Вызов T.showKey(key)
откроет попап-окно для редактирования указанной строки.
Чтобы обработать нажатие кнопки «Сохранить», предварительно необходимо повесить обработчик: T.addUpdateListener(func)
. В функцию func
будет передан объект вида
{
ru: {
updatedKey: 'new_value',
addedKey: 'value',
removedKey: null,
}
}
Ответственность за сохранение изменений (запрос к серверу и серверный обработчик) лежит на вашей стороне. Разумеется, не забудьте также позаботиться о проверке прав на изменение строк на серверной стороне.
Чтобы процесс перевода был более комфортным, предлагается также «инлайн-интерфейс» для переводчиков. Чтобы перевести библиотеку в такой режим, сделайте вызов T.toggleInlineTranslation(true)
перед отрисовкой приложения (или перерисуйте его заново). В этом режиме все строки будут обернуты в следующий HTML-код:
<span class="inline-translation" style="border-bottom: 1px dotted #333; cursor: pointer" onclick="T.showKey('key'); event.stopPropagation(); return false;">...</span>
Чтобы запретить оборачивание отдельных строк в этот код, при вызове T('key', ...)
допишите восклицательный знак после имени ключа: T('key!', ...)
.
После этого при клике по любому переведенному тексту будет открываться попап для перевода соответствующего ключа. При сохранении будет сделан тот же вызов обработчика, что указано выше.
При использовании инлайн-интерфейса в фреймворках, следящих за безопасностью выводимых строк нужно применять средства для подстановки «небезопасного HTML» (например, в Angular вызывать $sce.trustAsHtml и использовать директиву ng-bind-html
). В свою очередь, T.js по умолчанию делает безопасными все входные данные (но не сами строки — в них можно использовать HTML-тэги), заменяя < и > на < и > соответственно.