При интернационализации приложения недостаточно только подключить переводы и настроить переключение языка. Критически важной частью инфраструктуры становится контроль покрытия локалей — проверка того, что все сообщения переведены, не содержат ошибок и синхронизированы между языками.
В экосистеме FormatJS проверка покрытия локалей позволяет:
Без системной проверки локалей со временем возникают типичные проблемы:
Наиболее распространённая структура в проектах с FormatJS:
src/
├── locales/
│ ├── en.json
│ ├── ru.json
│ ├── de.json
│ └── fr.json
│
├── components/
└── pages/
Пример базовой локали:
{
"app.title": "Dashboard",
"menu.profile": "Profile",
"menu.logout": "Logout"
}
Русская локаль:
{
"app.title": "Панель управления",
"menu.profile": "Профиль",
"menu.logout": "Выход"
}
Обычно одна локаль считается эталонной. Чаще всего это:
en;en-US;Именно относительно неё выполняется анализ покрытия.
Самая базовая задача — обнаружение отсутствующих ключей.
Простейшая проверка:
import en fr om './locales/en.json';
import ru fr om './locales/ru.json';
const missingKeys = Object.keys(en).filter(
key => !(key in ru)
);
console.log(missingKeys);
Результат:
[
"menu.logout"
]
Подобная проверка помогает автоматически обнаруживать строки, которые забыли перевести.
Не менее важно находить сообщения, которых больше нет в исходной локали.
Пример:
const obsoleteKeys = Object.keys(ru).filter(
key => !(key in en)
);
console.log(obsoleteKeys);
Вывод:
[
"old.settings.label"
]
Лишние ключи создают несколько проблем:
На практике обе проверки объединяются.
Пример утилиты:
function validateLocale(base, target) {
const missing = [];
const obsolete = [];
for (const key of Object.keys(base)) {
if (!(key in target)) {
missing.push(key);
}
}
for (const key of Object.keys(target)) {
if (!(key in base)) {
obsolete.push(key);
}
}
return {
missing,
obsolete
};
}
Использование:
import en from './locales/en.json';
import ru from './locales/ru.json';
const result = validateLocale(en, ru);
console.log(result);
Результат:
{
"missing": ["menu.logout"],
"obsolete": ["old.settings.label"]
}
В крупных приложениях количество языков может измеряться десятками.
Автоматическая проверка всех файлов:
import fs from 'fs';
import path from 'path';
const localesDir = './locales';
const baseLocale = JSON.parse(
fs.readFileSync(
path.join(localesDir, 'en.json'),
'utf-8'
)
);
const localeFiles = fs.readdirSync(localesDir);
for (const file of localeFiles) {
if (file === 'en.json') {
continue;
}
const locale = JSON.parse(
fs.readFileSync(
path.join(localesDir, file),
'utf-8'
)
);
const missing = Object.keys(baseLocale)
.filter(key => !(key in locale));
if (missing.length > 0) {
console.log(`Missing in ${file}:`);
for (const key of missing) {
console.log(` - ${key}`);
}
}
}
Одна из важнейших задач — синхронизация ICU-шаблонов.
Базовая строка:
{
"cart.items": "{count} items"
}
Ошибка перевода:
{
"cart.items": "{items} товаров"
}
В результате приложение попытается передать параметр
count, которого нет в переводе.
Это может привести:
Проверку параметров можно реализовать через регулярные выражения.
Пример:
function extractVariables(message) {
const matches = message.match(/{(.*?)}/g) || [];
return matches.map(item =>
item.replace(/[{}]/g, '')
);
}
Использование:
extractVariables('{count} items');
Результат:
['count']
Полная проверка:
function validateVariables(base, target) {
const issues = [];
for (const key of Object.keys(base)) {
if (!(key in target)) {
continue;
}
const baseVars = extractVariables(base[key]);
const targetVars = extractVariables(target[key]);
const same =
JSON.stringify(baseVars.sort()) ===
JSON.stringify(targetVars.sort());
if (!same) {
issues.push({
key,
base: baseVars,
target: targetVars
});
}
}
return issues;
}
ICU plural-выражения особенно чувствительны к ошибкам.
Исходная строка:
{
"notifications":
"{count, plural, one {# notification} other {# notifications}}"
}
Некорректный перевод:
{
"notifications":
"{count, plural, one {# уведомление}}"
}
Отсутствие категории other нарушает работу
plural-механизма.
Для надёжной проверки рекомендуется использовать официальный парсер FormatJS.
Установка:
npm install @formatjs/icu-messageformat-parser
Проверка:
import { parse } from '@formatjs/icu-messageformat-parser';
function validateMessage(message) {
try {
parse(message);
return true;
} catch (error) {
return false;
}
}
Использование:
validateMessage(
'{count, plural, one {# item}}'
);
Типичные ошибки ICU:
{
"title": "{count items"
}
{
"title":
"{count, plural one {Item}}"
}
{
"gender":
"{gender, select, male {He}}"
}
Автоматическая валидация предотвращает подобные ошибки ещё до сборки приложения.
Иногда перевод существует формально, но содержит пустую строку.
Пример:
{
"menu.logout": ""
}
Проверка:
function findEmptyMessages(locale) {
return Object.entries(locale)
.filter(([_, value]) => {
return value.trim() === '';
})
.map(([key]) => key);
}
В некоторых проектах требуется искать сообщения, которые остались непереведёнными.
Пример:
{
"menu.profile": "Profile"
}
Для русской локали это может означать, что перевод забыли выполнить.
Проверка:
function findUntranslated(base, target) {
return Object.keys(base).filter(
key => base[key] === target[key]
);
}
Экосистема FormatJS содержит CLI для автоматизации проверки локалей.
Установка:
npm install --save-dev @formatjs/cli
Пример проверки:
formatjs extract "src/**/*.{js,jsx,ts,tsx}" \
--out-file lang/en.json
Команда извлекает все сообщения из исходного кода.
После извлечения сообщений можно проверять актуальность переводов.
Пример workflow:
formatjs extract \
"src/**/*.{ts,tsx}" \
--out-file temp/messages.json
Затем:
node scripts/check-locales.js
Контроль покрытия локалей особенно важен в непрерывной интеграции.
Пример GitHub Actions:
name: Locale Validation
on:
pull_request:
jobs:
locales:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run validate-locales
Хорошая практика — аварийно завершать pipeline при проблемах локализации.
Пример:
if (
missing.length > 0 ||
invalidMessages.length > 0
) {
process.exit(1);
}
Это предотвращает попадание дефектных переводов в production.
Иногда требуется вычислять процент заполнения локали.
Пример:
function calculateCoverage(base, locale) {
const total = Object.keys(base).length;
const translated = Object.keys(base)
.filter(key => {
return key in locale &&
locale[key].trim() !== '';
})
.length;
return (translated / total) * 100;
}
Использование:
const coverage = calculateCoverage(en, ru);
console.log(`${coverage.toFixed(2)}%`);
Часто результаты проверки экспортируются в отдельные отчёты.
Пример JSON-отчёта:
{
"locale": "ru",
"coverage": 94.5,
"missing": [
"settings.title"
],
"obsolete": [
"legacy.menu"
]
}
Для больших команд удобно генерировать визуальные отчёты.
Пример структуры:
<table>
<tr>
<th>Locale</th>
<th>Coverage</th>
</tr>
<tr>
<td>ru</td>
<td>98%</td>
</tr>
</table>
Некоторые приложения используют fallback-механизм:
<IntlProvider
locale="ru"
defaultLocale="en"
messages={messages}
/>
Важно понимать, что fallback скрывает проблемы перевода:
Поэтому fallback не заменяет полноценную проверку покрытия.
Крупные приложения часто группируют сообщения по namespace.
Пример:
{
"auth.login.title": "Login",
"auth.login.button": "Sign in",
"dashboard.header.title": "Dashboard"
}
Проверка namespace помогает:
Иногда разные ключи содержат одинаковые тексты.
Пример:
{
"button.save": "Save",
"form.submit": "Save"
}
Это не всегда ошибка, но может указывать:
Проверка:
function findDuplicates(locale) {
const map = new Map();
for (const [key, value] of Object.entries(locale)) {
if (!map.has(value)) {
map.set(value, []);
}
map.get(value).push(key);
}
return [...map.entries()]
.filter(([_, keys]) => keys.length > 1);
}
В некоторых интерфейсах критична длина текста.
Например:
Проверка:
function findLongTranslations(locale, lim it = 80) {
return Object.entries(locale)
.filter(([_, value]) => {
return value.length > lim it;
});
}
Для арабского и иврита дополнительно проверяются:
Пример RTL-локалей:
ar;he;fa.Проверка покрытия часто объединяется с:
Типичный pipeline:
Иногда удобно автоматически создавать шаблоны недостающих переводов.
Пример:
function generateMissing(base, locale) {
const result = {};
for (const key of Object.keys(base)) {
if (!(key in locale)) {
result[key] = base[key];
}
}
return result;
}
В development-режиме полезно отображать отсутствующие переводы визуально.
Пример:
function missingTranslation(id) {
return `@@${id}@@`;
}
Результат в интерфейсе:
@@menu.logout@@
Такой подход помогает быстро замечать проблемы локализации.
Некоторые проекты выполняют дополнительную валидацию прямо в браузере.
Например:
if (!messages[id]) {
console.warn(
`Missing translation: ${id}`
);
}
Однако runtime-проверки считаются вспомогательными. Основной контроль должен происходить до сборки.
При использовании TypeScript можно типизировать ключи переводов.
Пример:
type MessageKey =
keyof typeof messages;
Использование:
function t(id: MessageKey) {
return intl.formatMessage({ id });
}
Это позволяет обнаруживать ошибки ключей ещё на этапе компиляции.
Автоматическая генерация:
export type TranslationKey =
| 'menu.profile'
| 'menu.logout'
| 'settings.title';
Подобный подход существенно уменьшает вероятность ошибок.
В больших системах проверка локалей обычно включает:
Полноценная система контроля локализации становится отдельной частью инфраструктуры приложения и напрямую влияет на стабильность интернационализированного интерфейса.