FormatJS — набор библиотек для интернационализации JavaScript-приложений. Основная задача — локализация текста, форматирование дат, чисел, валют, относительного времени и сообщений ICU MessageFormat без привязки к React, Vue или другим фреймворкам.
В контексте Vanilla JavaScript чаще всего используются:
intl-messageformat@formatjs/intl@formatjs/intl-relativetimeformat@formatjs/intl-numberformat@formatjs/intl-datetimeformatFormatJS опирается на стандартный API Intl, встроенный в
современные браузеры.
Базовая установка:
npm install intl-messageformat
Либо подключение через CDN:
<script src="https://unpkg.com/intl-messageformat/dist/umd/intl-messageformat.min.js"></script>
При ручной локализации появляются типичные сложности:
Пример ручного подхода:
function getMessage(count) {
if (count === 1) {
return `${count} file`;
}
return `${count} files`;
}
Такой код быстро становится неуправляемым при поддержке нескольких языков.
FormatJS решает проблему через ICU MessageFormat.
ICU MessageFormat — стандарт описания локализуемых строк.
Простейший пример:
const message = 'Hello, {name}!';
Подстановка значений:
import {IntlMessageFormat} from 'intl-messageformat';
const msg = new IntlMessageFormat(
'Hello, {name}!',
'en'
);
console.log(
msg.format({
name: 'John'
})
);
Результат:
Hello, John!
Обычно переводы разделяются по языкам.
const messages = {
en: {
greeting: 'Hello, {name}!'
},
ru: {
greeting: 'Привет, {name}!'
}
};
const locale = navigator.language;
Иногда локаль нормализуется:
const locale = navigator.language.split('-')[0];
import {IntlMessageFormat} from 'intl-messageformat';
function translate(locale, key, values = {}) {
const message = messages[locale][key];
const formatter = new IntlMessageFormat(
message,
locale
);
return formatter.format(values);
}
Использование:
translate('ru', 'greeting', {
name: 'Алексей'
});
FormatJS активно использует стандартный API:
const formatter = new Intl.NumberFormat('ru-RU');
console.log(
formatter.format(1234567)
);
Результат:
1 234 567
const formatter = new Intl.NumberFormat(
'ru-RU',
{
style: 'currency',
currency: 'RUB'
}
);
console.log(
formatter.format(1500)
);
Результат:
1 500,00 ₽
const currencies = [
['en-US', 'USD'],
['de-DE', 'EUR'],
['ja-JP', 'JPY']
];
currencies.forEach(([locale, currency]) => {
const formatter = new Intl.NumberFormat(
locale,
{
style: 'currency',
currency
}
);
console.log(
formatter.format(1000)
);
});
const formatter = new Intl.DateTimeFormat(
'ru-RU'
);
console.log(
formatter.format(new Date())
);
const formatter = new Intl.DateTimeFormat(
'ru-RU',
{
year: 'numeric',
month: 'long',
day: 'numeric'
}
);
Пример результата:
15 января 2026 г.
const formatter = new Intl.DateTimeFormat(
'en-US',
{
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
);
Для отображения относительного времени используется
Intl.RelativeTimeFormat.
npm install @formatjs/intl-relativetimeformat
const rtf = new Intl.RelativeTimeFormat(
'ru',
{
numeric: 'auto'
}
);
console.log(
rtf.format(-1, 'day')
);
Результат:
вчера
console.log(rtf.format(-5, 'minute'));
console.log(rtf.format(2, 'hour'));
console.log(rtf.format(3, 'day'));
Разные языки имеют разные правила множественного числа.
Английский:
1 file
2 files
Русский:
1 файл
2 файла
5 файлов
const message = `
{count, plural,
one {# файл}
few {# файла}
many {# файлов}
other {# файла}
}
`;
const msg = new IntlMessageFormat(
message,
'ru'
);
console.log(
msg.format({
count: 1
})
);
console.log(
msg.format({
count: 3
})
);
console.log(
msg.format({
count: 10
})
);
select используется для выбора текста по условию.
const message = `
{gender, select,
male {Он}
female {Она}
other {Они}
} вошёл в систему
`;
const msg = new IntlMessageFormat(
message,
'ru'
);
console.log(
msg.format({
gender: 'male'
})
);
ICU позволяет комбинировать plural и sel ect.
const message = `
{gender, select,
male {
{count, plural,
one {Он загрузил # файл}
few {Он загрузил # файла}
many {Он загрузил # файлов}
other {Он загрузил # файла}
}
}
female {
{count, plural,
one {Она загрузила # файл}
few {Она загрузила # файла}
many {Она загрузила # файлов}
other {Она загрузила # файла}
}
}
other {
Загружено файлов: {count}
}
}
`;
FormatJS не предназначен для прямой генерации HTML.
Небезопасный пример:
element.innerHTML = translate(
'ru',
'welcome',
{
name: userInput
}
);
Если userInput содержит HTML, возможна XSS-атака.
element.textContent = translate(
'ru',
'welcome',
{
name: userInput
}
);
Создание экземпляров IntlMessageFormat — дорогостоящая
операция.
function t(locale, key, values) {
const formatter = new IntlMessageFormat(
messages[locale][key],
locale
);
return formatter.format(values);
}
const cache = new Map();
function getFormatter(locale, key) {
const cacheKey = `${locale}:${key}`;
if (!cache.has(cacheKey)) {
cache.set(
cacheKey,
new IntlMessageFormat(
messages[locale][key],
locale
)
);
}
return cache.get(cacheKey);
}
function t(locale, key, values) {
return getFormatter(
locale,
key
).format(values);
}
Крупные приложения редко загружают все языки сразу.
async function loadLocale(locale) {
const module = await import(
`./locales/${locale}.js`
);
return module.default;
}
let currentMessages = {};
async function setLocale(locale) {
currentMessages = await loadLocale(locale);
}
const formatter = new Intl.ListFormat(
'ru',
{
style: 'long',
type: 'conjunction'
}
);
console.log(
formatter.format([
'JavaScript',
'TypeScript',
'Python'
])
);
Результат:
JavaScript, TypeScript и Python
const formatter = new Intl.NumberFormat(
'en',
{
style: 'currency',
currency: 'USD'
}
);
console.log(
formatter.formatRange(10, 20)
);
const formatter = new Intl.DateTimeFormat(
'en-US',
{
timeZone: 'Asia/Almaty',
timeStyle: 'full'
}
);
Некоторые возможности Intl отсутствуют в старых
браузерах.
Для этого используются polyfill-пакеты FormatJS.
npm install @formatjs/intl-pluralrules
Подключение:
import '@formatjs/intl-pluralrules/polyfill';
if (!Intl.RelativeTimeFormat) {
await import(
'@formatjs/intl-relativetimeformat/polyfill'
);
}
{
greeting: 'Hello',
logout: 'Logout'
}
{
auth: {
login: 'Login',
logout: 'Logout'
}
}
{
common: {
save: 'Save'
},
profile: {
edit: 'Edit profile'
}
}
const message = `
{name} has {count} new messages
`;
msg.format({
name: 'John',
count: 5
});
ICU поддерживает встроенное форматирование.
const message = `
Balance: {value, number}
`;
const message = `
Price: {price, number, ::currency/USD}
`;
const message = `
Completed: {percent, number, percent}
`;
const message = `
Today: {today, date, long}
`;
const message = `
Current time: {now, time, short}
`;
В крупных проектах сообщения компилируются заранее.
npm install babel-plugin-formatjs
{
"plugins": [
[
"formatjs",
{
"idInterpolationPattern": "[sha512:contenthash:base64:6]"
}
]
]
}
FormatJS поддерживает автоматический extraction сообщений.
formatjs extract "src/**/*.js"
formatjs compile-folder locales compiled
Не только текст интерфейса требует перевода.
input.placeholder = t(
'ru',
'search_placeholder'
);
button.title = t(
'ru',
'save_button_title'
);
const formatter = new Intl.NumberFormat(
'ru',
{
style: 'unit',
unit: 'kilometer'
}
);
console.log(
formatter.format(10)
);
const formatter = new Intl.NumberFormat(
'en',
{
notation: 'compact'
}
);
console.log(
formatter.format(1500000)
);
Результат:
1.5M
const messages = {
ru: {
required: 'Поле обязательно',
invalid_email: 'Некорректный email'
}
};
throw new Error(
t('ru', 'invalid_email')
);
async function changeLanguage(locale) {
currentLocale = locale;
await setLocale(locale);
render();
}
<html lang="ru">
Обновление:
document.documentElement.lang = locale;
Если перевод отсутствует:
function t(locale, key, values) {
const message =
messages[locale]?.[key]
|| messages.en[key];
const formatter = new IntlMessageFormat(
message,
locale
);
return formatter.format(values);
}
function t(locale, key, values) {
const message = messages[locale]?.[key];
if (!message) {
console.warn(
`Missing translation: ${key}`
);
return key;
}
return new IntlMessageFormat(
message,
locale
).format(values);
}
Наиболее затратные операции:
Основные методы оптимизации:
Плохой пример:
button.textContent = 'Save';
Правильный вариант:
button.textContent = t(
currentLocale,
'save'
);
Ошибка:
'Hello ' + userName
Правильный вариант:
'Hello, {name}'
Ошибка:
`${count} items`
Правильный вариант:
{count, plural,
one {# item}
other {# items}
}
export default {
en: {
hello: 'Hello'
},
ru: {
hello: 'Привет'
}
};
import {IntlMessageFormat} fr om 'intl-messageformat';
import messages fr om './translations.js';
const cache = new Map();
let locale = 'en';
export function setLocale(newLocale) {
locale = newLocale;
}
function getFormatter(key) {
const cacheKey = `${locale}:${key}`;
if (!cache.has(cacheKey)) {
cache.set(
cacheKey,
new IntlMessageFormat(
messages[locale][key],
locale
)
);
}
return cache.get(cacheKey);
}
export function t(key, values = {}) {
return getFormatter(key)
.format(values);
}
import {t} fr om './i18n.js';
title.textContent = t('hello');
HTML:
<h1 data-i18n="title"></h1>
<button data-i18n="save"></button>
function renderTranslations() {
const elements = document.querySelectorAll(
'[data-i18n]'
);
elements.forEach(element => {
const key = element.dataset.i18n;
element.textContent = t(key);
});
}
function createNotification(count) {
const text = t(
'notifications',
{count}
);
const div = document.createElement('div');
div.textContent = text;
return div;
}
Для арабского и иврита требуется изменение направления текста.
document.documentElement.dir = 'rtl';
Для обычных языков:
document.documentElement.dir = 'ltr';
Современный API для работы с локалями.
const locale = new Intl.Locale('ru-RU');
console.log(locale.language);
console.log(locale.region);
Разделение текста по словам и предложениям.
const segmenter = new Intl.Segmenter(
'ru',
{
granularity: 'word'
}
);
const formatter = new Intl.NumberFormat(
'ru',
{
style: 'percent'
}
);
console.log(
formatter.format(0.25)
);
const formatter = new Intl.NumberFormat(
'en',
{
notation: 'scientific'
}
);
console.log(
formatter.format(123456)
);
const formatter = new Intl.NumberFormat(
'en',
{
notation: 'engineering'
}
);
const supportedLocales = ['en', 'ru'];
const locale = Intl.NumberFormat.supportedLocalesOf(
navigator.languages
)[0] || 'en';
Некоторые приложения переводят только интерфейс:
{
ru: {
save: 'Сохранить'
}
}
Другие локализуют:
FormatJS может использоваться не только в браузере.
import {IntlMessageFormat} from 'intl-messageformat';
const msg = new IntlMessageFormat(
'Hello, {name}',
'en'
);
console.log(
msg.format({
name: 'Admin'
})
);
Обычно процесс включает:
export default {
en: {
title: 'Dashboard',
files: `
{count, plural,
one {# file}
other {# files}
}
`
},
ru: {
title: 'Панель управления',
files: `
{count, plural,
one {# файл}
few {# файла}
many {# файлов}
other {# файла}
}
`
}
};
import {IntlMessageFormat} from 'intl-messageformat';
import translations from './translations.js';
let locale = 'ru';
const cache = new Map();
function t(key, values = {}) {
const cacheKey = `${locale}:${key}`;
if (!cache.has(cacheKey)) {
cache.set(
cacheKey,
new IntlMessageFormat(
translations[locale][key],
locale
)
);
}
return cache.get(cacheKey)
.format(values);
}
document.querySelector('#title')
.textContent = t('title');
document.querySelector('#files')
.textContent = t('files', {
count: 21
});