Создание пользовательских форматтеров

В архитектуре Globalize форматирование строится вокруг данных CLDR и набора специализированных модулей (числа, даты, сообщения, валюты). Однако реальная разработка часто требует выходить за пределы стандартных форматов: объединять несколько значений, внедрять доменные правила отображения, создавать локализованные представления сложных структур.

Пользовательский форматтер в этом контексте представляет собой функцию или объект, который принимает исходные данные и возвращает строку, сформированную с учётом локали и правил форматирования Globalize.

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


Регистрация форматтеров через messageFormatter

Наиболее распространённый способ создания пользовательских форматтеров в Globalize основан на системе сообщений. Сообщение может включать параметры, плейсхолдеры и вложенные форматтеры.

Инициализация базового сообщения:

Globalize.loadMessages({
  ru: {
    orderSummary: "Заказ №{orderId}, сумма: {total}",
  }
});

Получение форматтера:

const formatOrder = Globalize("ru").messageFormatter("orderSummary");

formatOrder({
  orderId: 1523,
  total: "1200 ₽"
});

Внутри системы сообщений параметры обрабатываются через ICU-подобный синтаксис, что позволяет расширять форматирование без изменения кода.


Встраивание форматтеров в сообщения

Пользовательские форматтеры часто комбинируются с встроенными механизмами Globalize, такими как форматирование чисел и дат.

Пример с числом:

Globalize.loadMessages({
  ru: {
    price: "Цена: {price, number, currency}"
  }
});

Использование:

const formatPrice = Globalize("ru").messageFormatter("price");

formatPrice({ price: 1250 });

Здесь number, currency использует внутренний модуль форматирования чисел, но структура сообщения остаётся пользовательской. Это фактически первый уровень композиции форматтеров.


Создание функций-обёрток над форматированием

В более сложных сценариях создаются явные функции-форматтеры, которые комбинируют несколько вызовов Globalize.

const cldr = Globalize("ru");

const formatUserCard = (user) => {
  const name = user.name;
  const balance = cldr.formatNumber(user.balance, { style: "currency", currency: "RUB" });
  const lastLogin = cldr.formatDate(user.lastLogin, { datetime: "medium" });

  return `${name} | Баланс: ${balance} | Последний вход: ${lastLogin}`;
};

Такой подход используется, когда сообщение становится слишком сложным для ICU-шаблонов или требует условной логики.


Инкапсуляция форматтеров в фабрики

Для масштабируемых приложений форматтеры часто оформляются как фабрики, привязанные к локали.

const createUserFormatter = (locale) => {
  const globalize = Globalize(locale);

  return {
    formatBalance: (value) =>
      globalize.formatNumber(value, { style: "currency", currency: "RUB" }),

    formatDate: (date) =>
      globalize.formatDate(date, { datetime: "short" }),

    formatProfile: (user) =>
      `${user.name} — ${globalize.formatNumber(user.score)}`
  };
};

Такой слой позволяет:

  • фиксировать локаль один раз;
  • избегать повторной инициализации Globalize;
  • централизовать правила отображения.

Использование кастомных токенов в сообщениях

Globalize поддерживает расширенные ICU-форматы, где можно подключать форматтеры через именованные функции.

Globalize.loadMessages({
  ru: {
    distance: "{value, number, ::unit-meter}"
  }
});

При этом unit-meter может быть переопределён через кастомный форматтер-слой:

const cldr = Globalize("ru");

const formatDistance = (meters) => {
  if (meters >= 1000) {
    return cldr.formatNumber(meters / 1000) + " км";
  }
  return cldr.formatNumber(meters) + " м";
};

Далее такой форматтер интегрируется на уровне бизнес-логики или сообщения-обёртки.


Расширение через форматтеры сообщений ICU

ICU-синтаксис позволяет создавать условные ветвления, что фактически заменяет часть кастомной логики форматтеров.

Globalize.loadMessages({
  ru: {
    notifications: "{count, plural, one {# уведомление} few {# уведомления} many {# уведомлений} other {# уведомления}}"
  }
});

Форматтер:

const formatNotifications = Globalize("ru").messageFormatter("notifications");

formatNotifications({ count: 5 });

При этом пользовательский форматтер здесь заключается не в коде, а в декларативном описании правил.


Композиция форматтеров

Ключевая техника построения сложных форматтеров — композиция.

const cldr = Globalize("ru");

const formatInvoice = (invoice) => {
  const header = `Счёт: ${invoice.id}`;
  const amount = cldr.formatNumber(invoice.amount, { style: "currency", currency: "RUB" });
  const date = cldr.formatDate(invoice.date, { date: "long" });

  const status = invoice.paid ? "Оплачен" : "Ожидает оплаты";

  return [header, amount, date, status].join("\n");
};

Композиция позволяет:

  • разделять форматирование разных типов данных;
  • комбинировать ICU и imperative-логику;
  • переиспользовать отдельные части форматтеров.

Переиспользование форматтеров через замыкания

Форматтеры могут быть «захвачены» замыканием, что снижает стоимость их повторного создания.

const cldr = Globalize("ru");

const currencyFormatter = (value) =>
  cldr.formatNumber(value, { style: "currency", currency: "RUB" });

const dateFormatter = (date) =>
  cldr.formatDate(date, { datetime: "medium" });

const formatTransaction = (tx) =>
  `${dateFormatter(tx.date)} | ${currencyFormatter(tx.amount)}`;

Такой подход особенно эффективен при обработке списков и таблиц.


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

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

const createCurrencyFormatter = (locale, currency) => {
  const cldr = Globalize(locale);

  return (value) =>
    cldr.formatNumber(value, { style: "currency", currency });
};

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

Подобная конструкция используется для мультивалютных интерфейсов и SaaS-систем.


Интеграция форматтеров с данными доменной модели

Форматтеры часто становятся частью слоя представления доменных объектов.

const cldr = Globalize("ru");

const formatProduct = (product) => ({
  title: product.name,
  price: cldr.formatNumber(product.price, { style: "currency", currency: "RUB" }),
  availability: product.stock > 0 ? "В наличии" : "Нет в наличии"
});

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


Кэширование результатов форматирования

При высокой нагрузке имеет смысл кэшировать форматтеры или их результаты.

const cldr = Globalize("ru");

const numberCache = new Map();

const cachedFormatNumber = (value) => {
  if (!numberCache.has(value)) {
    numberCache.set(
      value,
      cldr.formatNumber(value, { minimumFractionDigits: 2 })
    );
  }
  return numberCache.get(value);
};

Кэширование особенно эффективно при повторяющихся значениях (таблицы, отчёты, списки транзакций).


Динамическая замена форматтеров

В некоторых системах форматтеры могут подменяться в зависимости от контекста (например, режим отображения).

const createFormatter = (mode) => {
  const cldr = Globalize("ru");

  if (mode === "compact") {
    return (value) => cldr.formatNumber(value / 1000) + " тыс.";
  }

  return (value) =>
    cldr.formatNumber(value, { minimumFractionDigits: 2 });
};

Это позволяет адаптировать представление данных под разные UI-сценарии без изменения бизнес-логики.


Форматтеры как слой абстракции локализации

Пользовательские форматтеры в Globalize формируют промежуточный слой между:

  • сырой моделью данных;
  • локализационными правилами CLDR;
  • требованиями интерфейса.

Такой слой обеспечивает единообразие представления данных, снижает дублирование логики и позволяет масштабировать систему локализации без роста сложности бизнес-кода.