В экосистеме FormatJS типизация играет ключевую роль в обеспечении корректности работы с интернационализацией на уровне кода. Особенно это важно при использовании ICU Message Format, где структура сообщений может быть сложной и динамической. Одним из мощных инструментов становится генерация TypeScript-типов на основе дескрипторов сообщений, что позволяет связать runtime-локализацию с compile-time проверками.
В FormatJS сообщения обычно описываются в виде JSON-дескрипторов. Каждый дескриптор содержит метаданные, необходимые для рендеринга строки: идентификатор, дефолтный текст, описание и, при необходимости, параметры.
Пример дескриптора:
{
"greeting": {
"defaultMessage": "Hello, {name}!",
"description": "Приветствие пользователя"
}
}
В данном случае ключ greeting и параметр
name являются кандидатами для автоматического извлечения
типов. Цель генерации типов — превратить подобные структуры в строго
типизированные контракты.
Без генерации типов работа с FormatJS часто опирается на
string-ключи и ручное управление параметрами. Это приводит
к нескольким проблемам:
Генерация типов решает эти проблемы, создавая строгую связь между словарём сообщений и их использованием в коде.
Процесс генерации типов строится вокруг анализа структуры сообщений и извлечения:
Результатом становится TypeScript-модель, отражающая структуру локализационных данных.
Для простого набора сообщений генерация может выглядеть следующим образом:
type Messages = {
greeting: {
defaultMessage: string;
description: string;
};
};
Однако более важным является уровень параметров сообщений.
FormatJS поддерживает ICU Message Syntax:
{
"welcome": {
"defaultMessage": "Hello, {name}, you have {count} messages"
}
}
Из такого сообщения необходимо извлечь параметры name и
count.
Сгенерированный тип для параметров:
type WelcomeParams = {
name: string;
count: number;
};
На практике тип count может быть уточнён в зависимости
от контекста (например, number | string), но базовая модель
строится именно через анализ ICU-выражений.
Генерация типов обычно проходит несколько этапов.
На первом этапе JSON-дескрипторы преобразуются в AST-подобную структуру. ICU-строка разбивается на:
Пример:
"{count, plural, one {1 message} other {# messages}}"
Из AST выделяются переменные:
count как входной параметр;Для каждой переменной определяется тип:
number;string;Date.Если тип неявный, используется универсальный тип.
На основе извлечённых данных формируется итоговый тип:
type MessageParams = {
count: number;
};
В реальных приложениях используется единый тип для всех сообщений:
type IntlMessages = {
greeting: { name: string };
welcome: { count: number };
};
Такой подход позволяет централизовать контроль над всеми локализационными ключами.
FormatJS часто используется с вложенными структурами:
{
"auth": {
"login": {
"title": "Sign in",
"error": "Invalid credentials for {email}"
}
}
}
Типизация в этом случае должна отражать вложенность:
type IntlMessages = {
auth: {
login: {
title: {};
error: {
email: string;
};
};
};
};
Здесь пустой объект {} используется как маркер
отсутствия параметров.
При использовании react-intl типизация становится
особенно важной. Компонент <FormattedMessage>
принимает параметры через values.
Пример:
<FormattedMessage
id="welcome"
values={{ name: "Alex", count: 5 }}
/>
Сгенерированный тип позволяет проверить корректность:
type WelcomeValues = {
name: string;
count: number;
};
Если передать лишний параметр или забыть обязательный, TypeScript выдаст ошибку.
В экосистеме FormatJS существует CLI-инструментарий, позволяющий автоматизировать процесс генерации.
Обычно процесс включает:
defineMessages;.d.ts файлов.Пример результата:
declare namespace IntlMessages {
export interface Messages {
greeting: { name: string };
welcome: { count: number };
}
}
Генерация типов может быть встроена в пайплайн сборки:
Типичный сценарий:
Сложность возникает при использовании динамических ключей:
formatMessage({ id: dynamicKey });
В таких случаях строгая типизация теряет часть своей эффективности,
поскольку dynamicKey не может быть заранее известен.
Для таких сценариев применяется:
keyof и mapped typesTypeScript позволяет усиливать типизацию через mapped types:
type MessageKeys = keyof IntlMessages;
Это даёт возможность строго ограничить допустимые ID:
function formatMessage(id: MessageKeys, values?: any) {}
Таким образом исключаются несуществующие ключи.
Дополнительно генерация типов может учитывать структуру ICU-выражений:
Пример сложного сообщения:
{
"notifications": {
"defaultMessage": "{count, plural, one {# notification} other {# notifications}}"
}
}
Тип:
type NotificationsParams = {
count: number;
};
Несмотря на высокую полезность, генерация типов имеет ограничения:
Эти ограничения требуют компромисса между строгостью и гибкостью.
В крупных приложениях используется разделение:
Каждый модуль имеет собственный набор сгенерированных типов:
type AuthMessages = { ... };
type ProfileMessages = { ... };
type BillingMessages = { ... };
После этого они объединяются:
type AppMessages = AuthMessages & ProfileMessages & BillingMessages;
Важным аспектом является поддержание синхронности:
Это обеспечивает соответствие между кодом и переводами без ручного вмешательства.