Хуки и middleware

В библиотеке Globalize расширение поведения строится вокруг композиции функций форматирования, парсинга и локализационных преобразований. Несмотря на отсутствие формального механизма middleware в классическом смысле серверных фреймворков, архитектура позволяет выстраивать аналогичные цепочки обработки данных за счёт обёрток, пользовательских форматов и перехвата вызовов на уровне API.

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

  • слой данных (CLDR JSON),
  • слой вычислений (Globalize API).

Расширяемость достигается не через изменение внутреннего кода, а через композицию поверх API. Это создаёт основу для middleware-подобных решений, где каждый слой может модифицировать входные или выходные данные.

Ключевой принцип:

  • вход → преобразование → выход, где преобразование может быть заменено или дополнено внешней функцией.

Обёртки как механизм middleware

На практике middleware в контексте Globalize реализуется через обёртывание функций форматирования.

const baseNumberFormatter = Globalize.numberFormatter();

function loggingFormatter(value) {
    const start = performance.now();
    const result = baseNumberFormatter(value);
    const end = performance.now();

    console.log("Format duration:", end - start);
    return result;
}

Здесь создаётся поведенческий слой, который не изменяет сам Globalize, но вмешивается в процесс выполнения. Такая модель масштабируется:

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

Композиция остаётся прозрачной и управляемой.


Цепочки преобразований

Middleware-подход усиливается при построении цепочек функций. Каждая функция принимает результат предыдущей и возвращает модифицированное значение.

function trimWhitespace(next) {
    return function (value) {
        return next(value.trim());
    };
}

function toUpperCase(next) {
    return function (value) {
        return next(value.toUpperCase());
    };
}

const baseMessageFormatter = Globalize.messageFormatter("greeting");

const enhancedFormatter = trimWhitespace(
    toUpperCase(
        baseMessageFormatter
    )
);

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


Перехват числового форматирования

Числовые форматтеры Globalize часто используются в приложениях с высокой нагрузкой на UI. Middleware здесь применяется для стандартизации входных данных и контроля диапазонов значений.

const formatter = Globalize.numberFormatter({
    minimumFractionDigits: 2
});

function safeNumberFormatter(value) {
    if (typeof value !== "number" || Number.isNaN(value)) {
        return formatter(0);
    }

    if (!Number.isFinite(value)) {
        return formatter(0);
    }

    return formatter(value);
}

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


Расширение форматирования дат через композицию

Форматирование дат в Globalize также может быть включено в middleware-конвейер. Поскольку форматтеры возвращают чистые строки, они легко комбинируются с дополнительной логикой.

const dateFormatter = Globalize.dateFormatter({
    datetime: "medium"
});

function timezoneMiddleware(next, offsetHours) {
    return function (date) {
        const shifted = new Date(date.getTime());
        shifted.setHours(shifted.getHours() + offsetHours);
        return next(shifted);
    };
}

const formatterWithTimezone = timezoneMiddleware(dateFormatter, 3);

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


Middleware для сообщений и ICU-шаблонов

MessageFormatter в Globalize работает на основе ICU Message syntax. Это создаёт точку расширения, где можно внедрять промежуточную обработку сообщений до их форматирования.

const formatter = Globalize.messageFormatter("items");

function pluralizationMiddleware(next) {
    return function (data) {
        if (data.count < 0) {
            data = { ...data, count: 0 };
        }
        return next(data);
    };
}

const enhancedMessage = pluralizationMiddleware(formatter);

Такие обёртки используются для:

  • нормализации входных параметров,
  • корректировки бизнес-правил локализации,
  • защиты от неконсистентных данных.

Хуки загрузки CLDR данных

Отдельный уровень расширения связан с загрузкой и подготовкой CLDR данных. Хотя Globalize не навязывает единый lifecycle, приложения часто вводят собственные хуки инициализации.

Типичный сценарий:

  • загрузка JSON CLDR,
  • установка языкового окружения,
  • подготовка форматтеров.
async function loadLocale(locale) {
    const data = await fetch(`/cldr/${locale}.json`).then(r => r.json());

    Globalize.load(data);

    return initializeFormatters(locale);
}

Такая функция выступает как hook, через который можно:

  • кэшировать данные,
  • валидировать структуру CLDR,
  • логировать загрузку локалей.

Кэширование как middleware-слой

Одним из наиболее практичных применений middleware в Globalize является кэширование форматтеров. Поскольку создание форматтеров может быть дорогостоящим, добавляется слой мемоизации.

const cache = new Map();

function cachedNumberFormatter(options) {
    const key = JSON.stringify(options);

    if (cache.has(key)) {
        return cache.get(key);
    }

    const formatter = Globalize.numberFormatter(options);
    cache.set(key, formatter);

    return formatter;
}

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


Слой нормализации данных

Middleware часто используется для приведения входных данных к ожидаемому виду перед передачей в Globalize.

function normalizeInput(next) {
    return function (value) {
        if (typeof value === "string") {
            value = Number(value.replace(",", "."));
        }
        return next(value);
    };
}

Это особенно важно в интерфейсах, где данные поступают из разных источников:

  • формы пользователя,
  • API,
  • локального хранилища.

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

Наиболее гибкая модель возникает при объединении нескольких middleware в один pipeline.

function compose(...fns) {
    return fns.reduce((prev, next) => next(prev));
}

const formatter = compose(
    normalizeInput,
    safeNumberFormatter
);

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


Контроль побочных эффектов

Globalize сам по себе не имеет побочных эффектов в форматировании, но middleware-слой может их вводить. Поэтому распространённой практикой является разделение:

  • чистых форматтеров,
  • обёрток с логированием,
  • обёрток с доступом к внешним системам.
function sideEffectLogger(next) {
    return function (value) {
        const result = next(value);
        sendMetric("format_used");
        return result;
    };
}

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


Интеграция с UI-слоями

В прикладных архитектурах middleware вокруг Globalize часто используется на уровне сервисов локализации:

  • единая точка форматирования,
  • централизованная обработка ошибок,
  • унификация представления данных.
class I18nService {
    constructor(globalize) {
        this.globalize = globalize;
        this.numberFormatter = cachedNumberFormatter({ maximumFractionDigits: 2 });
    }

    formatPrice(value) {
        return this.numberFormatter(value);
    }
}

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