Библиотека Globalize поддерживает два подхода к созданию форматтеров и парсеров:
Во время обычной работы методы formatDate,
formatNumber, numberParser,
currencyFormatter и другие выполняют внутреннюю генерацию
функций на основе CLDR-данных. Этот процесс требует:
Для браузерных приложений это означает:
Предварительная компиляция решает эти проблемы путём генерации готовых форматтеров на этапе сборки проекта.
Globalize разделяет библиотеку на две части:
| Часть | Назначение |
|---|---|
| Runtime | Выполнение готовых форматтеров |
| Compiler | Генерация форматтеров |
Во время разработки используется полный пакет с возможностью компиляции, а в production остаётся только runtime.
Схема работы:
CLDR JSON
↓
Globalize Compiler
↓
Сгенерированный JS-код
↓
Runtime Globalize
↓
Форматирование без компиляции
Без precompile приложение включает:
После компиляции остаются только:
Разница может составлять десятки или сотни килобайт.
Динамическая генерация форматтера:
const formatter = Globalize.dateFormatter({
datetime: "medium"
});
выполняет сложную внутреннюю работу при первом вызове.
Предварительно скомпилированный форматтер:
const formatter = compiledDateFormatter;
готов к использованию мгновенно.
В production не требуется:
Это особенно важно для:
Компиляция происходит на этапе CI/CD.
Ошибки обнаруживаются заранее:
npm install globalize
npm install cldr-data
npm install globalize-compiler
Иногда используется отдельный runtime:
npm install globalize-runtime
Типичная структура:
project/
├── src/
├── build/
├── locales/
├── cldr/
├── compiled/
├── webpack.config.js
└── package.json
Globalize использует Unicode CLDR.
Минимальный набор:
const Cldr = require("cldrjs");
Cldr.load(
require("cldr-data/supplemental/likelySubtags.json"),
require("cldr-data/supplemental/numberingSystems.json"),
require("cldr-data/main/en/numbers.json"),
require("cldr-data/main/en/ca-gregorian.json")
);
Предварительная компиляция требует знания:
Для этого применяется extractor.
const GlobalizeCompiler = require("globalize-compiler");
const compiler = new GlobalizeCompiler({
defaultLocale: "en"
});
const formatter = Globalize("en").numberFormatter({
minimumFractionDigits: 2
});
Во время выполнения создаётся formatter factory.
compiler.compile([
{
path: "numberFormatter",
args: [
{
minimumFractionDigits: 2
}
]
}
]);
Компилятор генерирует Jav * aScript:
(function() {
return function(value) {
return value.toFixed(2);
};
})();
Реальный код значительно сложнее, поскольку учитывает:
const fs = require("fs");
const output = compiler.compile(...);
fs.writeFileSync(
"./compiled/formatters.js",
output
);
const Globalize = require("globalize-runtime");
const compiled = require("./compiled/formatters");
compiled.numberFormatter(12345.67);
const formatter = Globalize("ru").dateFormatter({
skeleton: "yMMMd"
});
compiler.compile([
{
path: "dateFormatter",
locale: "ru",
args: [
{
skeleton: "yMMMd"
}
]
}
]);
Компилятор заранее вычисляет:
Во время runtime остаётся только подстановка значений.
Globalize("en").currencyFormatter("USD");
compiler.compile([
{
path: "currencyFormatter",
locale: "en",
args: ["USD"]
}
]);
Компилятор заранее определяет:
Globalize("ru").plural(5);
compiler.compile([
{
path: "pluralGenerator"
}
]);
Пример упрощённой логики:
function plural(n) {
if (n % 10 === 1 && n % 100 !== 11) {
return "one";
}
return "other";
}
Для реальных локалей правила значительно сложнее.
const formatter = Globalize("ru").messageFormatter(
"{count, plural, one {# файл} few {# файла} many {# файлов}}"
);
ICU-компиляция — одна из самых тяжёлых операций в Globalize.
Необходимо:
compiler.compile([
{
path: "messageFormatter",
locale: "ru",
args: [
"{count, plural, one {# файл} few {# файла} many {# файлов}}"
]
}
]);
Упрощённый пример:
function(d) {
const count = d.count;
if (count === 1) {
return count + " файл";
}
return count + " файлов";
}
const locales = ["en", "ru", "de"];
locales.forEach(locale => {
compiler.compile([
{
path: "numberFormatter",
locale,
args: []
}
]);
});
Для крупных приложений локали обычно делятся:
compiled/
├── en.js
├── ru.js
├── de.js
└── fr.js
async function loadLocale(locale) {
return import(`./compiled/${locale}.js`);
}
const webpack = require("webpack");
const GlobalizePlugin = require("globalize-webpack-plugin");
Плагин автоматизирует:
plugins: [
new GlobalizePlugin({
production: true,
developmentLocale: "en"
})
]
Во время сборки:
Без предварительной компиляции tree shaking работает плохо, поскольку:
После precompile код становится статическим.
Dynamic formatter:
Globalize("en").numberFormatter();
может создавать:
Compiled formatter:
compiledNumberFormatter(value);
обычно не требует дополнительных allocations.
Без precompile часто используется:
const cache = {};
function getFormatter(locale) {
if (!cache[locale]) {
cache[locale] =
Globalize(locale).numberFormatter();
}
return cache[locale];
}
Поскольку formatter уже готов:
import formatter from "./compiled/en";
formatter(10);
На слабых устройствах startup bottleneck часто связан с:
Precompile уменьшает CPU-нагрузку при старте.
const Globalize = require("globalize-runtime");
Runtime package:
Нельзя:
Globalize("en").messageFormatter(...);
если formatter не был предварительно скомпилирован.
Типичная ошибка:
E_MISSING_CLDR
или:
Formatter not compiled
Сгенерированные formatter-функции можно использовать:
messages/
├── en.json
├── ru.json
└── de.json
{
"greeting": "Hello {name}"
}
compiler.compile([
{
path: "messageFormatter",
locale: "en",
args: [
"Hello {name}"
]
}
]);
Обычно compile step выполняется:
npm run compile-i18n
перед:
npm run build
В крупных приложениях компилируются только изменённые локали.
Пример:
git diff
↓
changed locale files
↓
partial compile
CLDR регулярно обновляется.
Изменения могут затрагивать:
После обновления CLDR необходима перекомпиляция formatter’ов.
Версии должны совпадать:
globalize
globalize-runtime
globalize-compiler
Иначе возможны несовместимости internal API.
Необязательно подключать полный CLDR.
Можно оставить только:
Пример минимального набора:
Cldr.load(
require("cldr-data/main/en/numbers.json")
);
Сгенерированный код полезно проверять:
Compiled output хорошо минифицируется:
terser compiled.js
поскольку содержит:
Предварительная компиляция помогает bundler’ам удалять:
В SSR критична скорость обработки запроса.
Runtime compilation:
request
↓
formatter compile
↓
render
увеличивает latency.
request
↓
ready formatter
↓
render
CPU-нагрузка значительно ниже.
const formatter =
Globalize(locale).dateFormatter();
может вызываться внутри компонента.
import { dateFormatter } from "./compiled/ru";
dateFormatter(date);
Hydration-sensitive приложения выигрывают за счёт:
create formatter: 4ms
format call: 0.05ms
create formatter: 0ms
format call: 0.05ms
Основная экономия — устранение стадии генерации.
Особенно при:
На low-end Android runtime compilation может быть дорогой.
Снижается startup time desktop-приложения.
Cold start становится меньше.
Небольшие проекты с:
source messages
↓
extract
↓
compile
↓
generate runtime
↓
webpack bundle
↓
deploy
compiled/
├── formatters/
│ ├── number.js
│ ├── date.js
│ └── currency.js
├── messages/
│ ├── en.js
│ └── ru.js
└── locales/
├── en.js
└── ru.js
Крупные проекты обычно разделяют:
Это позволяет:
Missing required CLDR content
Причина:
Cannot read property of undefined
Причина:
Formatter not found
Причина:
Это наиболее тяжёлая часть runtime.
Особенно для SPA.
globalize-runtime
вместо полного:
globalize
Подключать только используемые данные.
Через:
CLDR minimization
+
message precompile
+
formatter precompile
+
locale chunking
+
runtime-only package
↓
минимальный i18n overhead