Многие JavaScript-проекты начинали интернационализацию с простых
решений: ручных словарей, i18next,
Polyglot.js, moment.js, Intl,
Numeral.js или самописных модулей локализации. По мере
роста приложения появляются проблемы:
Globalize решает эти задачи за счёт интеграции с CLDR и стандартизированного подхода к интернационализации.
Нативный Intl предоставляет базовые механизмы
локализации:
new Intl.NumberFormat("fr").format(12345.67);
Globalize строится поверх CLDR и добавляет:
Пример с Intl:
const formatter = new Intl.DateTimeFormat("de");
formatter.format(new Date());
Пример с Globalize:
const dateFormatter = Globalize("de").dateFormatter({
datetime: "medium"
});
dateFormatter(new Date());
Главное отличие — Globalize требует явной загрузки данных CLDR.
Во многих проектах локализация выглядит так:
const translations = {
en: {
hello: "Hello"
},
ru: {
hello: "Привет"
}
};
function t(locale, key) {
return translations[locale][key];
}
Подобный подход быстро становится неуправляемым:
function price(value) {
return value + " USD";
}
const formatter = Globalize("en").currencyFormatter("USD");
formatter(1200);
Результат:
$1,200.00
Для русской локали:
const formatter = Globalize("ru").currencyFormatter("USD");
formatter(1200);
Результат:
1 200,00 $
Формат автоматически определяется локалью.
Numeral.js ориентирован только на числа:
numeral(1000).format("0,0");
Недостатки:
Numeral.js:
numeral(12345.67).format("0,0.00");
Globalize:
const formatter = Globalize("en").numberFormatter({
minimumFractionDigits: 2
});
formatter(12345.67);
Numeral.js:
numeral(0.56).format("0%");
Globalize:
const formatter = Globalize("en").numberFormatter({
style: "percent"
});
formatter(0.56);
Основные причины:
moment.js:
moment(date).format("DD.MM.YYYY");
Globalize:
const formatter = Globalize("ru").dateFormatter({
skeleton: "yMd"
});
formatter(date);
moment.js:
moment().fromNow();
Globalize:
const formatter = Globalize("en").relativeTimeFormatter("day");
formatter(-1);
Результат:
yesterday
i18next фокусируется на переводах.
Globalize ориентирован на:
Во многих проектах используется гибрид:
Но при полной миграции Globalize способен заменить оба слоя.
i18next.t("welcome");
const formatter = Globalize("ru").messageFormatter("welcome");
formatter();
i18next:
i18next.t("hello", {
name: "Анна"
});
Globalize:
const formatter = Globalize("ru").messageFormatter("hello");
formatter({
name: "Анна"
});
{
"items": "У вас {{count}} товаров"
}
Такой вариант не учитывает plural forms.
{
"items": "{count, plural, one{У вас # товар} few{У вас # товара} many{У вас # товаров} other{У вас # товара}}"
}
Использование:
const formatter = Globalize("ru").messageFormatter("items");
formatter({ count: 5 });
Polyglot.js подходит для небольших проектов, но имеет ограничения:
Polyglot.js:
polyglot.t("cars", 5);
Globalize:
const formatter = Globalize("ru").messageFormatter("cars");
formatter({ count: 5 });
/locales
en.json
ru.json
/date-utils
/number-utils
/currency-utils
/cldr
/messages
/globalize
formatPrice()
formatDate()
translate()
pluralize()
Все механизмы разрознены.
Globalize(locale)
Единая точка входа:
const g = Globalize("ru");
g.formatMessage(...);
g.formatNumber(...);
g.formatDate(...);
if (locale === "ru") {
...
}
Логика локализации переносится в CLDR.
Вместо ручных условий:
const formatter = Globalize(locale)
.currencyFormatter("EUR");
Globalize требует загрузки CLDR-данных.
Минимальный набор:
const Cldr = require("cldrjs");
const Globalize = require("globalize");
Globalize.load(
require("cldr-data/main/en/numbers"),
require("cldr-data/main/en/ca-gregorian"),
require("cldr-data/supplemental/likelySubtags")
);
Ошибка:
E_MISSING_CLDR
Причина:
Globalize.load(...)
загружает только часть данных.
Необходимо:
require("cldr-data/supplemental/numberingSystems")
require("cldr-data/supplemental/plurals")
Ошибка:
Globalize.locale("ru");
без загрузки locale data.
Правильный порядок:
Globalize.load(...)
Globalize.locale("ru");
CLDR содержит большой объём данных.
Неправильный импорт:
require("cldr-data");
резко увеличивает bundle.
require("cldr-data/main/ru/numbers");
require("cldr-data/main/ru/ca-gregorian");
async function loadLocale(locale) {
const data = await import(
`cldr-data/main/${locale}/numbers.json`
);
Globalize.load(data);
}
Без precompile:
Globalize.messageFormatter(...)
компилирует сообщения в runtime.
При большом количестве переводов это создаёт нагрузку.
globalize-compiler extract
globalize-compiler compile
После компиляции:
import compiled from "./compiled-messages";
compiled.formatMessage(...);
if (count === 1) {
return "товар";
}
"{count, plural,
one{товар}
few{товара}
many{товаров}
}"
Globalize автоматически использует правила языка.
Обычно:
UI → translate()
UI → formatDate()
UI → formatMoney()
Разные сервисы и библиотеки.
UI → Globalize
Единая инфраструктура локализации.
<span>{i18next.t("hello")}</span>
<span>{g.formatMessage("hello")}</span>
import Globalize from "globalize";
export function createI18n(locale) {
const g = new Globalize(locale);
return {
t: g.formatMessage.bind(g),
n: g.formatNumber.bind(g),
d: g.formatDate.bind(g)
};
}
Полный переход редко выполняется сразу.
Чаще используется схема:
1. Переводы
2. Числа
3. Валюты
4. Даты
5. Relative time
6. ICU messages
i18next.t(...)
Globalize.formatNumber(...)
Такой подход позволяет мигрировать поэтапно.
{
"hello": "Привет"
}
{
"hello": "Привет, {name}"
}
{
"save": "Сохранить"
}
{
"save": "{gender, select,
male{Сохранил}
female{Сохранила}
other{Сохранил(а)}
}"
}
function formatDate(date) {
return date.toLocaleDateString("ru");
}
const formatter = Globalize("ru")
.dateFormatter({
date: "long"
});
formatter(date);
После миграции:
Globalize особенно полезен при:
Использование Globalize не всегда оправдано.
Для небольших проектов иногда достаточно:
Intl;i18next;date-fns;Globalize наиболее эффективен в крупных системах с полноценной интернационализацией и глубокой зависимостью от CLDR.