Установка и начальная настройка

FormatJS — набор библиотек для интернационализации JavaScript-приложений. Основная задача библиотеки — организация локализации интерфейса, форматирование дат, времени, чисел, валют и сообщений в соответствии с региональными настройками пользователя.

Экосистема FormatJS включает несколько пакетов:

  • react-intl — интернационализация React-приложений;
  • intl-messageformat — форматирование сообщений ICU;
  • @formatjs/intl — полифилы для API Intl;
  • babel-plugin-formatjs — извлечение и оптимизация сообщений;
  • @formatjs/cli — инструменты командной строки;
  • @formatjs/ts-transformer — интеграция с TypeScript.

Библиотека построена вокруг стандарта ECMAScript Internationalization API (Intl) и активно использует ICU Message Syntax.


Установка базовых зависимостей

Установка для React-приложения

Наиболее распространённый вариант использования — пакет react-intl.

Установка через npm:

npm install react-intl

Установка через yarn:

yarn add react-intl

Установка через pnpm:

pnpm add react-intl

После установки становятся доступны:

  • React-компоненты локализации;
  • хуки;
  • API форматирования;
  • поддержка ICU-сообщений.

Минимальные требования среды

FormatJS использует встроенный объект Intl. Современные браузеры поддерживают его по умолчанию, однако старые среды могут требовать полифилы.

Проверка поддержки:

console.log(Intl);

Проверка конкретного API:

console.log(Intl.RelativeTimeFormat);

При отсутствии поддержки потребуется подключение дополнительных модулей.


Установка полифилов Intl

Поддержка старых браузеров

Для Internet Explorer и старых мобильных браузеров используется пакет:

npm install @formatjs/intl-pluralrules

Дополнительно могут понадобиться:

npm install @formatjs/intl-relativetimeformat
npm install @formatjs/intl-numberformat
npm install @formatjs/intl-datetimeformat

Подключение полифилов

Пример инициализации:

import '@formatjs/intl-pluralrules/polyfill';
import '@formatjs/intl-relativetimeformat/polyfill';

import '@formatjs/intl-pluralrules/locale-data/ru';
import '@formatjs/intl-relativetimeformat/locale-data/ru';

Для английской локали:

import '@formatjs/intl-pluralrules/locale-data/en';

Структура проекта локализации

Типичная структура:

src/
├── i18n/
│   ├── messages/
│   │   ├── en.json
│   │   └── ru.json
│   ├── config.js
│   └── provider.jsx
├── components/
└── app.jsx

Назначение файлов

Файл Назначение
en.json Английские переводы
ru.json Русские переводы
config.js Конфигурация локалей
provider.jsx Подключение IntlProvider

Создание файлов переводов

Английская локаль

{
  "app.title": "Application",
  "menu.home": "Home",
  "menu.profile": "Profile"
}

Русская локаль

{
  "app.title": "Приложение",
  "menu.home": "Главная",
  "menu.profile": "Профиль"
}

Подключение IntlProvider

Главный компонент библиотеки — IntlProvider.

Базовая настройка

import React from 'react';
import ReactDOM from 'react-dom/client';
import { IntlProvider } from 'react-intl';

import ruMessages from './i18n/messages/ru.json';

import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <IntlProvider locale="ru" messages={ruMessages}>
    <App />
  </IntlProvider>
);

Назначение IntlProvider

IntlProvider выполняет несколько задач:

  • хранение текущей локали;
  • передача переводов через Context API;
  • форматирование сообщений;
  • настройка форматирования чисел и дат;
  • обработка отсутствующих переводов.

Основные свойства IntlProvider

locale

Текущая локаль приложения.

<IntlProvider locale="ru">

messages

Объект переводов.

<IntlProvider messages={messages}>

defaultLocale

Локаль по умолчанию.

<IntlProvider
  locale="ru"
  defaultLocale="en"
>

onError

Обработчик ошибок локализации.

<IntlProvider
  onEr ror={(error) => {
    console.error(error);
  }}
>

Использование FormattedMessage

Наиболее популярный компонент библиотеки.

import { FormattedMessage } from 'react-intl';

function Header() {
  return (
    <h1>
      <FormattedMessage id="app.title" />
    </h1>
  );
}

Форматирование с defaultMessage

defaultMessage используется как резервный текст.

<FormattedMessage
  id="menu.home"
  defaultMessage="Home"
/>

Если перевод отсутствует, будет показан текст из defaultMessage.


Использование ICU Message Syntax

FormatJS использует ICU-синтаксис.

Пример параметров:

{
  "welcome": "Добро пожаловать, {name}"
}

Использование:

<FormattedMessage
  id="welcome"
  values={{ name: 'Алексей' }}
/>

Результат:

Добро пожаловать, Алексей

Настройка динамической локали

Хранение локали в состоянии

import { useState } from 'react';
import { IntlProvider } from 'react-intl';

import ru from './ru.json';
import en from './en.json';

const messages = {
  ru,
  en
};

function App() {
  const [locale, setLocale] = useState('ru');

  return (
    <IntlProvider
      locale={locale}
      messages={messages[locale]}
    >
      <Main />
    </IntlProvider>
  );
}

Автоматическое определение языка браузера

const locale = navigator.language;

Пример:

console.log(navigator.language);

Результат:

ru-RU

Нормализация локали

Часто используются только короткие коды языка.

const locale = navigator.language.split('-')[0];

Результат:

ru

Конфигурационный файл локализации

Пример:

import en from './messages/en.json';
import ru from './messages/ru.json';

export const messages = {
  en,
  ru
};

export const defaultLocale = 'en';

Организация переводов

Существует несколько подходов к именованию ключей.

Иерархическая схема

{
  "header.logo": "Логотип",
  "header.profile": "Профиль"
}

Feature-based схема

{
  "auth.login.title": "Авторизация",
  "auth.login.button": "Войти"
}

Flat-структура

{
  "loginButton": "Войти"
}

На крупных проектах предпочтительна feature-based организация.


Настройка Babel Plugin

Для извлечения сообщений используется:

npm install babel-plugin-formatjs --save-dev

Конфигурация Babel

{
  "plugins": [
    [
      "formatjs",
      {
        "idInterpolationPattern": "[sha512:contenthash:base64:6]",
        "ast": true
      }
    ]
  ]
}

Назначение babel-plugin-formatjs

Плагин решает несколько задач:

  • извлечение переводов;
  • оптимизация production-сборки;
  • удаление лишних сообщений;
  • генерация идентификаторов;
  • валидация ICU-синтаксиса.

Установка CLI-инструментов

npm install @formatjs/cli --save-dev

Извлечение сообщений

Пример команды:

formatjs extract "src/**/*.{js,jsx,ts,tsx}" \
--out-file lang/en.json

Генерация переводов

formatjs compile lang/en.json \
--out-file compiled/en.json

Поддержка TypeScript

Установка типов:

npm install --save-dev @types/react

Сам react-intl уже содержит встроенные типы.


Использование с TypeScript

import { FormattedMessage } from 'react-intl';

export function Title() {
  return (
    <FormattedMessage id="app.title" />
  );
}

Типизация сообщений

type MessageIds =
  | 'app.title'
  | 'menu.home'
  | 'menu.profile';

Создание типизированного helper

import { useIntl } from 'react-intl';

export function useTranslate() {
  const intl = useIntl();

  return (id: string) => intl.formatMessage({ id });
}

Lazy Loading локалей

Для уменьшения размера bundle локали часто загружаются динамически.

Пример динамического импорта

async function loadLocale(locale) {
  switch (locale) {
    case 'ru':
      return import('./messages/ru.json');

    case 'en':
      return import('./messages/en.json');

    default:
      return import('./messages/en.json');
  }
}

Асинхронная инициализация переводов

const [messages, setMessages] = useState(null);

useEffect(() => {
  loadLocale(locale).then((module) => {
    setMessages(module.default);
  });
}, [locale]);

Проверка загрузки переводов

if (!messages) {
  return <div>Loading...</div>;
}

Интеграция с Vite

Установка:

npm install react-intl

Настройка дополнительных плагинов обычно не требуется.

Пример vite.config.js:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()]
});

Интеграция с Next.js

Установка:

npm install react-intl

Пример использования:

import { IntlProvider } from 'react-intl';

export default function App({ Component, pageProps }) {
  return (
    <IntlProvider locale="ru">
      <Component {...pageProps} />
    </IntlProvider>
  );
}

Серверный рендеринг

FormatJS поддерживает SSR без дополнительной конфигурации.

Пример:

import { renderToString } from 'react-dom/server';

Библиотека использует Context API, поэтому серверная локализация работает прозрачно.


Использование useIntl

Хук предоставляет доступ к API форматирования.

import { useIntl } from 'react-intl';

function Profile() {
  const intl = useIntl();

  return (
    <h1>
      {intl.formatMessage({
        id: 'profile.title'
      })}
    </h1>
  );
}

Форматирование чисел

intl.formatNumber(1000);

Формат валюты:

intl.formatNumber(1000, {
  style: 'currency',
  currency: 'RUB'
});

Форматирование дат

intl.formatDate(new Date());

С дополнительными параметрами:

intl.formatDate(new Date(), {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

Режим разработки и production

В development-режиме FormatJS показывает предупреждения:

  • отсутствующие переводы;
  • ошибки ICU;
  • неправильные параметры сообщений.

В production часть проверок отключается для повышения производительности.


Обработка отсутствующих переводов

Пользовательский обработчик

<IntlProvider
  onEr ror={(error) => {
    if (error.code === 'MISSING_TRANSLATION') {
      return;
    }

    console.error(error);
  }}
>

Частые ошибки настройки

Отсутствие locale-data

Ошибка:

Missing locale data

Причина — не подключены данные локали для полифила.


Неверный id сообщения

<FormattedMessage id="unknown.key" />

Результат:

[React Intl] Missing message

Ошибки ICU-синтаксиса

Неверный шаблон:

{
  "msg": "Hello {name"
}

Правильный вариант:

{
  "msg": "Hello {name}"
}

Рекомендации по начальной настройке

Использование defaultMessage

Рекомендуется всегда указывать резервный текст:

<FormattedMessage
  id="button.save"
  defaultMessage="Save"
/>

Централизованное хранение локалей

Оптимальный подход:

i18n/
  messages/
  hooks/
  providers/
  utils/

Разделение переводов по модулям

Пример:

messages/
  auth/
  dashboard/
  profile/

Такой подход упрощает поддержку больших приложений.


Базовая production-конфигурация

Пример:

<IntlProvider
  locale={locale}
  messages={messages}
  defaultLocale="en"
  onEr ror={() => {}}
>
  <App />
</IntlProvider>

В production обычно отключается вывод предупреждений в консоль.


Полная минимальная конфигурация

import React from 'react';
import ReactDOM from 'react-dom/client';
import { IntlProvider } from 'react-intl';

import messages from './messages/ru.json';

import App from './App';

ReactDOM.createRoot(
  document.getElementById('root')
).render(
  <IntlProvider
    locale="ru"
    messages={messages}
    defaultLocale="en"
  >
    <App />
  </IntlProvider>
);