Разрабатывая продукт, в какой-то момент задумываешься о поддержке языков. И, казалось бы, что могло пойти не так?
Иллюзия выбора
Давайте посмотрим, какие инструменты и библиотеки существуют, и что у них под капотом:
react-localization
Не самое популярное решение и на то есть причины, использование выглядит примерно так:
import LocalizedStrings from 'react-localization';
// Инициализация:
const strings = new LocalizedStrings({
en: {
how: "How do you want your egg today?",
choice: "How to choose the egg"
},
it: {
how: "Come vuoi il tuo uovo oggi?",
choice: "Come scegliere l'uovo"
}
});
// В компоненте:
<div>{strings.how}</div>
// Переключение:
strings.setLanguage('it');
Плюсы:
Простой API
Маленький вес (6.7kB)
Минусы:
При переключении языков автоматически не обновляются компоненты, нужно писать свою обертку
Нет поддержки асинхронной подгрузки переводов, вручную добавлять можно, но это, опять же, дополнительная сложность
Нет полноценной поддержки фолбеков, если в текущей локали не найден перевод
Под капотом используется localized-strings.
react-i18next
Пожалуй, самое популярное и распространенное решения для локализации. Использование выглядит так:
import i18n from "i18next";
import { useTranslation, initReactI18next } from "react-i18next";
// Инициализация:
i18n.use(initReactI18next).init({
resources: {
en: {
translation: {
how: "How do you want your egg today?",
choice: "How to choose the egg"
}
},
it: {
translation: {
how: "Come vuoi il tuo uovo oggi?",
choice: "Come scegliere l'uovo"
}
}
},
lng: "en",
fallbackLng: "en"
});
// В компоненте:
const { t } = useTranslation();
<div>{t('how')}</div>
// Или используя готовый компонент:
const { t } = useTranslation();
<Trans t={t}>how</Trans>
// Переключение:
const { i18n } = useTranslation();
i18n.changeLanguage('it');
Плюсы:
Большое комьюнити
Множество плагинов, которые позволяют, например, использовать асинхронную подгрузку других локалей
Автоматическое обновление компонентов при смене языка
Поддержка concurrent features (React 18), для упрощения работы с асинхронными манипуляциями
Минусы:
Весит 20.1 kB, но не дайте себя обмануть! Для его работы требуется i18next, который добавляет 54.5 kB, и того мы имеем 74.6 kB
Каждый плагин добавляет дополнительный вес вашему бандлу
Из примера использования понятно, что использует i18next.
@lingui/react
import { i18n } from '@lingui/core'
// Инициализация:
i18n.load('en', {
how: "How do you want your egg today?",
choice: "How to choose the egg"
});
i18n.load('it', {
how: "Come vuoi il tuo uovo oggi?",
choice: "Come scegliere l'uovo"
});
i18n.activate('en');
// В компоненте:
const context = useLingui();
<div>{context.i18n._('how')}</div>
// Или используя готовый компонент:
<Trans id="how" />
// Переключение:
const context = useLingui();
context.i18.activate('it');
Плюсы:
Простое API
Маленький вес (6.6 kB)
Поддерживает асинхронную загрузку, правда, не обойдется без дополнительного кода
Есть плагины, для более удобного использования, например, для автоматического определения языка
Минусы:
Все удобные инструменты находятся в @lingui/macro, которые не понятно сколько на самом деле весят, так как там используются макросы babel
react-intl
Достаточно распространенное решение, так как использует нативные методы форматирования и предоставляет полифилы для них.
import { IntlProvider } from 'react-intl';
const messages = {
en: {
how: "How do you want your egg today?",
choice: "How to choose the egg"
},
it: {
how: "Come vuoi il tuo uovo oggi?",
choice: "Come scegliere l'uovo"
}
};
// В рутовом компоненте:
const [lang, setLang] = useState('en');
<IntlProvider messages={messages[lang]} locale={lang}>
...
</IntlProvider>
// В компоненте:
const intl = useIntl();
<div>{intl.formatMessage({ id: 'how' })}</div>
// Или используя готовый компонент:
<FormattedMessage id="how" />
// Переключение:
// Нужно прокинуть в нужный компонент `setLang`
setLang('it');
Плюсы:
Поддерживает AST для переводов (подробнее в документации), с помощью которого можно уменьшить вес конечного бандла
Минусы:
Для переключения языков нужно реализовывать свою обертку
Большой вес (56.7 kB)
Имеет собственную достаточно специфичную реализацию форматирования
Под капотом использует formatjs.
Что имеем
Без рисков и излишнего бойлерплейта выбор падает на react-i18next, но не поймите меня не правильно, это не чистое решение в экосистеме React, да и вес оставляет желать лучшего.
i18nano
Давайте сформируем функциональные требования к библиотеке для локализации:
Поддержка асинхронной загрузки переводов, без бойлерплейта и специфичных решений, чтобы не грузить на клиент все переводы сразу и не раздувать тем самым бандл
Соответственно, поддержка лоадера или скелетона, пока загружается перевод
Удобный способ переключения между языками и автоматическое переключение на текущую локаль в компонентах
Поддержка фолбеков, если в текущем переводе не нашлось перевода
Поддержка шаблонизации, например, чтобы вставлять имя пользователя в строку с переводом
Из хотелок можно выделить поддержку вложенных переводов для человекопонятой структуризации, то бишь, например, main.header.title
и предзагрузку переводов.
Как выглядит?
import { TranslationProvider } from 'i18nano';
// Инициализация
const translations = {
// Используя динамический импорт
'en': () => import('translations/en.json'),
// или с помощью собственной реализации загрузки
'it': () => load('it')
};
// В рутовом компоненте:
<TranslationProvider translations={translations} language="en">
...
</TranslationProvider>
// В компоненте:
const t = useTranslation();
<div>{t('how')}</div>
// Или используя готовый компонент:
<Translation path="how" />
// Переключение:
const translation = useTranslationChange();
translation.change('it');
Для удобства
Поддерживается передача любого компонента в качестве лоадера или скелетона:
<Translation path="how">
<Loader />
</Translation>
Передача всех переданных поддерживаемых языков:
const translation = useTranslationChange();
<select value={translation.lang} onChange={(event) => {
translation.change(event.target.value);
}}>
{translation.all.map((lang) => (
<option key={lang} value={lang}>
{lang}
</option>
))}
</select>
Предзагрузка перевода отличного от текущего:
const translation = useTranslationChange();
<button onHover={() => translation.preload('it')}>
Sono italiano
</button>
Использование concurrent features из React 18, которые позволяют показывать предыдущий перевод, вместо лоадера, при смене языка:
<TranslationProvider unstable_transition={true}>
...
</TranslationProvider>
Поддержка шаблонизации:
// В файле перевода:
hello: 'Hello {{user.name}}!'
// Использование:
<Translation path="hello" values={{ user: { name: 'Ivan' } }}>
<Skeleton />
</Translation>
// -> Hello Ivan!
"Но есть один нюанс"
Кто-то может возразить, а как же форматирование дат? Как реализовать пруализацию?
Все проще, чем кажется: во-первых, можно реализовать через шаблонизацию, во-вторых, у всех встроенных типов, будь то Date
или Number
, есть методы toLocaleString
и подобные, в которые можно передать свой язык, а не только установленный по-умолчанию в браузере.
Через шаблонизацию:
date: '{{day}}/{{month}}/{{year}}'
<Translation path="date" values={{ day: '1', month: '4', year: '2021' }} />
// -> 1/4/2021
Через встроенный метод:
date: '{{date}}'
const date = new Date('2022-04-01').toLocaleDateString('it');
<Translation path="date" values={{ date }} />
// -> 1/4/2021
По поводу поддержки браузерами можно посмотреть caniuse — там все очень хорошо.
Что касается поддержки множественных чисел, то тут тоже все просто:
count: ['zero', 'one', 'two']
<Translation path={`count.${2}`} />
// -> two
Ссылки
Комментарии (6)
kai3341
01.04.2022 23:57Все приведённые примеры про статический текст, что решается совсем просто. Мой интерес, как решают проблемы типа той, что приведена ниже, не удовлетворён:
Поле ${field} заполнено неверно: ${error_description}
Вообще тут проблема в дизайне самого JS: шаблоны строк мы завезли, а явных методов форматирования нет
yroman
03.04.2022 23:26Не вижу здесь нормальной поддержки плюрализации в самих строках переводов. Вижу какую-то хрень наколеночную. Вы извините, конечно, но ваш велик сравнивать с i18next можно с оооочень большой натяжкой.
Houston
Мне больше всего нравится ttag. Особенно тот факт, что ключами в нём являются сами строки на дефолтном языке, и не нужно придумывать миллиард этих ключей
mayorovp
Когда те самые "строки на дефолтном языке" меняется — достоинство становится недостатком. К примеру, на ru.SO от подобного каждую неделю куски перевода "слетают".
KivApple
Одна и та же фраза может использоваться в нескольких местах и требовать, внезапно, разного перевода в зависимости от контекста использования. Так как смыслы слов в языках не связаны отношением 1 к 1. И уж тем более в разных языках разные устойчивые выражения для разных ситуаций.
"Фраза на естественном языке" может измениться в ходе разработки приложения и будет неудобно менять все места использования и все переводы.