Типы для сообщений

FormatJS предоставляет строгую модель работы с сообщениями, основанную на ICU MessageFormat и типизации TypeScript. Центральная идея заключается в том, что каждое сообщение рассматривается как структурированный объект, а не как произвольная строка. Это позволяет проверять корректность параметров на этапе компиляции и снижает вероятность ошибок при интернационализации интерфейса.

Базовая модель сообщения

Сообщение в FormatJS описывается через интерфейс MessageDescriptor. Он задаёт минимальный контракт:

  • id — уникальный идентификатор сообщения
  • defaultMessage — строка по умолчанию
  • description — контекст для переводчиков (опционально)
import { MessageDescriptor } from 'react-intl';

const message: MessageDescriptor = {
  id: 'user.greeting',
  defaultMessage: 'Hello, world',
  description: 'Greeting shown on home page',
};

Типизация гарантирует, что id и defaultMessage присутствуют, а дополнительные поля не нарушают структуру.


ICU MessageFormat как основа типизации

FormatJS использует ICU MessageFormat как основу синтаксиса. Это означает, что сообщение может содержать не только текст, но и структурные элементы:

  • интерполяцию значений
  • множественные формы (pluralization)
  • выбор (sel ect)
  • форматирование чисел, дат, валют
  • вложенные сообщения

Пример базовой интерполяции:

const message = {
  id: 'cart.items',
  defaultMessage: 'You have {count} items',
};

Здесь count становится параметром сообщения, который должен быть передан при рендеринге.


Типизация параметров сообщений

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

Простые параметры

type Values = {
  name: string;
};

const message = {
  id: 'welcome.user',
  defaultMessage: 'Hello, {name}',
};

При использовании через react-intl:

intl.formatMessage(message, { name: 'Alex' });

TypeScript может выводить тип name как обязательный параметр.


Типы для plural (множественные формы)

ICU plural-формат автоматически превращает числовые параметры в управляемые ветвления:

const message = {
  id: 'inbox.messages',
  defaultMessage: 'You have {count, plural, one {# message} other {# messages}}',
};

Типизация

Параметр count обязан быть числом:

type Values = {
  count: number;
};

Особенность заключается в том, что TypeScript фиксирует тип count как number, поскольку plural-синтаксис ICU требует числовой дискриминант.


Типизация sel ect-выражений

select используется для ветвления по строковым значениям:

const message = {
  id: 'user.status',
  defaultMessage:
    '{status, select, active {Active} inactive {Inactive} banned {Banned} other {Unknown}}',
};

Тип параметра

type Values = {
  status: 'active' | 'inactive' | 'banned' | string;
};

TypeScript может уточнять тип до union, если он явно задан в коде приложения.


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

FormatJS включает типизированные форматтеры для встроенных значений.

Number formatting

const message = {
  id: 'price.label',
  defaultMessage: 'Price: {price, number, currency}',
};

Типизация:

type Values = {
  price: number;
};

Date formatting

const message = {
  id: 'event.date',
  defaultMessage: 'Date: {date, date, long}',
};
type Values = {
  date: Date | number;
};

Автоматическое выведение типов через defineMessages

Функция defineMessages позволяет группировать сообщения и сохранять типизацию на уровне объекта.

import { defineMessages } fr om 'react-intl';

const messages = defineMessages({
  title: {
    id: 'page.title',
    defaultMessage: 'Dashboard',
  },
  greeting: {
    id: 'page.greeting',
    defaultMessage: 'Hello, {name}',
  },
});

Поведение типов

TypeScript выводит структуру:

{
  title: MessageDescriptor;
  greeting: MessageDescriptor;
}

При этом параметры каждого сообщения могут быть выведены отдельно при использовании formatMessage.


defineMessage и локальная типизация

defineMessage используется для одиночных сообщений и позволяет сохранять строгую типизацию даже вне групп.

import { defineMessage } fr om 'react-intl';

const message = defineMessage({
  id: 'button.submit',
  defaultMessage: 'Submit',
});

Тип результата сохраняет структуру MessageDescriptor.


Типы в IntlShape

IntlShape — основной интерфейс доступа к API форматирования.

import { IntlShape } from 'react-intl';

function render(intl: IntlShape) {
  return intl.formatMessage(
    {
      id: 'hello',
      defaultMessage: 'Hello {name}',
    },
    {
      name: 'User',
    }
  );
}

Важные типовые свойства:

  • formatMessage
  • formatDate
  • formatNumber
  • formatTime

Каждый метод строго типизирует входные параметры.


Выведение типов значений сообщений

TypeScript может автоматически сопоставлять параметры с ICU-синтаксисом при использовании вспомогательных утилит.

Пример условной типизации

type ExtractValues<T> = T extends { defaultMessage: string }
  ? Record<string, string | number | Date>
  : never;

Такой подход используется в кастомных обёртках над FormatJS для усиления типовой безопасности.


Типы для rich-text сообщений

Rich-text форматирование позволяет использовать React-элементы внутри сообщений:

const message = {
  id: 'terms',
  defaultMessage: 'Accept <b>terms</b> and <link>conditions</link>',
};

Типизация компонентов

type Values = {
  b: (chunks: string) => React.ReactNode;
  link: (chunks: string) => React.ReactNode;
};

Это обеспечивает строгую проверку всех вставляемых компонентов.


MessageFormatElement и AST-типизация

Внутри FormatJS сообщения разбираются в AST (Abstract Syntax Tree). Основной тип — MessageFormatElement.

Он включает:

  • literal — текст
  • argument — переменная
  • number, date, time — форматированные аргументы
  • plural — множественные формы
  • select — выбор
type MessageFormatElement =
  | LiteralElement
  | ArgumentElement
  | NumberElement
  | DateElement
  | TimeElement
  | PluralElement
  | SelectElement;

Эта структура используется внутри intl-messageformat для парсинга строк сообщений.


Строгая типизация ключей сообщений

При использовании TypeScript можно фиксировать список допустимых id:

type MessageKeys = 'home.title' | 'home.subtitle';

type Messages = Record<MessageKeys, MessageDescriptor>;

Это исключает возможность обращения к несуществующим сообщениям.


Интеграция с React-компонентами

В react-intl типизация сообщений тесно связана с useIntl:

import { useIntl } from 'react-intl';

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

  return intl.formatMessage({
    id: 'title',
    defaultMessage: 'Title',
  });
}

Тип intl содержит строго определённые методы, каждый из которых ожидает корректные значения ICU-параметров.


Параметризация и безопасность типов

Главная цель типизации сообщений — предотвращение несоответствия между шаблоном и передаваемыми значениями.

Типовые ошибки, которые устраняются:

  • отсутствие обязательного параметра
  • передача строки вместо числа в plural
  • несоответствие ключей select-выражения
  • неверные типы для дат и чисел

Кастомные расширения типов

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

type AppMessages = {
  [K in string]: {
    id: K;
    defaultMessage: string;
    description?: string;
  };
};

Или строгая обёртка:

type TypedMessage<T extends string> = {
  id: T;
  defaultMessage: string;
};

Связь типов и runtime-валидации

FormatJS разделяет:

  • compile-time типизацию (TypeScript)
  • runtime парсинг ICU строк

Это означает, что даже при строгой типизации окончательная проверка структуры происходит во время выполнения через парсер ICU.

Такая архитектура позволяет сохранять баланс между безопасностью и гибкостью локализации.