Стандартный набор методов Intl покрывает большинство
задач локализации: форматирование чисел, дат, валют, списков,
относительного времени и единиц измерения. Однако в крупных приложениях
прямое использование Intl.NumberFormat,
Intl.DateTimeFormat или других объектов быстро приводит к
дублированию кода, сложностям поддержки и несогласованности
интерфейса.
Пользовательский форматтер — это абстракция над
Intl API, скрывающая детали локали, настроек и
повторяющейся логики.
Базовая проблема прямого использования Intl:
const price = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(1500)
const shortDate = new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'short'
}).format(new Date())
При увеличении количества экранов подобный код начинает повторяться десятки раз.
Более масштабируемый подход:
const formatters = {
money(value) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(value)
},
shortDate(value) {
return new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'short'
}).format(value)
}
}
Использование:
formatters.money(4500)
formatters.shortDate(new Date())
Создание экземпляров Intl является сравнительно дорогой
операцией. Особенно это заметно при рендеринге таблиц, списков и больших
наборов данных.
Неэффективный вариант:
function formatPrice(value) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(value)
}
При каждом вызове создаётся новый объект форматтера.
Правильный подход — кеширование:
const moneyFormatter = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
})
function formatPrice(value) {
return moneyFormatter.format(value)
}
При работе с несколькими локалями удобнее создавать фабрику форматтеров.
Пример:
function createFormatters(locale) {
const currency = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
})
const date = new Intl.DateTimeFormat(locale, {
dateStyle: 'long'
})
return {
currency(value) {
return currency.format(value)
},
date(value) {
return date.format(value)
}
}
}
Использование:
const ru = createFormatters('ru-RU')
const en = createFormatters('en-US')
ru.currency(1200)
en.currency(1200)
В больших приложениях создают централизованный реестр.
Пример универсального кеша:
const formatterCache = new Map()
function getNumberFormatter(locale, options) {
const key = JSON.stringify([locale, options])
if (!formatterCache.has(key)) {
formatterCache.set(
key,
new Intl.NumberFormat(locale, options)
)
}
return formatterCache.get(key)
}
Использование:
const formatter = getNumberFormatter('ru-RU', {
style: 'currency',
currency: 'RUB'
})
formatter.format(5000)
Форматирование валют почти всегда требует дополнительных правил.
Базовый вариант:
function createCurrencyFormatter(locale, currency) {
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency
})
return value => formatter.format(value)
}
Использование:
const rub = createCurrencyFormatter('ru-RU', 'RUB')
const usd = createCurrencyFormatter('en-US', 'USD')
rub(1000)
usd(1000)
function createMoneyFormatter(locale, currency) {
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
return value => formatter.format(value)
}
Иногда требуется скрывать копейки или центы.
Пример:
function formatMoney(value, locale, currency) {
const hasFraction = value % 1 !== 0
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: hasFraction ? 2 : 0,
maximumFractionDigits: 2
}).format(value)
}
function createPercentFormatter(locale) {
const formatter = new Intl.NumberFormat(locale, {
style: 'percent',
maximumFractionDigits: 1
})
return value => formatter.format(value)
}
Использование:
const percent = createPercentFormatter('ru-RU')
percent(0.25)
Intl.NumberFormat поддерживает сокращённую запись.
const compact = new Intl.NumberFormat('ru-RU', {
notation: 'compact'
})
compact.format(1500000)
Пользовательская обёртка:
function createCompactFormatter(locale) {
const formatter = new Intl.NumberFormat(locale, {
notation: 'compact',
maximumFractionDigits: 1
})
return value => formatter.format(value)
}
Базовая фабрика:
function createDateFormatter(locale, options = {}) {
const formatter = new Intl.DateTimeFormat(locale, options)
return value => formatter.format(value)
}
Использование:
const longDate = createDateFormatter('ru-RU', {
dateStyle: 'full'
})
longDate(new Date())
Практика крупных проектов — создание единого словаря форматов.
const dateFormats = {
short: {
dateStyle: 'short'
},
medium: {
dateStyle: 'medium'
},
full: {
dateStyle: 'full'
}
}
Создание форматтера:
function createNamedDateFormatter(locale, preset) {
return new Intl.DateTimeFormat(
locale,
dateFormats[preset]
)
}
function createTimeFormatter(locale) {
const formatter = new Intl.DateTimeFormat(locale, {
timeStyle: 'short'
})
return value => formatter.format(value)
}
const formatter = new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'medium',
timeStyle: 'short'
})
formatter.format(new Date())
Intl.RelativeTimeFormat используется для отображения
строк вроде:
Создание форматтера:
function createRelativeFormatter(locale) {
const formatter = new Intl.RelativeTimeFormat(locale, {
numeric: 'auto'
})
return {
minutes(value) {
return formatter.format(value, 'minute')
},
days(value) {
return formatter.format(value, 'day')
}
}
}
Использование:
const relative = createRelativeFormatter('ru-RU')
relative.minutes(-5)
relative.days(2)
function formatRelativeDate(date, locale = 'ru-RU') {
const diff = date - Date.now()
const minutes = Math.round(diff / 60000)
const hours = Math.round(diff / 3600000)
const days = Math.round(diff / 86400000)
const formatter = new Intl.RelativeTimeFormat(locale, {
numeric: 'auto'
})
if (Math.abs(minutes) < 60) {
return formatter.format(minutes, 'minute')
}
if (Math.abs(hours) < 24) {
return formatter.format(hours, 'hour')
}
return formatter.format(days, 'day')
}
Intl.ListFormat позволяет корректно объединять элементы
списка.
const formatter = new Intl.ListFormat('ru-RU', {
style: 'long',
type: 'conjunction'
})
formatter.format(['JavaScript', 'TypeScript', 'Rust'])
Результат:
JavaScript, TypeScript и Rust
Пользовательская фабрика:
function createListFormatter(locale, options) {
const formatter = new Intl.ListFormat(locale, options)
return items => formatter.format(items)
}
function createUnitFormatter(locale, unit) {
const formatter = new Intl.NumberFormat(locale, {
style: 'unit',
unit
})
return value => formatter.format(value)
}
Использование:
const kilometers = createUnitFormatter(
'ru-RU',
'kilometer'
)
kilometers(15)
Intl.DateTimeFormat поддерживает диапазоны дат.
const formatter = new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'medium'
})
formatter.formatRange(
new Date('2025-01-01'),
new Date('2025-01-10')
)
function createRangeFormatter(locale, options) {
const formatter = new Intl.DateTimeFormat(
locale,
options
)
return (start, end) =>
formatter.formatRange(start, end)
}
Метод formatToParts() позволяет получить структуру
форматированной строки.
Пример:
const formatter = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
})
formatter.formatToParts(1500)
Результат:
[
{ type: 'integer', value: '1' },
{ type: 'group', value: ' ' },
{ type: 'integer', value: '500' },
{ type: 'literal', value: ',' },
{ type: 'fraction', value: '00' },
{ type: 'literal', value: ' ' },
{ type: 'currency', value: '₽' }
]
function formatMoneyParts(value) {
const formatter = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
})
const parts = formatter.formatToParts(value)
return parts
.map(part => {
if (part.type === 'currency') {
return `<strong>${part.value}</strong>`
}
return part.value
})
.join('')
}
Пример разделения целой и дробной части:
function formatPrice(value) {
const formatter = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
})
const parts = formatter.formatToParts(value)
let integer = ''
let fraction = ''
let currency = ''
for (const part of parts) {
if (part.type === 'integer' || part.type === 'group') {
integer += part.value
}
if (part.type === 'fraction') {
fraction = part.value
}
if (part.type === 'currency') {
currency = part.value
}
}
return `
<span class="price-main">${integer}</span>
<span class="price-fraction">${fraction}</span>
<span class="price-currency">${currency}</span>
`
}
Форматтеры удобно комбинировать.
function createFormatService(locale) {
const money = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
})
const date = new Intl.DateTimeFormat(locale, {
dateStyle: 'medium'
})
const relative = new Intl.RelativeTimeFormat(locale)
return {
money: value => money.format(value),
date: value => date.format(value),
relativeDays: value =>
relative.format(value, 'day')
}
}
В больших приложениях форматтеры часто внедряются через DI-контейнер.
Пример:
class FormatterService {
constructor(locale) {
this.locale = locale
this.moneyFormatter =
new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
})
}
money(value) {
return this.moneyFormatter.format(value)
}
}
Проблема:
const formatter = new Intl.NumberFormat(currentLocale)
После изменения локали форматтер останется прежним.
Правильный подход:
class LocaleManager {
constructor(locale) {
this.setLocale(locale)
}
setLocale(locale) {
this.locale = locale
this.numberFormatter =
new Intl.NumberFormat(locale)
}
formatNumber(value) {
return this.numberFormatter.format(value)
}
}
class FormatterRegistry {
constructor(locale) {
this.locale = locale
this.cache = new Map()
}
getCurrencyFormatter(currency) {
const key = `currency:${currency}`
if (!this.cache.has(key)) {
this.cache.set(
key,
new Intl.NumberFormat(this.locale, {
style: 'currency',
currency
})
)
}
return this.cache.get(key)
}
}
class I18nAdapter {
constructor(locale) {
this.locale = locale
}
formatPrice(value) {
return new Intl.NumberFormat(this.locale, {
style: 'currency',
currency: 'USD'
}).format(value)
}
formatDate(value) {
return new Intl.DateTimeFormat(this.locale, {
dateStyle: 'long'
}).format(value)
}
}
type CurrencyFormatter = (value: number) => string
function createCurrencyFormatter(
locale: string,
currency: string
): CurrencyFormatter {
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency
})
return value => formatter.format(value)
}
interface Formatters {
money(value: number): string
date(value: Date): string
percent(value: number): string
}
function createFormatter<T>(
factory: () => T
): () => T {
let instance: T | null = null
return () => {
if (!instance) {
instance = factory()
}
return instance
}
}
Использование:
const getFormatter = createFormatter(
() =>
new Intl.NumberFormat('ru-RU')
)
getFormatter().format(1000)
Некорректная локаль может вызвать проблемы в старых окружениях.
Проверка поддержки:
function isLocaleSupported(locale) {
return Intl.NumberFormat.supportedLocalesOf([
locale
]).length > 0
}
function createSafeFormatter(locale) {
const safeLocale =
isLocaleSupported(locale)
? locale
: 'en-US'
return new Intl.NumberFormat(safeLocale)
}
Типичная архитектура:
const format = {
money: createCurrencyFormatter(
'ru-RU',
'RUB'
),
percent: createPercentFormatter(
'ru-RU'
),
date: createDateFormatter('ru-RU', {
dateStyle: 'medium'
})
}
Использование:
format.money(5000)
format.percent(0.15)
format.date(new Date())
Все параметры локализации должны находиться централизованно.
Создание объектов Intl должно происходить минимальное
количество раз.
Компоненты интерфейса не должны содержать настройки
Intl.
Все форматтеры обязаны корректно работать при смене языка и региона.
Добавление новых типов форматирования не должно требовать переписывания существующей архитектуры.