В экосистеме FormatJS ключевая сложность локализации связана не с
форматированием сообщений, а с их полнотой. Отсутствующие переводы
возникают, когда идентификатор сообщения присутствует в коде, но
отсутствует в наборе локализованных ресурсов для выбранной локали. В
результате система вынуждена использовать fallback-значения или
выбрасывать ошибки, в зависимости от конфигурации intl.
Поведение определяется несколькими уровнями: runtime-библиотекой
(react-intl или @formatjs/intl), конфигурацией
провайдера и инструментами извлечения сообщений. Отсутствие единого
контроля на всех уровнях приводит к появлению «тихих» дефектов, которые
проявляются только в продакшене или при смене локали.
Сообщения в FormatJS идентифицируются через id. Типичная
структура:
{
id: "user.profile.title",
defaultMessage: "Profile"
}
Проблема отсутствующего перевода возникает в следующих сценариях:
id, но старые файлы локализации не
синхронизированыОсобую сложность создают динамические идентификаторы:
intl.formatMessage({ id: `error.${code}` })
Такие конструкции часто выпадают из статического анализа, что приводит к неполным каталогам переводов.
В react-intl поведение зависит от конфигурации
IntlProvider.
defaultMessageЕсли указан defaultMessage, он используется как
fallback:
intl.formatMessage({
id: "dashboard.title",
defaultMessage: "Dashboard"
})
При отсутствии перевода возвращается defaultMessage.
defaultMessageЕсли перевод отсутствует и defaultMessage не задан,
поведение зависит от onError:
id или пустой строки (в
зависимости от конфигурации)Ключевым механизмом контроля является onError в
IntlProvider:
<IntlProvider
locale="ru"
messages={messages}
onEr ror={(err) => {
if (err.code === "MISSING_TRANSLATION") {
logMissingTranslation(err.message);
return;
}
throw err;
}}
>
На уровне архитектуры этот хук используется для:
babel-plugin-formatjs позволяет извлекать сообщения во
время сборки:
{
"plugins": [
["formatjs", {
"idInterpolationPattern": "[sha512:contenthash:base64:6]"
}]
]
}
Экстракция выполняется командой:
formatjs extract "src/**/*.ts" --out-file messages.json
Результат используется как источник истины для всех локалей.
Критически важный аспект — синхронизация:
en.json считается базовымТиповая проверка включает сравнение ключей:
const base = require("./en.json");
const ru = require("./ru.json");
const missing = Object.keys(base).filter(
(key) => !(key in ru)
);
if (missing.length > 0) {
console.error("Missing translations:", missing);
process.exit(1);
}
Такая проверка предотвращает попадание неполных локалей в production.
В дополнение к CI применяется runtime-логирование. В
react-intl возможно перехватывать форматирование через
обёртку:
function safeFormatMessage(intl, descriptor) {
const message = intl.messages?.[descriptor.id];
if (!message) {
reportMissing(descriptor.id);
}
return intl.formatMessage(descriptor);
}
Этот слой полезен для выявления:
TypeScript позволяет снизить количество отсутствующих переводов через строгие типы:
type MessageIds =
| "dashboard.title"
| "user.profile.title";
const messages: Record<MessageIds, string> = {
"dashboard.title": "Dashboard",
"user.profile.title": "Profile"
};
При таком подходе невозможно добавить обращение к несуществующему ключу без ошибки компиляции.
Линтер обеспечивает контроль использования сообщений:
Основные правила:
no-missing-message-idenforce-default-messageno-idПример конфигурации:
{
"plugins": ["formatjs"],
"rules": {
"formatjs/enforce-default-message": "error",
"formatjs/no-missing-message-id": "error"
}
}
В production-системах отсутствующие переводы часто фиксируются как события телеметрии:
Пример структуры события:
{
"type": "missing_translation",
"id": "settings.notifications.title",
"locale": "ru",
"route": "/settings"
}
Агрегация таких данных позволяет выявлять:
Наиболее сложный класс ошибок связан с динамическими ключами:
const id = `validation.${field}.${rule}`;
intl.formatMessage({ id });
Такие конструкции не могут быть корректно извлечены
babel-plugin-formatjs, что приводит к:
Решения:
FormatJS extraction pipeline позволяет формировать единый каталог:
formatjs compile messages.json --out-file compiled.json
Результат используется как runtime-источник:
FormatJS использует ICU MessageFormat:
{
id: "cart.items",
defaultMessage: "There are {count, plural, one {# item} other {# items}}"
}
Ошибки отсутствующих переводов часто сопровождаются ошибками ICU:
При отсутствии перевода ICU-строка может не интерпретироваться, если fallback не предусмотрен.
Для предотвращения деградации UI при отсутствии переводов используются подходы:
defaultMessageidconst text = message ?? `[${id}]`;
Такой подход предотвращает «пустые» интерфейсы.
Переименование id является одной из основных причин
потери переводов. Для управления используется:
domain.entity.property)Полный контроль достигается при интеграции:
Так формируется замкнутый цикл контроля переводов, где отсутствие локализации становится воспроизводимым и отслеживаемым событием, а не скрытой ошибкой интерфейса.