Разрабатывая продукт, в какой-то момент задумываешься о поддержке языков. И, казалось бы, что могло пойти не так?
Иллюзия выбора
Давайте посмотрим, какие инструменты и библиотеки существуют, и что у них под капотом:
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
Ссылки
ссылка на оригинал статьи https://habr.com/ru/post/658713/
Добавить комментарий