Глава 6. Локализация Aiogram с помощью проекта Fluent.
Синтаксис Fluent.
Официальная справка по синтаксису: https://projectfluent.org/fluent/guide/
FTL спроектирован так, чтобы его было легко читать, но в то же время он позволял представлять сложные понятия из естественных языков, такие как род, множественное число, спряжения и другие.
Во Fluent основная единица перевода называется сообщением. Сообщения — это контейнеры для информации. Вы используете сообщения для
идентификации, хранения и вызова информации о переводе, которая будет использоваться в продукте. Базовый синтаксис сообщения это —
идентификатор = перевод
. В синтаксисе идентификаторов используется kebab-case
. Пример:
# Это комментарий
# Ниже идет сообщение
Hello = Привет
# Это пример многострочного сообщения, отступы обязательны.
# Длинные идентификаторы пишутся через дефис. Можно включать другие сообщения
long-message = { Hello }, это
многострочное сообщение.
Продолжение начинается с отступа.
Отступ может быть только пробелом.
Табуляция не считается отступом.
Синтаксис Fluent поддерживает ссылки на другие сообщения, переменные, селекторы, и даже функции.
Давайте посмотрим их использование на примере нашего кода про монеты. Добавим наш хэндлер про монеты. При вводе числа бот будет писать сколько у вас монет.
39@router.message(F.text)
40async def handler_2(message: Message, i18n: I18nContext) -> None:
41 try:
42 name = message.from_user.mention_html()
43 n = int(message.text)
44 await message.answer(text=i18n.get("you-have-coin", value=n, user=name))
45 except ValueError as e:
46 logging.log(INFO, e)
47 await message.answer(text=i18n.get("enter-a-number"))
Нам потребуется имя пользователя и количество монет, чтобы передать эти данные в перевод.
Переменные во Fluent, то есть объекты, которые попадают извне из питоновских функций
в сообщения, обозначаются {$ переменная }
. Через контекст I18nContext
, переданный в
хэндлер, мы будем получать строки переводов, а переменные Fluent
передавать как именованные аргументы: i18n.get(идентификатор
,
**kwargs
):
40async def handler_2(message: Message, i18n: I18nContext) -> None:
44await message.answer(text=i18n.get("you-have-coin", value=n, user=name))
В переменную { $value }
отправим наше число монет n: value=n
. А
имя пользователя name в переменную { $user }
отправим так: user=name
.
Переменные отправляются как именованные аргументы (kwargs) в основном коде.
А в нашем файле перевода добавим эти переменные в нужное место:
hello = Привет, <b>{ $user }</b>!
cur-lang = Текущий язык : <i>{ $language }</i>
help = Помощь
you-have-coin = У пользователя { $user } { $value ->
[0] совсем нет монет
[one] имеется одна монета
[few] { $value } монеты
*[many] есть { $value } монет
}!
enter-a-number = введите число
Сообщение you-have-coin использует подстановку данных, полученных
извне (из нашего основного кода) в переменные { $user }
и
{ $value }
, а также у переменной { $value }
есть селекторы
[селектор]
, которые помогут выбрать форму сообщения в зависимости от
количества монет. Синтаксис такой:
идентификатор = необязательный текст { $переменная ->
*[селектор] вариант текста 1
[селектор] вариант текста 2
} еще необязательный текст
Итак, у нас есть сообщение, которое формирует строку перевода:
подставляет в { $user }
имя пользователя name
, а из { $value }
берется наша переменна n
.
С помощью селекторов происходит выбор множественной формы: [one]
при n
равному 1, [few]
при n от 2 до 4-х,
и [many]
в остальных случаях. Кроме того, мы добавили (просто для примера) собственный селектор [0]
, для
случая n
, равного 0. Он будет выводить сообщение, что монет нет,
без указания самого числа, при n
равном 0.
Будьте внимательны при переносе строк.
Звездочкой *
отмечается вариант селектора по-умолчанию, если Fluent
не смог применить ни один селектор. Вариант по-умолчанию должен быть указан всегда.
Поскольку, в английском нет малой множественной формы, то будем
обозначать вариантом по-умолчанию [many]
, а единственное число
вычислится в обоих языках по селектору [one]
.
Обратите внимание, что нам удалось всю сложную логику уместить в одном сообщении.
Текст перевода для английского:
hello = Hello, <b>{ $user }</b>!
cur-lang = Your current language: <i>{ $language }</i>
help = Help
you-have-coin = The user { $user } { $value ->
[zero] hasn't got nothing
[one] have one coin
*[many] has { $value } coins
}!
enter-a-number = Input a number, please.
В английском мы сделали свою логику, а в русском - свою. При этом заметьте, что не надо ничего менять в коде проекта.
Селекторы неявно обрабатываются встроенными функциями внутри Fluent. Но если нужны еще более сложные вещи, то можно придумать свою функцию внутри перевода и применять ее в конкретном переводе. Об этом можно прочитать в документации: https://projectfluent.org/fluent/guide/functions.html.
Еще пару слов о селекторах. Селектор может быть строкой. В этом случае он будет сравниваться непосредственно с ключами вариантов, определенных в выражении выбора.
Для селекторов, которые являются числами, ключи вариантов либо точно
соответствуют числу (мы сделали [0]
для n равному 0), либо
соответствуют категории множественного числа по справочнику проекта
unicode CLDR https://cldr.unicode.org/ для числа. Возможные категории
множественных чисел: [zero]
, [one]
, [two]
, [few]
,
[many]
, [other]
.
Если перевод требует, чтобы число было отформатировано не по умолчанию, селектор должен использовать те же параметры форматирования. Отформатированное число затем будет использоваться для выбора правильной категории множественного числа CLDR, которая для некоторых языков может отличаться от категории неформатированного числа. Вот пример из документации:
your-score =
{ NUMBER($score, minimumFractionDigits: 1) ->
[0.0] You scored zero points. What happened?
*[other] You scored { NUMBER($score, minimumFractionDigits: 1) } points.
}
Замечу, что здесь NUMBER – это встроенная функция, которая вызывается явно. Подробнее в документации: https://projectfluent.org/fluent/functions.html#built-in-functions.
Еще один пример использования селекторов — склонение имен и выражения для разных родов существительных.
Например, у нас уже есть в базе данных пол пользователя, и мы хотим вывести строку «Вася ответил(а) на ваше сообщение».
mention = { $mention-user } ответил(а) на ваше сообщение.
Преобразуем это в человеческий вид:
mention =
{ $mention-user } { $user-gender ->
[male] ответил
[female] ответила
*[other] ответил(а)
} на ваше сообщение.
Мы добавили третью опцию [other]
и отметили ее *
как опцию по-умолчанию.
В случаях, когда пол не получен, будет выдаваться обезличенная строка.
И в хэндлер нам нужно лишь дополнительно передать селектор в виде пола
(естественно из базы нужно его извлечь в виде male, female, так как будет произведено сравнение строк).
Примерно предполагается так:
gender = database.get_data(gender_data)
await message.answer(text=i18n.get("mention", mention-user=gender))
Еще один интересный кейс, это работа с параметризованными терминами, что очень важно для флективных языков.
Термин, это отдельный вид сообщений, начинающийся со знака дефис:
-термин
. Значения терминов следуют тем же правилам, что и значения
сообщений. Они могут быть и простым текстом, и включать в себя другие
выражения, включая переменные. Но сообщения получают данные для
переменных непосредственно из приложения, а вот термины получают такие
данные из сообщений, в которых они используются. То есть все происходит
внутри файлов перевода. Передача параметра выглядит так:
-термин(параметр: значение_параметра)
, где переменные и значение
параметра, доступные внутри термина, определены в скобках, например:
-brand-name =⠀
{ $case ->
*[nominative] Aiogram
[prepositional] Aiogram’е
}
about = Информация об { -brand-name(case: "prepositional") }.
download = Скачать { -brand-name }
Это фактически те же селекторы, но работают внутри файлов переводов.
await message.answer(text=i18n.get("about"))
await message.answer(text=i18n.get("download"))
Результат формирования сообщений будет “Информация об Aiogram’е” и “Скачать Aiogram”.
Передавая термин с параметром, вы можете определить выражение с несколькими вариантами одного и того же значения термина. Этот шаблон может быть полезен для определения аспектов термина, которые могут быть связаны с грамматической или стилистической особенностью языка. Во многих флективных языках (немецком, финском, венгерском, и славянских языках), предлог о (об) определяет падеж. Это может быть винительный падеж (немецкий), абляционный падеж (латинский) или предложный падеж (русский). Падежи могут быть определены посредством параметризации, а термин вызываться в нужной форме из других сообщений. Если в термин не переданы никакие параметры или если на термин ссылаются без скобок, будет использован вариант по умолчанию.
Важно, что параметризованный вызов терминов улучшает перевод на конкретном языке, не затрагивая структуру основного кода приложения.
Даты. Что касается правильного формирования дат, синтаксис Fluent поддерживает библиотеки форматов вышеупомянутой CLDR, достаточно записать строку с нужными параметрами форматирования и передать в неё время:
order-time = Время заказа: { DATETIME($date, month: "long", year: "numeric", day: "numeric", weekday: "long") }
В качестве переменной мы должны передать время в формате Unix Time:
unixdate = 556593884000
await message.answer(text=i18n.get("order-time", date=unixdate))
На выходе мы получим примерно: “Время заказа: вторник, 30 апреля 2019 г.” - для русского языка, и “Время заказа: Tuesday, April 30, 2019” для английского языка.
Ну вот мы познакомились с проектом Fluent. Несмотря на то, что он направлен на работу с фронтэндом и переводом UI браузера, мы смело можем его использовать в своих проектах на Python.
Документация, перевод документации и примеры использования по ссылкам ниже:
WIKI проекта на Github https://github.com/projectfluent/fluent/wiki
Документация https://projectfluent.org/fluent/guide/
Перевод документации https://blog.wtigga.com/fluent-syntax/
Playground песочница https://projectfluent.org/play/