Хуки и композиция

Intl API в JavaScript предоставляет набор специализированных объектов для локализации: форматирование чисел, дат, списков, сравнений строк, отображения языков и регионов, работы с множественными формами и сегментацией текста. Основная сложность при работе с этими инструментами возникает не в их использовании по отдельности, а в организации связного, повторно используемого слоя форматирования, который можно масштабировать, комбинировать и переиспользовать в приложении.

Композиция в контексте Intl API означает построение системы небольших функций-обёрток, каждая из которых инкапсулирует один аспект локализации, а затем объединение этих функций в единый поток преобразования данных.


Базовые принципы композиции в локализации

Intl API изначально ориентирован на создание экземпляров форматтеров:

  • Intl.NumberFormat
  • Intl.DateTimeFormat
  • Intl.Collator
  • Intl.ListFormat
  • Intl.RelativeTimeFormat
  • Intl.DisplayNames
  • Intl.Segmenter

Каждый из этих объектов выполняет строго ограниченную задачу. Именно это делает их удобными для композиции: они не смешивают ответственность.

Композиция строится вокруг двух идей:

1. Функция как единица локализации Каждый форматтер превращается в функцию:

const formatNumber = new Intl.NumberFormat("ru-RU").format;

const formatDate = new Intl.DateTimeFormat("ru-RU", {
  year: "numeric",
  month: "long",
  day: "numeric",
}).format;

Теперь форматирование становится не объектным вызовом, а функцией, которую можно передавать, хранить и комбинировать.

2. Разделение данных и представления Данные остаются неизменными, а форматирование накладывается слоями:

const price = 1999.99;
const formatted = formatNumber(price);

Функции-обёртки как строительные блоки

Оборачивание Intl API в функции позволяет создавать предсказуемые интерфейсы.

Фабрики форматтеров

Фабрика создаёт специализированную функцию с заранее заданной конфигурацией:

function createCurrencyFormatter(locale, currency) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  });

  return (value) => formatter.format(value);
}

const formatUSD = createCurrencyFormatter("en-US", "USD");
const formatEUR = createCurrencyFormatter("de-DE", "EUR");

Такая структура позволяет изолировать конфигурацию и повторно использовать форматтеры без пересоздания.


Композиция нескольких форматтеров

Композиция проявляется, когда результат одной функции становится входом другой.

Пример цепочки форматирования

const formatNumber = new Intl.NumberFormat("ru-RU").format;
const formatPercent = new Intl.NumberFormat("ru-RU", {
  style: "percent",
}).format;

const toFixedTwo = (n) => Number(n.toFixed(2));
const pipe =
  (...fns) =>
  (value) =>
    fns.reduce((acc, fn) => fn(acc), value);

const formatPipeline = pipe(toFixedTwo, formatPercent);

formatPipeline(0.1234);

Здесь формируется цепочка преобразований:

  1. нормализация числа
  2. приведение к фиксированной точности
  3. локализованное форматирование

Частичное применение Intl-форматтеров

Intl API естественным образом поддерживает паттерн частичного применения через конфигурацию конструктора.

Пример замыкания на конфигурации

const ruDateFormatter = (options) =>
  new Intl.DateTimeFormat("ru-RU", options).format;

const formatShortDate = ruDateFormatter({
  day: "2-digit",
  month: "2-digit",
  year: "numeric",
});

const formatLongDate = ruDateFormatter({
  day: "numeric",
  month: "long",
  year: "numeric",
});

Здесь конфигурация отделена от выполнения, что позволяет строить библиотечные уровни абстракции.


Композиция с учётом локали

Одна из ключевых проблем — динамическая смена языка интерфейса.

Инкапсуляция локали

function createI18n(locale) {
  const number = new Intl.NumberFormat(locale).format;
  const date = new Intl.DateTimeFormat(locale).format;
  const list = new Intl.ListFormat(locale).format;

  return {
    number,
    date,
    list,
  };
}

const i18nRU = createI18n("ru-RU");
const i18nEN = createI18n("en-US");

Такой подход позволяет заменить всю локализацию через одну точку входа.


Композиция форматирования списков

Intl.ListFormat особенно полезен в композиционных цепочках, когда требуется объединить массив данных в человекочитаемую форму.

const formatList = new Intl.ListFormat("ru-RU", {
  style: "long",
  type: "conjunction",
}).format;

const users = ["Анна", "Иван", "Мария"];

formatList(users);

Композиция усиливается, когда список предварительно трансформируется:

const upper = (arr) => arr.map((x) => x.toUpperCase());
const format = (arr) => formatList(arr);

const pipeline = pipe(upper, format);

pipeline(users);

Колляция и композиция сортировки

Intl.Collator используется для корректной сортировки строк в зависимости от языка.

const collator = new Intl.Collator("ru-RU").compare;

const sorted = ["яблоко", "Ёж", "арбуз"].sort(collator);

Композиция возникает при объединении фильтрации и сортировки:

const normalize = (s) => s.toLowerCase();

const prepare = (arr) =>
  arr.map(normalize).sort(collator);

prepare(["ЯБЛОКО", "ёж", "Арбуз"]);

Композиция относительного времени

Intl.RelativeTimeFormat позволяет выражать время относительно текущего момента.

const rtf = new Intl.RelativeTimeFormat("ru-RU", {
  numeric: "auto",
});

const formatRelativeDay = (days) => rtf.format(days, "day");

Композиция проявляется при расчёте разницы дат:

const diffInDays = (date) =>
  Math.round((date - new Date()) / (1000 * 60 * 60 * 24));

const formatFromNow = pipe(diffInDays, formatRelativeDay);

Сегментация текста как слой композиции

Intl.Segmenter используется для разбиения текста на символы, слова или предложения с учётом языка.

const segmenter = new Intl.Segmenter("ru", { granularity: "word" });

const segment = (text) =>
  Array.from(segmenter.segment(text), (s) => s.segment);

Композиция становится особенно полезной при последующей обработке:

const filterEmpty = (arr) => arr.filter((x) => x.trim());
const count = (arr) => arr.length;

const wordCount = pipe(segment, filterEmpty, count);

Функциональные конвейеры как основа архитектуры Intl

Композиция Intl API наиболее эффективно проявляется в виде конвейеров преобразования данных.

Универсальный pipe

const pipe =
  (...fns) =>
  (value) =>
    fns.reduce((acc, fn) => fn(acc), value);

Пример комплексного форматирования

const formatPrice = createCurrencyFormatter("ru-RU", "RUB");
const round = (n) => Math.round(n);

const processPrice = pipe(round, formatPrice);

processPrice(1999.55);

Здесь каждый слой независим:

  • округление
  • локализация
  • отображение

Инкапсуляция Intl как слой доменной модели

При построении сложных приложений Intl API часто выносится в отдельный слой доменной логики.

function createFormatterService(locale) {
  const number = new Intl.NumberFormat(locale).format;
  const date = new Intl.DateTimeFormat(locale).format;
  const percent = new Intl.NumberFormat(locale, {
    style: "percent",
  }).format;

  return {
    number,
    date,
    percent,
  };
}

Такой слой становится единственной точкой, через которую проходит вся локализация.


Композиция и неизменяемость конфигурации

Intl объекты являются неизменяемыми после создания. Это свойство напрямую усиливает композиционный подход:

  • отсутствуют побочные эффекты
  • форматтеры можно безопасно передавать между модулями
  • повторное использование не требует пересоздания конфигурации
const fmt = new Intl.NumberFormat("ru-RU");
const f1 = fmt.format;
const f2 = fmt.format;

Обе функции идентичны по поведению и безопасны для многократного использования.


Переиспользуемые композиционные паттерны

Локализационный адаптер

const withLocale =
  (locale) =>
  (formatter) =>
  (...args) =>
    formatter(locale)(...args);

Унификация форматтеров

const createLocalizedFormatters = (locale) => ({
  number: new Intl.NumberFormat(locale).format,
  date: new Intl.DateTimeFormat(locale).format,
  list: new Intl.ListFormat(locale).format,
});

Слоистая архитектура форматирования

Композиция Intl API естественным образом ложится на слоистую модель:

  1. Слой данных — сырые значения
  2. Слой трансформации — map, pipe, фильтры
  3. Слой локализации — Intl форматтеры
  4. Слой представления — UI или вывод

Каждый слой изолирован и может быть заменён без изменения остальных.