Интернационализация (i18n
)
Плагин интернационализации заставляет вашего бота говорить на нескольких языках.
Не путать
Не путайте его с fluent.
Этот плагин - улучшенная версия fluent, которая работает как на Deno, так и на Node.js.
Объяснение интернационализации
Этот раздел объясняет, что такое интернационализация, зачем она нужна, что в ней сложного, как она связана с локализацией и зачем вам нужен плагин для всего этого. Если вы уже знаете эти вещи, прокрутите страницу до раздела Начало работы.
Во-первых, internationalization
— это очень длинное слово. Поэтому люди любят писать первую букву (i) и последнюю (n). Затем они подсчитывают все оставшиеся буквы (nternationalizatio - 18 букв) и помещают это число между i и n, так что в итоге получается i18n. Не спрашивайте нас, почему. Так что i18n — это просто странная аббревиатура слова internationalization.
Так же поступают и с локализацией, которая превращается в l10n.
Что такое локализация?
Локализация означает создание бота, который может говорить на нескольких языках. Он должен автоматически подстраивать свой язык под язык пользователя.
Локализовать можно не только язык. Вы также можете учесть культурные различия или другие стандарты, такие как форматы даты и времени. Вот еще несколько примеров того, что в разных странах представлено по-разному:
- Даты
- Времена
- Числа
- Единицы измерения
- Множественные числа
- Гендеры
- Переносы
- Большие буквы
- Выравнивание
- Символы и иконки
- Сортировка
… и много другое.
Все эти вещи в совокупности определяют локаль пользователя. Локалям часто присваивают двухбуквенные коды, например en
для английского языка, de
для немецкого, ru
для русского и так далее. Если вы хотите узнать код своей локали, посмотрите этот список.
Что такое интернационализация?
В двух словах, интернационализация означает написание кода, который может подстраиваться под локаль пользователя. Другими словами, интернационализация — это то, что обеспечивает локализацию (см. выше). Это означает, что хотя ваш бот в принципе работает одинаково для всех, конкретные сообщения, которые он отправляет, отличаются от пользователя к пользователю, поэтому бот может говорить на разных языках.
Вы занимаетесь интернационализацией, если не пишите тексты, которые отправляет бот, а считываете их из файла динамически. Вы делаете интернационализацию, если не пишите жесткое представление дат и времени, а используете библиотеку, которая корректирует эти значения в соответствии с различными стандартами. Вы поняли идею: Не стоит жестко программировать то, что должно меняться в зависимости от места проживания пользователя или языка, на котором он говорит.
Зачем нам нужен этот плагин?
Этот плагин поможет вам в процессе интернационализации. Он основан на Fluent — системе локализации, созданной Mozilla. Эта система имеет очень мощный и элегантный синтаксис, который позволяет вам писать естественные переводы эффективным способом.
По сути, вы можете извлечь все, что должно быть изменено в зависимости от локали пользователя, в некоторые текстовые файлы, которые вы помещаете рядом с вашим кодом. Затем вы можете использовать этот плагин для загрузки этих локализаций. Плагин автоматически определит локаль пользователя и позволит вашему боту выбрать нужный язык для общения.
Ниже мы будем называть эти текстовые файлы файлами перевода. Они должны соответствовать синтаксису Fluent.
Начало работы
В этом разделе описывается настройка структуры проекта и места размещения файлов перевода. Если вы уже знакомы с этим, пропустите вперед, чтобы узнать, как установить и использовать плагин.
Существует несколько способов добавить больше языков в бот. Самый простой способ - создать папку с файлами перевода Fluent. Обычно эта папка называется locales
. Файлы перевода должны иметь расширение .ftl
(fluent).
Вот пример структуры проекта:
.
├── bot.ts
└── locales/
├── de.ftl
├── en.ftl
├── it.ftl
└── ru.ftl
Если вы не знакомы с синтаксисом Fluent, вы можете прочитать их руководство: https://
Вот пример файла перевода для английского языка, который называется locales
:
start = Hi, how can I /help you?
help =
Send me some text, and I can make it bold for you.
You can change my language using the /language command.
2
3
4
Русский эквивалент будет называться locales
и выглядеть следующим образом:
start = Привет, как я могу /help вам?
help =
Пришлите мне текст, и я сделаю его полу-жирным.
Вы можете изменить мой язык с помощью команды /language.
2
3
4
Теперь вы можете использовать эти переводы в своем боте через плагин. Он сделает их доступными через ctx
:
bot.command("start", async (ctx) => {
await ctx.reply(ctx.t("start"));
});
bot.command("help", async (ctx) => {
await ctx.reply(ctx.t("help"));
});
2
3
4
5
6
7
Когда вы вызываете ctx
, локаль текущего контекстного объекта ctx
используется для поиска подходящего перевода. Поиск правильного перевода осуществляется с помощью посредника локали. В простейшем случае он просто возвращает ctx
.
В результате пользователи с разными локалями смогут читать сообщения каждый на своем языке.
Использование
Плагин определяет локаль пользователя на основе множества различных факторов. Одним из них является ctx
, который будет предоставлен клиентом пользователя.
Однако существует множество других факторов, которые можно использовать для определения локали пользователя. Например, вы можете хранить локаль пользователя в вашей сессии. Таким образом, существует два основных способа использования этого плагина: С сессиями и Без сессий.
Без сессий
Проще использовать и настраивать плагин без сессий. Его главный недостаток заключается в том, что вы не можете хранить языки, которые выбирают пользователи.
Как уже говорилось выше, локаль, которую будет использовать пользователь, определяется с помощью ctx
, который поступает от клиента пользователя. Но язык по умолчанию будет использоваться, если у вас нет перевода на этот язык. Иногда бот может не видеть предпочитаемый язык пользователя, предоставленный его клиентом, и в этом случае также будет использоваться язык по умолчанию.
Код ctx
будет виден только в том случае, если пользователь ранее начал приватную беседу с вашим ботом..
import { Bot, Context } from "grammy";
import { I18n, I18nFlavor } from "@grammyjs/i18n";
// Для поддержки TypeScript и автозаполнения,
// расширьте контекст с помощью расширителя I18n:
type MyContext = Context & I18nFlavor;
// Создайте бота, как обычно.
// Не забудьте расширить контекст.
const bot = new Bot<MyContext>("");
// Создайте экземпляр `I18n`.
// Продолжайте читать, чтобы узнать, как настроить экземпляр.
const i18n = new I18n<MyContext>({
defaultLocale: "ru", // смотрите ниже для получения дополнительной информации
directory: "locales", // Загрузите все файлы перевода из locales/.
});
// Наконец, зарегистрируйте экземпляр i18n в боте,
// чтобы сообщения переводились на ходу!
bot.use(i18n);
// Теперь все готово.
// Вы можете получить доступ к переводам с помощью `t` или `translate`.
bot.command("start", async (ctx) => {
await ctx.reply(ctx.t("start-msg"));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const { Bot } = require("grammy");
const { I18n } = require("@grammyjs/i18n");
// Создайте бота, как обычно.
const bot = new Bot("");
// Создайте экземпляр `I18n`.
// Продолжайте читать, чтобы узнать, как настроить экземпляр.
const i18n = new I18n({
defaultLocale: "ru", // смотрите ниже для получения дополнительной информации
directory: "locales", // Загрузите все файлы перевода из locales/.
});
// Наконец, зарегистрируйте экземпляр i18n в боте,
// чтобы сообщения переводились на ходу!
bot.use(i18n);
// Теперь все готово.
// Вы можете получить доступ к переводам с помощью `t` или `translate`.
bot.command("start", async (ctx) => {
await ctx.reply(ctx.t("start-msg"));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Bot, Context } from "https://deno.land/x/grammy@v1.33.0/mod.ts";
import { I18n, I18nFlavor } from "https://deno.land/x/grammy_i18n@v1.1.0/mod.ts";
// Для поддержки TypeScript и автозаполнения,
// расширьте контекст с помощью расширителя I18n:
type MyContext = Context & I18nFlavor;
// Создайте бота, как обычно.
// Не забудьте расширить контекст.
const bot = new Bot<MyContext>("");
// Создайте экземпляр `I18n`.
// Продолжайте читать, чтобы узнать, как настроить экземпляр.
const i18n = new I18n<MyContext>({
defaultLocale: "ru", // смотрите ниже для получения дополнительной информации
// Загрузите все файлы перевода из locales/. (Не работает в Deno Deploy.)
directory: "locales",
});
// Загрузка файлов перевода таким образом работает и в Deno Deploy.
// await i18n.loadLocalesDir("locales");
// Наконец, зарегистрируйте экземпляр i18n в боте,
// чтобы сообщения переводились на ходу!
bot.use(i18n);
// Теперь все готово.
// Вы можете получить доступ к переводам с помощью `t` или `translate`.
bot.command("start", async (ctx) => {
await ctx.reply(ctx.t("start-msg"));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ctx
возвращает переведенное сообщение для указанного ключа. Вам не нужно беспокоиться о языках, так как они будут выбраны плагином автоматически.
Поздравляем! Теперь ваш бот говорит на нескольких языках! 🌍🎉
С сессиями
Предположим, что у вашего бота есть команда /language
. Как правило, в grammY мы можем использовать sessions для хранения данных пользователя в чате. Чтобы сообщить вашему экземпляру интернационализации, что сессии включены, нужно установить use
в true
в опциях I18n
.
Вот пример, включающий простую команду /language
:
import { Bot, Context, session, SessionFlavor } from "grammy";
import { I18n, I18nFlavor } from "@grammyjs/i18n";
interface SessionData {
__language_code?: string;
}
type MyContext = Context & SessionFlavor<SessionData> & I18nFlavor;
const bot = new Bot<MyContext>("");
const i18n = new I18n<MyContext>({
defaultLocale: "ru",
useSession: true, // хранить ли язык пользователя в сессии
directory: "locales", // Загрузите все файлы перевода из locales/.
});
// Не забудьте зарегистрировать middleware `session` перед тем, как
// регистрировать middleware для i18n
bot.use(
session({
initial: () => {
return {};
},
}),
);
// Зарегистрируйте middleware для i18n
bot.use(i18n);
bot.command("start", async (ctx) => {
await ctx.reply(ctx.t("greeting"));
});
bot.command("language", async (ctx) => {
if (ctx.match === "") {
return await ctx.reply(ctx.t("language.specify-a-locale"));
}
// `i18n.locales` содержит все локали, которые были зарегистрированы
if (!i18n.locales.includes(ctx.match)) {
return await ctx.reply(ctx.t("language.invalid-locale"));
}
// `ctx.i18n.getLocale` возвращает текущую используемую локаль.
if ((await ctx.i18n.getLocale()) === ctx.match) {
return await ctx.reply(ctx.t("language.already-set"));
}
await ctx.i18n.setLocale(ctx.match);
await ctx.reply(ctx.t("language.language-set"));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const { Bot, session } = require("grammy");
const { I18n } = require("@grammyjs/i18n");
const bot = new Bot("");
const i18n = new I18n({
defaultLocale: "ru",
useSession: true, // хранить ли язык пользователя в сессии
directory: "locales", // Загрузите все файлы перевода из locales/.
});
// Не забудьте зарегистрировать middleware `session` перед тем, как
// регистрировать middleware для i18n
bot.use(
session({
initial: () => {
return {};
},
}),
);
// Зарегистрируйте middleware для i18n
bot.use(i18n);
bot.command("start", async (ctx) => {
await ctx.reply(ctx.t("greeting"));
});
bot.command("language", async (ctx) => {
if (ctx.match === "") {
return await ctx.reply(ctx.t("language.specify-a-locale"));
}
// `i18n.locales` содержит все локали, которые были зарегистрированы
if (!i18n.locales.includes(ctx.match)) {
return await ctx.reply(ctx.t("language.invalid-locale"));
}
// `ctx.i18n.getLocale` возвращает текущую используемую локаль.
if ((await ctx.i18n.getLocale()) === ctx.match) {
return await ctx.reply(ctx.t("language.already-set"));
}
await ctx.i18n.setLocale(ctx.match);
await ctx.reply(ctx.t("language.language-set"));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import {
Bot,
Context,
session,
SessionFlavor,
} from "https://deno.land/x/grammy@v1.33.0/mod.ts";
import { I18n, I18nFlavor } from "https://deno.land/x/grammy_i18n@v1.1.0/mod.ts";
interface SessionData {
__language_code?: string;
}
type MyContext = Context & SessionFlavor<SessionData> & I18nFlavor;
const bot = new Bot<MyContext>("");
const i18n = new I18n<MyContext>({
defaultLocale: "ru",
useSession: true, // хранить ли язык пользователя в сессии
// НЕ РАБОТАЕТ в Deno Deploy
directory: "locales",
});
// Загрузка файлов перевода таким образом работает и в Deno Deploy.
// await i18n.loadLocalesDir("locales");
// Не забудьте зарегистрировать middleware `session` перед тем, как
// регистрировать middleware для i18n
bot.use(
session({
initial: () => {
return {};
},
}),
);
// Зарегистрируйте middleware для i18n
bot.use(i18n);
bot.command("start", async (ctx) => {
await ctx.reply(ctx.t("greeting"));
});
bot.command("language", async (ctx) => {
if (ctx.match === "") {
return await ctx.reply(ctx.t("language.specify-a-locale"));
}
// `i18n.locales` содержит все локали, которые были зарегистрированы
if (!i18n.locales.includes(ctx.match)) {
return await ctx.reply(ctx.t("language.invalid-locale"));
}
// `ctx.i18n.getLocale` возвращает текущую используемую локаль.
if ((await ctx.i18n.getLocale()) === ctx.match) {
return await ctx.reply(ctx.t("language.already-set"));
}
await ctx.i18n.setLocale(ctx.match);
await ctx.reply(ctx.t("language.language-set"));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Когда сессии включены, свойство _
в сессии будет использоваться вместо ctx
(предоставляемого клиентом Telegram) при выборе языка. Когда ваш бот отправляет сообщения, локаль выбирается из ctx
.
Существует метод set
, который вы можете использовать для установки желаемого языка. Он сохранит это значение в вашей сессии.
await ctx.i18n.setLocale("de");
Это эквивалентно ручной настройке в сессии, а затем повторному определению локали:
ctx.session.__language_code = "de";
await ctx.i18n.renegotiateLocale();
2
Переопределение локали
Если вы используете сессии или что-то еще — помимо ctx
— для выбора пользовательской локали, есть некоторые ситуации, когда вы можете изменить язык при обработке обновления. Например, посмотрите на приведенный выше пример с использованием сессий.
Когда вы делаете только
ctx.session.__language_code = "de";
он не будет обновлять текущую используемую локаль в экземпляре I18n
. Вместо этого он обновляет только сессию. Таким образом, изменения произойдут только при следующем обновлении.
Если вы не можете дождаться следующего обновления, вам может понадобиться обновить изменения после обновления языка пользователя. Для таких случаев используйте метод renegotiate
.
ctx.session.__language_code = "de";
await ctx.i18n.renegotiateLocale();
2
После этого, когда бы мы ни использовали метод t
, бот будет пытаться ответить немецким переводом этого сообщения (указанным в locales
).
Также помните, что при использовании встроенных сессий вы можете добиться того же результата с помощью метода set
.
Установка локали без сессий
Если в случае работы без сессий вам необходимо установить локаль для пользователя, вы можете сделать это с помощью метода use
.
await ctx.i18n.useLocale("de");
Устанавливает указанную локаль для использования в будущих переводах. Эффект действует только в текущем обновлении и не сохраняется. Этот метод можно использовать для изменения локали перевода в середине обновления (например, когда пользователь меняет язык).
Пользовательское согласование локали
Вы можете использовать опцию locale
, чтобы указать пользовательское определение локали. Эта опция полезна, если вы хотите выбрать локаль на основе внешних источников (например, баз данных) или в других ситуациях, когда вы хотите контролировать, какая локаль будет использоваться.
По умолчанию плагин выбирает локаль в следующем порядке:
Если сессии включены, попробуйте прочитать
_
из сессии. Если он возвращает правильную локаль, то она используется. Если ничего не возвращается или возвращается незарегистрированная локаль, переходите к шагу 2._language _code Попытайтесь прочитать из
ctx
. Если он возвращает действительную локаль, то она используется. Если он не возвращает ничего или возвращает незарегистрированную локаль, перейдите к шагу 3..from .language _code Обратите внимание, что
ctx
доступен только в том случае, если пользователь запустил бота. Это означает, что если бот увидит пользователя в группе или где-то еще без предварительного запуска бота, он не сможет увидеть.from .language _code ctx
..from .language _code Попробуйте использовать язык по умолчанию, настроенный в опциях
I18n
. Если он установлен в правильную локаль, то он будет использоваться. Если он не указан или установлен в незарегистрированную локаль, перейдите к шагу 4.Попробуйте использовать английский язык (
en
). Плагин сам устанавливает эту локаль как конечную резервную. Несмотря на то, что это запасная локаль, и мы рекомендуем иметь перевод, это не является обязательным условием. Если английская локаль не указана, переходите к шагу 5.Если все вышеперечисленное не помогло, используйте
{key}
вместо перевода. Мы настоятельно рекомендуем установить локаль, которая существует в ваших переводах, в качествеdefault
в опцияхLocale I18n
.
Определение локали
Определение локали обычно происходит только один раз во время обработки обновлений Telegram. Однако вы можете выполнить команду ctx
для повторного вызова определителя и установки новой локали. Это полезно, если локаль меняется во время обработки одного обновления.
Вот пример locale
, где мы используем locale
из сессии вместо _
. В таком случае не нужно устанавливать use
на true
в опциях I18n
.
const i18n = new I18n<MyContext>({
localeNegotiator: (ctx) =>
ctx.session.locale ?? ctx.from?.language_code ?? "ru",
});
2
3
4
const i18n = new I18n({
localeNegotiator: (ctx) =>
ctx.session.locale ?? ctx.from?.language_code ?? "ru",
});
2
3
4
Если пользовательский определитель локали возвращает недопустимую локаль, он отступает и выбирает локаль, следуя вышеуказанному порядку.
Отображение переведенных сообщений
Давайте рассмотрим отображение сообщений подробнее.
bot.command("start", async (ctx) => {
// Вызовите "translate" или "t" для отображения
// сообщения, указав его ID и дополнительные параметры:
await ctx.reply(ctx.t("welcome"));
});
2
3
4
5
Теперь вы можете /start
своего бота. Он должен отобразить следующее сообщение
Привет!
Подстановка
Иногда вы можете захотеть поместить такие значения, как числа и имена, внутрь строк. Это можно сделать с помощью подстановки.
bot.command("cart", async (ctx) => {
// Вы можете передать подстановочные данные в качестве второго объекта.
await ctx.reply(ctx.t("cart-msg", { items: 10 }));
});
2
3
4
Объект { items:
называется контекстом перевода строки cart
.
Теперь, используя команду /cart
:
В настоящее время в вашей корзине находится 10 товаров.
Попробуйте изменить значение переменной items
и посмотрите, как изменится отображаемое сообщение! Также ознакомьтесь с документацией по Fluent, особенно с документацией по подстановщикам.
Глобальные переменные подстановки
Может быть полезно указать количество переменных, которые должны быть доступны для всех переводов. Например, если вы используете имя пользователя во многих сообщениях, может быть утомительно передавать везде контекст перевода { name:
.
На помощь приходят глобальные подстановки! Рассмотрим следующее:
const i18n = new I18n<MyContext>({
defaultLocale: "ru",
directory: "locales",
// Определители глобальное доступные переменные
globalTranslationContext(ctx) {
return { name: ctx.from?.first_name ?? "" };
},
});
bot.use(i18n);
bot.command("start", async (ctx) => {
// Можно использовать `name`, не указывая его снова!
await ctx.reply(ctx.t("welcome"));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Возможные проблемы с форматированием
По умолчанию Fluent использует знаки изоляции Unicode для интерполяций.
Если вы используете подстановки внутри тегов или сущностей, наличие изолирующих знаков может привести к неправильному форматированию (например, вместо ожидаемой ссылки или кештега — обычный текст).
Чтобы исправить это, используйте следующие параметры:
const i18n = new I18n({
fluentBundleOptions: { useIsolating: false },
});
2
3
Добавление перевода
Существует три основных способа загрузки переводов.
Загрузка локалей с помощью опции directory
Самый простой способ добавить переводы в экземпляр I18n
— это разместить все переводы в папке и указать её название в опциях.
const i18n = new I18n({
directory: "locales",
});
2
3
Загрузка локалей из директории
Этот метод - то же самое, что и указание папки
в параметрах. Просто поместите их все в папку и загрузите следующим образом:
const i18n = new I18n();
await i18n.loadLocalesDir("locales"); // асинхронная версия
i18n.loadLocalesDirSync("locales-2"); // синхронная версия
2
3
4
Обратите внимание, что некоторые среды требуют использования версии
async
. Например, Deno Deploy не поддерживает синхронные файловые операции.
Загрузка одной локали
Также можно добавить один перевод в экземпляр. Вы можете указать путь к файлу перевода, используя
const i18n = new I18n();
await i18n.loadLocale("en", { filePath: "locales/en.ftl" }); // асинхронная версия
i18n.loadLocaleSync("ru", { filePath: "locales/ru.ftl" }); // синхронная версия
2
3
4
или вы можете напрямую загрузить данные перевода в виде строки, как показано ниже:
const i18n = new I18n();
// асинхронная версия
await i18n.loadLocale("en", {
source: `greeting = Hello { $name }!
language-set = Language has been set to English!`,
});
// синхронная версия
i18n.loadLocaleSync("ru", {
source: `greeting = Привет, { $name }!
language-set = Язык был установлен на Русский!`,
});
2
3
4
5
6
7
8
9
10
11
12
13
Прослушивание локализованного текста
Нам удалось отправить локализованные сообщения пользователю. Теперь давайте рассмотрим, как прослушивать сообщения, отправленные пользователем. В grammY мы обычно используем обработчик bot
для прослушивания входящих сообщений. Но поскольку мы говорили об интернационализации, в этом разделе мы рассмотрим, как прослушивать локализованные входящие сообщения.
Эта функция пригодится, если у вашего бота есть пользовательские клавиатуры, содержащие локализованный текст.
Вот небольшой пример прослушивания локализованного текстового сообщения, отправленного с помощью пользовательской клавиатуры. Вместо того чтобы использовать обработчик bot
, мы используем bot
в сочетании с middleware hears
, предоставляемым этим плагином.
import { hears } from "@grammyjs/i18n";
bot.filter(hears("back-to-menu-btn"), async (ctx) => {
await ctx.reply(ctx.t("main-menu-msg"));
});
2
3
4
5
const { hears } = require("@grammyjs/i18n");
bot.filter(hears("back-to-menu-btn"), async (ctx) => {
await ctx.reply(ctx.t("main-menu-msg"));
});
2
3
4
5
import { hears } from "https://deno.land/x/grammy_i18n@v1.1.0/mod.ts";
bot.filter(hears("back-to-menu-btn"), async (ctx) => {
await ctx.reply(ctx.t("main-menu-msg"));
});
2
3
4
5
Вспомогательная функция hears
позволяет вашему боту прослушать сообщение, написанное в локали пользователя.
Дальнейшие шаги
- Завершите чтение документации по Fluent, особенно руководства по синтаксису.
- Ознакомьтесь с соответствующими примерами этого плагина для Deno и Node.js.