FormatJS — набор библиотек для интернационализации JavaScript-приложений. В экосистеме Angular чаще всего используются:
react-intl — для React;intl-messageformat — ядро форматирования
сообщений;@formatjs/intl — полифилы ECMAScript Intl API;@formatjs/cli — извлечение и компиляция переводов;babel-plugin-formatjs — анализ и оптимизация
сообщений.В Angular FormatJS применяется не как готовый Angular-фреймворк, а как низкоуровневая инфраструктура интернационализации.
Основные задачи:
npm install intl-messageformat @formatjs/intl
Для поддержки старых браузеров:
npm install @formatjs/intl-pluralrules
npm install @formatjs/intl-numberformat
npm install @formatjs/intl-datetimeformat
CLI для извлечения переводов:
npm install --save-dev @formatjs/cli
FormatJS построен поверх ECMAScript Intl API.
Angular-приложение использует:
Intl.NumberFormatIntl.DateTimeFormatIntl.RelativeTimeFormatIntl.PluralRulesIntl.ListFormatПример форматирования числа:
const formatter = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
});
console.log(formatter.format(1500));
Результат:
1 500,00 ₽
Типичная структура:
src/
├── app/
├── i18n/
│ ├── en.json
│ ├── ru.json
│ └── kk.json
└── assets/
Пример ru.json:
{
"app.title": "Панель управления",
"menu.home": "Главная",
"menu.profile": "Профиль"
}
Пример en.json:
{
"app.title": "Dashboard",
"menu.home": "Home",
"menu.profile": "Profile"
}
import { Injectable } from '@angular/core';
import { IntlMessageFormat } from 'intl-messageformat';
@Injectable({
providedIn: 'root'
})
export class LocalizationService {
private locale = 'ru';
private messages: Record<string, string> = {};
setLocale(locale: string): void {
this.locale = locale;
}
loadMessages(messages: Record<string, string>): void {
this.messages = messages;
}
translate(
key: string,
values?: Record<string, unknown>
): string {
const message = this.messages[key];
if (!message) {
return key;
}
const formatter = new IntlMessageFormat(
message,
this.locale
);
return formatter.format(values) as string;
}
}
import { Component } from '@angular/core';
import { LocalizationService } from './localization.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(
public i18n: LocalizationService
) {}
}
<h1>
{{ i18n.translate('app.title') }}
</h1>
FormatJS использует ICU MessageFormat.
Поддерживаются:
{
"user.welcome": "Здравствуйте, {name}"
}
this.i18n.translate('user.welcome', {
name: 'Алексей'
});
Результат:
Здравствуйте, Алексей
Pluralization — одна из главных возможностей FormatJS.
{
"cart.items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
this.i18n.translate('cart.items', {
count: 5
});
Русский язык содержит несколько форм множественного числа.
{
"notifications":
"{count, plural, " +
"one {# уведомление} " +
"few {# уведомления} " +
"many {# уведомлений} " +
"other {# уведомления}}"
}
Примеры:
1 уведомление
2 уведомления
5 уведомлений
21 уведомление
Выбор сообщений по условию.
{
"user.gender":
"{gender, select, " +
"male {Он вошёл} " +
"female {Она вошла} " +
"other {Они вошли}}"
}
Использование:
this.i18n.translate('user.gender', {
gender: 'female'
});
ICU поддерживает сложную вложенность.
{
"complex":
"{gender, select, " +
"male {{count, plural, one {Он добавил # файл} other {Он добавил # файлов}}} " +
"female {{count, plural, one {Она добавила # файл} other {Она добавила # файлов}}} " +
"other {Добавлено # файлов}}"
}
const formatter = new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'full',
timeStyle: 'short'
});
console.log(formatter.format(new Date()));
formatDate(date: Date): string {
return new Intl.DateTimeFormat(this.locale, {
dateStyle: 'long'
}).format(date);
}
Использование:
<p>{{ i18n.formatDate(today) }}</p>
formatCurrency(
value: number,
currency: string
): string {
return new Intl.NumberFormat(this.locale, {
style: 'currency',
currency
}).format(value);
}
new Intl.NumberFormat('ru-RU', {
style: 'percent'
}).format(0.25);
Результат:
25 %
Позволяет отображать:
const formatter = new Intl.RelativeTimeFormat('ru', {
numeric: 'auto'
});
formatter.format(-1, 'day');
Результат:
вчера
Angular-приложения часто загружают переводы лениво.
async loadLocale(locale: string) {
const messages = await import(
`../. ./i18n/${locale}.json`
);
this.locale = locale;
this.messages = messages.default;
}
switchLocale(locale: string) {
this.i18n.loadLocale(locale);
}
<button (click)="switchLocale('ru')">
RU
</button>
<button (click)="switchLocale('en')">
EN
</button>
import { Pipe, PipeTransform } fr om '@angular/core';
import { LocalizationService } fr om './localization.service';
@Pipe({
name: 't',
pure: false
})
export class TranslatePipe
implements PipeTransform {
constructor(
private i18n: LocalizationService
) {}
transform(
key: string,
values?: Record<string, unknown>
): string {
return this.i18n.translate(key, values);
}
}
<h1>{{ 'app.title' | t }}</h1>
С параметрами:
<p>
{{
'user.welcome'
| t:{ name: 'Иван' }
}}
</p>
Pipe должен быть pure: false, иначе Angular не обновит
UI после смены языка.
Недостаток:
Оптимизация:
Создание IntlMessageFormat — дорогая операция.
private cache = new Map<
string,
IntlMessageFormat
>();
translate(
key: string,
values?: Record<string, unknown>
): string {
const cacheKey =
`${this.locale}:${key}`;
let formatter =
this.cache.get(cacheKey);
if (!formatter) {
formatter =
new IntlMessageFormat(
this.messages[key],
this.locale
);
this.cache.set(
cacheKey,
formatter
);
}
return formatter.format(values) as string;
}
Большие приложения разделяют переводы по feature-модулям.
Структура:
i18n/
├── common/
├── dashboard/
├── auth/
└── admin/
{
"dashboard.title": "Статистика",
"dashboard.users": "Пользователи"
}
{
"auth.login": "Вход",
"auth.logout": "Выход"
}
Полезно использовать namespace.
Пример:
auth.login
auth.logout
dashboard.title
Это предотвращает конфликты ключей.
FormatJS CLI умеет извлекать сообщения из кода.
const messages = {
title: {
id: 'app.title',
defaultMessage: 'Dashboard'
}
};
formatjs extract "src/**/*.{ts,html}" \
--out-file lang/en.json
formatjs compile lang/en.json \
--out-file dist/en.json
Без компиляции ICU-парсер работает runtime.
С предкомпиляцией:
FormatJS полностью совместим с SSR.
Особенности:
const locale =
request.headers['accept-language'];
Критически важно:
Иначе Angular hydration завершится ошибкой.
Некоторые среды не поддерживают:
RelativeTimeFormat;ListFormat;DisplayNames.import '@formatjs/intl-pluralrules/polyfill';
import '@formatjs/intl-relativetimeformat/polyfill';
Для некоторых полифилов нужны locale-data.
import '@formatjs/intl-relativetimeformat/locale-data/ru';
import '@formatjs/intl-relativetimeformat/locale-data/en';
Пример:
/ru/dashboard
/en/dashboard
/kk/dashboard
const locale =
this.route.snapshot.paramMap.get('locale');
Для multilingual-приложений важны:
hreflang;this.title.setTitle(
this.i18n.translate('meta.home.title')
);
Типичные проблемы:
if (!message) {
console.warn(`Missing: ${key}`);
}
Ошибка:
EXPECT_ARGUMENT_CLOSING_BRACE
Причина:
"{count, plural, one {item}"
Пропущена закрывающая фигурная скобка.
FormatJS не экранирует HTML автоматически.
Опасно:
{
"danger": "<script>alert(1)</script>"
}
Нельзя вставлять переводы через:
[innerHTML]
без sanitization.
export type TranslationKey =
| 'app.title'
| 'menu.home'
| 'menu.profile';
translate(
key: TranslationKey
): string
export enum TranslationKeys {
TITLE = 'app.title',
HOME = 'menu.home'
}
describe('LocalizationService', () => {
it('should translate message', () => {
service.loadMessages({
hello: 'Привет'
});
expect(
service.translate('hello')
).toBe('Привет');
});
});
service.loadMessages({
items:
'{count, plural, one {# item} other {# items}}'
});
expect(
service.translate('items', {
count: 5
})
).toBe('5 items');
Проверяются:
Для арабского и иврита:
<html dir="rtl">
Angular может динамически менять направление:
document.documentElement.dir = 'rtl';
Форматирование списков.
new Intl.ListFormat('ru', {
style: 'long',
type: 'conjunction'
}).format([
'Angular',
'React',
'Vue'
]);
Результат:
Angular, React и Vue
Локализованные имена языков и стран.
const formatter =
new Intl.DisplayNames(['ru'], {
type: 'language'
});
formatter.of('en');
Результат:
английский
Разбиение текста на сегменты.
const segmenter =
new Intl.Segmenter('ru', {
granularity: 'word'
});
Основные затраты:
Снижает runtime overhead.
Повторное использование formatter instances.
Загрузка переводов по модулям.
Translation bundles удобно хранить на CDN.
| Возможность | Angular i18n | FormatJS |
|---|---|---|
| Runtime switching | Нет | Да |
| ICU syntax | Ограничено | Полноценная |
| Lazy translations | Ограничено | Да |
| Dynamic locale | Нет | Да |
| SSR | Да | Да |
| Pluralization | Да | Да |
| Runtime API | Слабая | Гибкая |
Наиболее подходящие сценарии: