Архитектура и основные компоненты

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

Архитектура FormatJS строится вокруг нескольких ключевых принципов:

  • использование стандарта ECMAScript Internationalization API (Intl);
  • декларативное описание переводимых сообщений;
  • независимость от конкретного фреймворка;
  • поддержка ICU MessageFormat;
  • разделение runtime-части и инструментов сборки.

FormatJS активно применяется в React-приложениях, однако большая часть инфраструктуры может использоваться отдельно от React.


Общая структура экосистемы FormatJS

Экосистема включает несколько взаимосвязанных пакетов:

Пакет Назначение
react-intl Интернационализация React-приложений
intl-messageformat Парсинг и выполнение ICU MessageFormat
@formatjs/intl Базовые утилиты интернационализации
@formatjs/cli Извлечение и компиляция переводов
@formatjs/ts-transformer Трансформация сообщений на этапе сборки
babel-plugin-formatjs Оптимизация и extraction через Babel
@formatjs/intl-localematcher Сопоставление локалей
@formatjs/fast-memoize Кэширование formatter-объектов

Внутренняя архитектура разделяется на несколько уровней:

  1. Уровень Intl API
  2. Уровень форматирования сообщений
  3. Уровень runtime-компонентов
  4. Уровень extraction и компиляции
  5. Уровень интеграции с приложением

Intl API как фундамент FormatJS

FormatJS не реализует собственные механизмы локализации «с нуля». Библиотека строится поверх встроенного API браузера:

  • Intl.NumberFormat
  • Intl.DateTimeFormat
  • Intl.RelativeTimeFormat
  • Intl.ListFormat
  • Intl.DisplayNames
  • Intl.PluralRules

Например:

const formatter = new Intl.NumberFormat('fr-FR', {
  style: 'currency',
  currency: 'EUR'
})

console.log(formatter.format(1000))
// 1 000,00 €

FormatJS инкапсулирует работу с этими API и предоставляет унифицированный интерфейс.


Основной runtime-слой

Архитектурная схема runtime

Runtime-часть отвечает за:

  • хранение активной локали;
  • загрузку переводов;
  • форматирование сообщений;
  • кэширование formatter-объектов;
  • предоставление API компонентам приложения.

Типичная схема выглядит следующим образом:

Application
    ↓
IntlProvider
    ↓
Intl Context
    ↓
Formatting API
    ↓
Intl.* Objects

IntlProvider

IntlProvider — центральный компонент архитектуры react-intl.

Он выполняет несколько функций:

  • хранит текущую локаль;
  • хранит словарь сообщений;
  • создает formatter-контекст;
  • передает настройки через React Context API.

Пример:

import { IntlProvider } from 'react-intl'

<IntlProvider
  locale="ru"
  messages={messages}
>
  <App />
</IntlProvider>

Внутреннее устройство IntlProvider

При инициализации создается объект конфигурации:

const config = {
  locale,
  formats,
  messages,
  defaultLocale,
  defaultFormats,
  timeZone
}

Далее создается объект intl, содержащий:

  • formatter-методы;
  • кэш;
  • runtime-конфигурацию.

Упрощенная схема:

IntlProvider
    ├── config
    ├── cache
    ├── formatters
    └── intl object

Контекст интернационализации

FormatJS использует React Context для передачи runtime-состояния.

Пример внутренней структуры:

const IntlContext = React.createContext(null)

Компоненты получают доступ к контексту через:

  • useIntl
  • injectIntl
  • <Formatted*>

useIntl

useIntl предоставляет доступ к runtime API.

import { useIntl } from 'react-intl'

function Price() {
  const intl = useIntl()

  return intl.formatNumber(1000, {
    style: 'currency',
    currency: 'USD'
  })
}

Форматирующие компоненты

FormatJS предоставляет декларативные компоненты.

Основные:

Компонент Назначение
FormattedMessage Перевод сообщений
FormattedDate Форматирование даты
FormattedTime Форматирование времени
FormattedNumber Форматирование чисел
FormattedRelativeTime Относительное время
FormattedPlural Работа с plural forms
FormattedList Форматирование списков

FormattedMessage

Это ключевой компонент всей системы.

<FormattedMessage
  id="app.greeting"
  defaultMessage="Привет, {name}"
  values={{ name: 'Анна' }}
/>

Архитектурно он выполняет:

  1. Поиск сообщения по id
  2. Компиляцию ICU-шаблона
  3. Подстановку переменных
  4. Возврат React-элемента

ICU MessageFormat

Роль ICU в FormatJS

FormatJS использует ICU Message syntax как основной язык описания переводов.

Пример:

Hello {name}

Pluralization:

{count, plural,
  one {# item}
  few {# items}
  many {# items}
  other {# items}
}

Select:

{gender, select,
  male {He}
  female {She}
  other {They}
}

Архитектура обработки сообщений

Процесс обработки ICU-сообщения:

Message String
      ↓
Parser
      ↓
AST
      ↓
Compiler
      ↓
Formatter Function
      ↓
Formatted Output

intl-messageformat

Пакет intl-messageformat — ядро всей системы сообщений.

Он отвечает за:

  • parsing ICU syntax;
  • создание AST;
  • форматирование строк;
  • plural/select логику;
  • interpolation.

Парсер ICU

Сообщение:

Hello {name}

Преобразуется в AST:

[
  {
    type: 'literal',
    value: 'Hello '
  },
  {
    type: 'argument',
    value: 'name'
  }
]

Компиляция сообщений

После построения AST создается formatter-функция.

Упрощенно:

function format(values) {
  return 'Hello ' + values.name
}

Для plural/select логика значительно сложнее.


Система pluralization

FormatJS использует Intl.PluralRules.

Пример:

const rules = new Intl.PluralRules('ru')

rules.select(1) // one
rules.select(2) // few
rules.select(5) // many

Это особенно важно для славянских языков.


ICU plural architecture

Пример:

{count, plural,
  one {# файл}
  few {# файла}
  many {# файлов}
  other {# файла}
}

Внутренний алгоритм:

count
   ↓
PluralRules
   ↓
plural category
   ↓
message branch
   ↓
formatted result

Система форматтеров

FormatJS создает formatter-объекты поверх Intl.

Например:

intl.formatDate(date)
intl.formatNumber(number)
intl.formatRelativeTime(value, unit)

Кэширование formatter-объектов

Создание Intl.NumberFormat или Intl.DateTimeFormat — дорогая операция.

Поэтому используется memoization.

const cache = new Map()

Ключ кэша:

locale + options

Например:

ru-RU|currency:RUB

Архитектура memoization

format request
      ↓
cache lookup
      ↓
formatter exists?
   ┌───────┴───────┐
 yes              no
  ↓                ↓
reuse         create formatter
  ↓                ↓
return         save to cache

Message Descriptor

Каждое сообщение в FormatJS описывается объектом descriptor.

Пример:

{
  id: 'app.header',
  defaultMessage: 'Главная',
  description: 'Название главной страницы'
}

Назначение descriptor

Descriptor используется:

  • во время extraction;
  • при генерации переводов;
  • в runtime;
  • при проверке уникальности сообщений.

Система extraction

Назначение extraction

FormatJS умеет автоматически извлекать сообщения из исходного кода.

Пример:

intl.formatMessage({
  id: 'app.title',
  defaultMessage: 'Dashboard'
})

После extraction:

{
  "app.title": "Dashboard"
}

Babel Plugin FormatJS

babel-plugin-formatjs анализирует AST JavaScript-кода.

Основные задачи:

  • поиск переводимых сообщений;
  • извлечение descriptor-объектов;
  • оптимизация runtime;
  • удаление лишних полей.

Архитектура Babel plugin

Source Code
     ↓
Babel Parser
     ↓
AST Traversal
     ↓
Message Extraction
     ↓
Compiled Output

Что анализирует plugin

Plugin ищет:

defineMessages()
formatMessage()
<FormattedMessage />

CLI-инструменты

@formatjs/cli

CLI используется для:

  • extraction;
  • compilation;
  • validation;
  • управления переводами.

Пример extraction:

formatjs extract "src/**/*.{js,ts,tsx}"

Компиляция переводов

Переводы можно предварительно компилировать.

Исходный ICU:

Hello {name}

После компиляции:

function(d) {
  return "Hello " + d.name
}

Преимущества precompilation

Предкомпиляция уменьшает runtime-нагрузку:

  • не нужен parser в браузере;
  • быстрее рендеринг;
  • меньше bundle size;
  • меньше CPU overhead.

Архитектура хранения переводов

Наиболее распространенная структура:

/locales
    /en
        common.json
    /ru
        common.json

Namespace-подход

Крупные приложения делят переводы по namespace.

Пример:

auth.json
dashboard.json
profile.json

Динамическая загрузка переводов

FormatJS хорошо сочетается с code splitting.

Пример:

const messages = await import(`./locales/${locale}.json`)

Архитектура lazy loading

user locale
     ↓
dynamic import
     ↓
messages chunk
     ↓
IntlProvider update
     ↓
rerender

Локаль и fallback-механизм

defaultLocale

Если перевод отсутствует:

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

Используется fallback.


Алгоритм fallback

message exists?
    ├── yes → localized message
    └── no
          ↓
defaultMessage exists?
    ├── yes → defaultMessage
    └── no
          ↓
message id

Rich Text Formatting

FormatJS поддерживает React-элементы внутри переводов.

Пример:

<FormattedMessage
  id="welcome"
  defaultMessage="Нажмите <b>сюда</b>"
  values={{
    b: chunks => <b>{chunks}</b>
  }}
/>

Внутренняя обработка rich text

Парсер создает дерево:

text
 └── tag
      └── text

Далее генерируется React tree.


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

FormatJS поддерживает строгую типизацию.

Пример:

type MessageIds =
  | 'app.title'
  | 'app.subtitle'

Typed Message IDs

Возможна генерация типов автоматически.

intl.formatMessage({
  id: 'app.title'
})

Ошибочный ID:

intl.formatMessage({
  id: 'wrong.id'
})

может быть обнаружен на этапе компиляции.


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

SSR в FormatJS

FormatJS совместим с:

  • Next.js
  • Remix
  • Express SSR
  • React Server Components

Архитектура SSR

Request
   ↓
detect locale
   ↓
load messages
   ↓
create intl context
   ↓
renderToString
   ↓
HTML response

Hydration и локализация

Критически важно, чтобы:

  • locale на сервере и клиенте совпадала;
  • messages были идентичны;
  • timezone не различалась.

Иначе возможны hydration mismatch errors.


Polyfills

Проблема поддержки Intl

Не все окружения поддерживают полный набор Intl API.

FormatJS предоставляет polyfills:

  • @formatjs/intl-pluralrules
  • @formatjs/intl-relativetimeformat
  • @formatjs/intl-numberformat
  • @formatjs/intl-listformat

Архитектура polyfill-пакетов

environment check
       ↓
native support?
   ├── yes → native Intl
   └── no
         ↓
      polyfill

Производительность

Основные источники нагрузки

Наиболее затратные операции:

  • parsing ICU;
  • создание formatter-объектов;
  • plural resolution;
  • React rerendering.

Основные методы оптимизации

Предкомпиляция сообщений

Убирает parser из runtime.

Memoization

Повторно использует formatter-объекты.

Lazy loading локалей

Снижает размер initial bundle.

Разделение namespace

Уменьшает объем переводов.


Архитектура сообщений в больших приложениях

Domain-based структура

src/
  modules/
    auth/
      locales/
    dashboard/
      locales/

Centralized registry

Некоторые приложения создают единый registry:

const messages = {
  ru,
  en,
  de
}

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

FormatJS поддерживает callback:

onEr ror={(err) => {
  console.error(err)
}}

Типы runtime-ошибок

Ошибка Причина
Missing translation Нет перевода
Invalid ICU syntax Ошибка ICU
Missing value Нет переменной
Unsupported locale Локаль не поддерживается

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

DateTimeFormat abstraction

FormatJS оборачивает Intl.DateTimeFormat.

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

TimeZone support

intl.formatDate(date, {
  timeZone: 'Europe/Moscow'
})

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

Currency formatting

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

Compact notation

intl.formatNumber(1200000, {
  notation: 'compact'
})

Результат:

1,2 млн

Relative Time Formatting

intl.formatRelativeTime(-1, 'day')

Результат:

вчера

Архитектура locale matching

При определении локали используется алгоритм сопоставления.

Пример:

ru-KZ

может быть преобразован в:

ru

Locale negotiation

browser locales
      ↓
supported locales
      ↓
best match
      ↓
active locale

Build-time и Runtime

Архитура FormatJS делится на две независимые части.

Build-time

Включает:

  • extraction;
  • compilation;
  • validation;
  • optimization.

Runtime

Включает:

  • форматирование;
  • рендеринг;
  • pluralization;
  • interpolation.

Поток обработки сообщения

Полный жизненный цикл сообщения:

Source Code
    ↓
Message Descriptor
    ↓
Extraction
    ↓
Translation
    ↓
Compilation
    ↓
Runtime Formatting
    ↓
Rendered UI

Архитектурные преимущества FormatJS

Стандартизированный ICU

Единый синтаксис для всех локалей.

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

Минимизация собственных реализаций.

Масштабируемость

Подходит для enterprise-приложений.

Framework-agnostic ядро

Большая часть инфраструктуры не зависит от React.

Build-time optimization

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


Ограничения архитектуры

Сложность ICU

Plural/select-сообщения быстро усложняются.

Размер runtime

Без tree-shaking и compilation bundle может вырасти.

Зависимость от Intl

В старых окружениях требуются polyfills.

Высокая чувствительность к структуре сообщений

Ошибки ICU часто обнаруживаются только во время выполнения.


Типичная enterprise-архитектура

src/
 ├── app/
 ├── modules/
 ├── locales/
 │     ├── en/
 │     ├── ru/
 │     └── de/
 ├── i18n/
 │     ├── provider.tsx
 │     ├── config.ts
 │     ├── loaders.ts
 │     └── formatters.ts
 └── messages/

Взаимодействие компонентов экосистемы

Полная схема работы FormatJS:

React Components
        ↓
react-intl
        ↓
intl-messageformat
        ↓
Intl API
        ↓
Browser Runtime

Build-time pipeline:

Source Code
      ↓
Babel Plugin / CLI
      ↓
Message Extraction
      ↓
Translation Files
      ↓
Compilation
      ↓
Production Bundle