FormatJS представляет собой набор библиотек и инструментов для локализации JavaScript-приложений. Основная задача экосистемы — обеспечить единый подход к интернационализации интерфейсов, форматированию данных и управлению переводами в приложениях различного масштаба.
Архитектура FormatJS строится вокруг нескольких ключевых принципов:
Intl);FormatJS активно применяется в React-приложениях, однако большая часть инфраструктуры может использоваться отдельно от React.
Экосистема включает несколько взаимосвязанных пакетов:
| Пакет | Назначение |
|---|---|
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-объектов |
Внутренняя архитектура разделяется на несколько уровней:
FormatJS не реализует собственные механизмы локализации «с нуля». Библиотека строится поверх встроенного API браузера:
Intl.NumberFormatIntl.DateTimeFormatIntl.RelativeTimeFormatIntl.ListFormatIntl.DisplayNamesIntl.PluralRulesНапример:
const formatter = new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
})
console.log(formatter.format(1000))
// 1 000,00 €
FormatJS инкапсулирует работу с этими API и предоставляет унифицированный интерфейс.
Runtime-часть отвечает за:
Типичная схема выглядит следующим образом:
Application
↓
IntlProvider
↓
Intl Context
↓
Formatting API
↓
Intl.* Objects
IntlProvider — центральный компонент архитектуры
react-intl.
Он выполняет несколько функций:
Пример:
import { IntlProvider } from 'react-intl'
<IntlProvider
locale="ru"
messages={messages}
>
<App />
</IntlProvider>
При инициализации создается объект конфигурации:
const config = {
locale,
formats,
messages,
defaultLocale,
defaultFormats,
timeZone
}
Далее создается объект intl, содержащий:
Упрощенная схема:
IntlProvider
├── config
├── cache
├── formatters
└── intl object
FormatJS использует React Context для передачи runtime-состояния.
Пример внутренней структуры:
const IntlContext = React.createContext(null)
Компоненты получают доступ к контексту через:
useIntlinjectIntl<Formatted*>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
id="app.greeting"
defaultMessage="Привет, {name}"
values={{ name: 'Анна' }}
/>
Архитектурно он выполняет:
idFormatJS использует 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 — ядро всей системы
сообщений.
Он отвечает за:
Сообщение:
Hello {name}
Преобразуется в AST:
[
{
type: 'literal',
value: 'Hello '
},
{
type: 'argument',
value: 'name'
}
]
После построения AST создается formatter-функция.
Упрощенно:
function format(values) {
return 'Hello ' + values.name
}
Для plural/select логика значительно сложнее.
FormatJS использует Intl.PluralRules.
Пример:
const rules = new Intl.PluralRules('ru')
rules.select(1) // one
rules.select(2) // few
rules.select(5) // many
Это особенно важно для славянских языков.
Пример:
{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)
Создание Intl.NumberFormat или
Intl.DateTimeFormat — дорогая операция.
Поэтому используется memoization.
const cache = new Map()
Ключ кэша:
locale + options
Например:
ru-RU|currency:RUB
format request
↓
cache lookup
↓
formatter exists?
┌───────┴───────┐
yes no
↓ ↓
reuse create formatter
↓ ↓
return save to cache
Каждое сообщение в FormatJS описывается объектом descriptor.
Пример:
{
id: 'app.header',
defaultMessage: 'Главная',
description: 'Название главной страницы'
}
Descriptor используется:
FormatJS умеет автоматически извлекать сообщения из исходного кода.
Пример:
intl.formatMessage({
id: 'app.title',
defaultMessage: 'Dashboard'
})
После extraction:
{
"app.title": "Dashboard"
}
babel-plugin-formatjs анализирует AST
JavaScript-кода.
Основные задачи:
Source Code
↓
Babel Parser
↓
AST Traversal
↓
Message Extraction
↓
Compiled Output
Plugin ищет:
defineMessages()
formatMessage()
<FormattedMessage />
CLI используется для:
Пример extraction:
formatjs extract "src/**/*.{js,ts,tsx}"
Переводы можно предварительно компилировать.
Исходный ICU:
Hello {name}
После компиляции:
function(d) {
return "Hello " + d.name
}
Предкомпиляция уменьшает runtime-нагрузку:
Наиболее распространенная структура:
/locales
/en
common.json
/ru
common.json
Крупные приложения делят переводы по namespace.
Пример:
auth.json
dashboard.json
profile.json
FormatJS хорошо сочетается с code splitting.
Пример:
const messages = await import(`./locales/${locale}.json`)
user locale
↓
dynamic import
↓
messages chunk
↓
IntlProvider update
↓
rerender
Если перевод отсутствует:
<IntlProvider
locale="ru"
defaultLocale="en"
>
Используется fallback.
message exists?
├── yes → localized message
└── no
↓
defaultMessage exists?
├── yes → defaultMessage
└── no
↓
message id
FormatJS поддерживает React-элементы внутри переводов.
Пример:
<FormattedMessage
id="welcome"
defaultMessage="Нажмите <b>сюда</b>"
values={{
b: chunks => <b>{chunks}</b>
}}
/>
Парсер создает дерево:
text
└── tag
└── text
Далее генерируется React tree.
FormatJS поддерживает строгую типизацию.
Пример:
type MessageIds =
| 'app.title'
| 'app.subtitle'
Возможна генерация типов автоматически.
intl.formatMessage({
id: 'app.title'
})
Ошибочный ID:
intl.formatMessage({
id: 'wrong.id'
})
может быть обнаружен на этапе компиляции.
FormatJS совместим с:
Request
↓
detect locale
↓
load messages
↓
create intl context
↓
renderToString
↓
HTML response
Критически важно, чтобы:
Иначе возможны hydration mismatch errors.
Не все окружения поддерживают полный набор Intl API.
FormatJS предоставляет polyfills:
@formatjs/intl-pluralrules@formatjs/intl-relativetimeformat@formatjs/intl-numberformat@formatjs/intl-listformatenvironment check
↓
native support?
├── yes → native Intl
└── no
↓
polyfill
Наиболее затратные операции:
Убирает parser из runtime.
Повторно использует formatter-объекты.
Снижает размер initial bundle.
Уменьшает объем переводов.
src/
modules/
auth/
locales/
dashboard/
locales/
Некоторые приложения создают единый registry:
const messages = {
ru,
en,
de
}
FormatJS поддерживает callback:
onEr ror={(err) => {
console.error(err)
}}
| Ошибка | Причина |
|---|---|
| Missing translation | Нет перевода |
| Invalid ICU syntax | Ошибка ICU |
| Missing value | Нет переменной |
| Unsupported locale | Локаль не поддерживается |
FormatJS оборачивает Intl.DateTimeFormat.
intl.formatDate(new Date(), {
year: 'numeric',
month: 'long'
})
intl.formatDate(date, {
timeZone: 'Europe/Moscow'
})
intl.formatNumber(1000, {
style: 'currency',
currency: 'RUB'
})
intl.formatNumber(1200000, {
notation: 'compact'
})
Результат:
1,2 млн
intl.formatRelativeTime(-1, 'day')
Результат:
вчера
При определении локали используется алгоритм сопоставления.
Пример:
ru-KZ
может быть преобразован в:
ru
browser locales
↓
supported locales
↓
best match
↓
active locale
Архитура FormatJS делится на две независимые части.
Включает:
Включает:
Полный жизненный цикл сообщения:
Source Code
↓
Message Descriptor
↓
Extraction
↓
Translation
↓
Compilation
↓
Runtime Formatting
↓
Rendered UI
Единый синтаксис для всех локалей.
Минимизация собственных реализаций.
Подходит для enterprise-приложений.
Большая часть инфраструктуры не зависит от React.
Высокая производительность в production.
Plural/select-сообщения быстро усложняются.
Без tree-shaking и compilation bundle может вырасти.
В старых окружениях требуются polyfills.
Ошибки ICU часто обнаруживаются только во время выполнения.
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