Разрабатывая продукт, в какой-то момент задумываешься о поддержке языков. И, казалось бы, что могло пойти не так?

Иллюзия выбора

Давайте посмотрим, какие инструменты и библиотеки существуют, и что у них под капотом:

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

Ссылки

  1. react-localization

  2. react-i18next

  3. @lingui/react

  4. react-intl

  5. i18nano

Комментарии (6)


  1. Houston
    01.04.2022 22:56

    Мне больше всего нравится ttag. Особенно тот факт, что ключами в нём являются сами строки на дефолтном языке, и не нужно придумывать миллиард этих ключей


    1. mayorovp
      02.04.2022 00:07
      +2

      Когда те самые "строки на дефолтном языке" меняется — достоинство становится недостатком. К примеру, на ru.SO от подобного каждую неделю куски перевода "слетают".


    1. KivApple
      02.04.2022 02:02
      +1

      1. Одна и та же фраза может использоваться в нескольких местах и требовать, внезапно, разного перевода в зависимости от контекста использования. Так как смыслы слов в языках не связаны отношением 1 к 1. И уж тем более в разных языках разные устойчивые выражения для разных ситуаций.

      2. "Фраза на естественном языке" может измениться в ходе разработки приложения и будет неудобно менять все места использования и все переводы.


  1. kai3341
    01.04.2022 23:57

    Все приведённые примеры про статический текст, что решается совсем просто. Мой интерес, как решают проблемы типа той, что приведена ниже, не удовлетворён:

    Поле ${field} заполнено неверно: ${error_description}

    Вообще тут проблема в дизайне самого JS: шаблоны строк мы завезли, а явных методов форматирования нет


    1. kai3341
      03.04.2022 17:57

      UPD: ммм, статья обновлена. Спасибо!


  1. yroman
    03.04.2022 23:26

    Не вижу здесь нормальной поддержки плюрализации в самих строках переводов. Вижу какую-то хрень наколеночную. Вы извините, конечно, но ваш велик сравнивать с i18next можно с оооочень большой натяжкой.