Кастомные форматтеры

Роль форматтеров в архитектуре интернационализации

В экосистеме FormatJS форматтеры представляют собой слой преобразования данных в локализованное строковое представление. Они используются на этапе рендеринга сообщений и позволяют адаптировать вывод под культурные и языковые особенности.

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

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


Механизм подключения кастомных форматтеров

FormatJS использует объект конфигурации, в который можно передать пользовательские форматтеры через formats.

Базовая структура выглядит следующим образом:

import { IntlMessageFormat } from 'intl-messageformat';

const formats = {
  number: {
    percentage: {
      style: 'percent',
      minimumFractionDigits: 2
    }
  }
};

const message = new IntlMessageFormat(
  'Завершено: {progress, number, percentage}',
  'ru',
  formats
);

message.format({ progress: 0.756 });

В данном примере создаётся кастомный числовой формат percentage, который переопределяет стандартное поведение форматирования числа.


Кастомизация числовых форматтеров

Числовые форматтеры в FormatJS основаны на Intl.NumberFormat. Кастомизация позволяет задавать предустановленные профили отображения чисел.

Пример расширенного набора числовых форматов:

const formats = {
  number: {
    currencyUSD: {
      style: 'currency',
      currency: 'USD',
      minimumFractionDigits: 2,
      maximumFractionDigits: 2
    },
    compactShort: {
      notation: 'compact',
      compactDisplay: 'short'
    }
  }
};

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

const mf = new IntlMessageFormat(
  'Баланс: {amount, number, currencyUSD}',
  'en',
  formats
);

mf.format({ amount: 12345.67 });

Кастомные числовые форматтеры часто применяются для:

  • финансовых систем
  • аналитических панелей
  • интерфейсов с ограниченным пространством
  • отображения больших чисел в компактной форме

Форматирование дат и времени через пользовательские шаблоны

Дата и время являются одним из наиболее частых источников локализационных ошибок. FormatJS позволяет определять именованные шаблоны поверх Intl.DateTimeFormat.

const formats = {
  date: {
    shortDateTime: {
      year: '2-digit',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit'
    },
    longDate: {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    }
  }
};

Применение:

const mf = new IntlMessageFormat(
  'Создано: {createdAt, date, longDate}',
  'ru',
  formats
);

mf.format({ createdAt: new Date() });

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


Кастомные форматтеры списков

Списки в FormatJS используют Intl.ListFormat, однако кастомизация позволяет задать предопределённые стили объединения элементов.

const formats = {
  list: {
    semicolon: {
      style: 'short',
      type: 'unit',
      separator: '; '
    },
    narrow: {
      style: 'narrow',
      type: 'conjunction'
    }
  }
};

Пример использования:

const mf = new IntlMessageFormat(
  'Теги: {tags, list, semicolon}',
  'ru',
  formats
);

mf.format({ tags: ['JS', 'i18n', 'FormatJS'] });

Подобные форматтеры полезны при отображении тегов, категорий и системных меток.


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

Стандартный механизм FormatJS ограничен конфигурацией Intl.*Format. Однако возможна регистрация полностью кастомных форматтеров через расширение API.

Подход заключается в создании функции, которая принимает значение и возвращает строку:

const customFormats = {
  custom: {
    uppercase: (value) => String(value).toUpperCase(),
    maskEmail: (value) => {
      const [name, domain] = value.split('@');
      return `${name[0]}***@${domain}`;
    }
  }
};

Интеграция через обёртку:

function formatMessage(message, locale, formats, values) {
  return message.replace(
    /\{(\w+),\s*custom,\s*(\w+)\}/g,
    (_, key, formatName) => {
      const value = values[key];
      return customFormats.custom[formatName](value);
    }
  );
}

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

formatMessage(
  'Пользователь: {email, custom, maskEmail}',
  'ru',
  customFormats,
  { email: 'user@example.com' }
);

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


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

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

export const formats = {
  number: {
    percentStrict: {
      style: 'percent',
      minimumFractionDigits: 1,
      maximumFractionDigits: 1
    }
  },
  date: {
    isoLike: {
      year: 'numeric',
      month: '2-digit',
      day: '2-digit'
    }
  }
};

Такая структура обеспечивает:

  • единообразие интерфейсов
  • снижение дублирования кода
  • упрощение поддержки локализаций

Переопределение стандартных форматов

FormatJS позволяет не только добавлять новые форматы, но и переопределять существующие. Это используется при необходимости адаптации стандартов под требования продукта.

Пример изменения поведения числового форматтера:

const formats = {
  number: {
    integer: {
      maximumFractionDigits: 0,
      useGrouping: false
    }
  }
};

В результате все сообщения, использующие number, integer, будут выводить числа без разделителей и десятичных знаков.


Условные форматтеры и контекстная логика

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

function formatAmount(value, currency) {
  const formats = {
    number: {
      dynamicCurrency: {
        style: 'currency',
        currency
      }
    }
  };

  const mf = new IntlMessageFormat(
    'Сумма: {value, number, dynamicCurrency}',
    'ru',
    formats
  );

  return mf.format({ value });
}

Такой подход позволяет динамически менять поведение форматтера без изменения шаблонов сообщений.


Интеграция кастомных форматтеров в масштабируемые системы

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

Типичная структура:

/i18n
  formats/
    number.js
    date.js
    list.js
  index.js

Файл агрегации:

import number from './formats/number';
import date from './formats/date';
import list from './formats/list';

export const formats = {
  number,
  date,
  list
};

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


Ограничения кастомных форматтеров

Несмотря на гибкость, кастомные форматтеры имеют ряд ограничений:

  • невозможность полной интеграции с ICU-синтаксисом при произвольной логике
  • необходимость ручного контроля локализации внутри пользовательских функций
  • потенциальное усложнение тестирования сообщений
  • риск нарушения консистентности форматов при неконтролируемом расширении

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