В Globalize форматирование сообщений реализовано через
предварительную компиляцию шаблонов в функции форматирования. Каждый
вызов Globalize.messageFormatter() или
Globalize.formatMessage() не просто интерпретирует строку
шаблона, а преобразует её в исполняемую функцию, которая затем
применяется к входным данным. Именно этот этап компиляции является
ключевой точкой для кеширования.
Сообщения в Globalize задаются через ICU-подобные шаблоны:
Globalize.loadMessages({
ru: {
greeting: "Привет, {name}!",
items: "У вас {count, plural, one{# элемент} few{# элемента} many{# элементов}}"
}
});
При обращении к сообщению:
const formatter = Globalize.messageFormatter("greeting");
formatter({ name: "Иван" });
происходит трансформация строки "Привет, {name}!" в
функцию, которая знает, как подставлять параметры, обрабатывать
плейсхолдеры и применять правила локали.
Эта трансформация стоит дороже обычного вызова функции форматирования, поэтому повторная компиляция одного и того же сообщения становится узким местом при частых операциях i18n.
Globalize использует стратегию внутреннего кеширования, где скомпилированные функции форматирования сохраняются по ключу:
locale)message key)Структурно это можно представить как:
cache[locale][messageKey] -> compiledFormatterFunction
При повторном вызове:
const f1 = Globalize.messageFormatter("greeting");
const f2 = Globalize.messageFormatter("greeting");
f1 и f2 ссылаются на одну и ту же
скомпилированную функцию, если локаль и сообщения не изменялись.
messageFormatter как точки кешированияФункция Globalize.messageFormatter() является ленивым
фабричным методом. Она выполняет две задачи:
const formatter = Globalize.messageFormatter("items");
Первая операция требует парсинга ICU-синтаксиса и построения AST (абстрактного синтаксического дерева). Вторая и последующие — только вызов уже готовой функции.
Альтернативный способ работы:
Globalize.formatMessage("greeting", { name: "Иван" });
Этот вызов внутри себя также использует кешированный форматтер, но с дополнительным уровнем абстракции: каждый раз происходит обращение к фабрике, которая возвращает уже готовую функцию.
Поэтому при интенсивных вызовах предпочтительнее сохранять форматтер явно:
const greeting = Globalize.messageFormatter("greeting");
greeting({ name: "Иван" });
greeting({ name: "Мария" });
Так устраняется лишний lookup по ключу сообщения.
Критически важный аспект кеширования связан с локалью:
Globalize.locale("ru");
const f1 = Globalize.messageFormatter("greeting");
Globalize.locale("en");
const f2 = Globalize.messageFormatter("greeting");
Хотя ключ сообщения одинаков, результат будет разным, поскольку:
Кеш разделяется по локали, поэтому смена локали автоматически приводит к работе с другим пространством кеша. Старые форматтеры не пересоздаются, но становятся логически привязанными к своей локали.
До момента вызова messageFormatter сообщения должны быть
загружены:
Globalize.loadMessages({
en: { hello: "Hello" },
ru: { hello: "Привет" }
});
Важно, что кеширование скомпилированных сообщений не заменяет этап загрузки. При обновлении сообщений:
Globalize.loadMessages({
ru: { hello: "Здравствуйте" }
});
поведение зависит от реализации загрузки:
Поэтому динамическая подмена сообщений требует аккуратного управления жизненным циклом форматтеров.
Компиляция сообщений включает:
Эта операция линейно зависит от сложности сообщения. Например:
Кеширование снижает нагрузку до уровня:
Разница становится особенно заметной при массовом рендеринге интерфейсов.
На практике часто используется явное хранение форматтеров:
const formatters = {
greeting: Globalize.messageFormatter("greeting"),
items: Globalize.messageFormatter("items"),
error: Globalize.messageFormatter("error")
};
Такой подход даёт несколько эффектов:
Особенно это важно в SPA и SSR-рендеринге, где одни и те же сообщения используются многократно в одном цикле рендеринга.
В Node.js каждый инстанс приложения обычно работает с одним набором локалей. Это позволяет использовать долговременное кеширование форматтеров на уровне процесса:
const Globalize = require("globalize");
Globalize.loadMessages(...);
Globalize.locale("ru");
const fmt = Globalize.messageFormatter("greeting");
При таком подходе кеш живёт весь жизненный цикл процесса, что делает повторные запросы к одному и тому же сообщению практически бесплатными.
Однако при мультиарендности (multi-tenant) с разными локалями возникает необходимость:
Несмотря на очевидные преимущества, кеширование скомпилированных сообщений может приводить к ряду эффектов:
Утечка памяти при динамических локалях
Если приложение постоянно добавляет новые локали или сообщения, кеш может разрастаться без очистки.
Несогласованность сообщений
При изменении набора сообщений в рантайме старые форматтеры могут продолжать использовать устаревшие шаблоны.
Неявная зависимость от порядка загрузки
Если форматтер создаётся до полной загрузки сообщений, кеш закрепляет неполный или дефолтный вариант.
На уровне архитектуры обычно применяются следующие подходы:
messageFormatter() внутри
горячих цикловЛогически кеш скомпилированных сообщений в Globalize можно представить как трёхуровневую систему:
Переход между уровнями происходит только один раз на сообщение в рамках конкретной локали, после чего система работает исключительно с функциями, минуя парсинг и компиляцию.