При интернационализации приложений недостаточно корректно форматировать только строки, даты и числа. В реальных системах регулярно встречаются специальные значения:
nullundefinedNaNInfinity-InfinityБиблиотека Globalize предоставляет инструменты для обработки большинства подобных ситуаций, однако часть логики необходимо реализовывать самостоятельно.
При локализации данные проходят несколько этапов:
На любом этапе могут появляться специальные значения, нарушающие корректную работу приложения.
Например:
const price = NaN;
const date = null;
const amount = Infinity;
Без дополнительной обработки форматтеры могут:
Перед использованием форматтеров рекомендуется выполнять предварительную валидацию.
function safeNumber(value) {
if (value === null || value === undefined) {
return "Нет данных";
}
if (Number.isNaN(value)) {
return "Некорректное число";
}
if (!Number.isFinite(value)) {
return "Бесконечность";
}
return value;
}
Использование:
const formatter = Globalize.numberFormatter();
console.log(
formatter(safeNumber(1500))
);
null и
undefinedЧасто сервер возвращает:
{
amount: null
}
или:
{
amount: undefined
}
Попытка форматирования может привести к ошибке:
const formatter = Globalize.numberFormatter();
formatter(null);
Результат зависит от конфигурации и версии библиотек CLDR, но подобное поведение считается небезопасным.
Практический подход — создание универсальной функции.
function formatNullableNumber(value, locale = "en") {
const globalize = new Globalize(locale);
const formatter = globalize.numberFormatter();
if (value == null) {
return "—";
}
return formatter(value);
}
Пример:
console.log(formatNullableNumber(null));
console.log(formatNullableNumber(undefined));
console.log(formatNullableNumber(15000));
Иногда вместо заглушки требуется использовать дефолтное значение.
function formatPrice(price) {
const formatter = Globalize.currencyFormatter("USD");
return formatter(price ?? 0);
}
Оператор ?? особенно удобен, так как не заменяет:
0false""NaNNaNЗначение NaN возникает при ошибочных вычислениях:
const result = 0 / 0;
или:
const value = Number("abc");
Number.isNaNНаиболее безопасный вариант:
function safeFormatNumber(value) {
if (Number.isNaN(value)) {
return "Ошибка вычисления";
}
return Globalize.formatNumber(value);
}
Специальные значения желательно локализовывать.
const messages = {
en: {
invalidNumber: "Invalid number"
},
ru: {
invalidNumber: "Некорректное число"
}
};
function localizedNaN(value, locale) {
if (Number.isNaN(value)) {
return messages[locale].invalidNumber;
}
return Globalize(locale).formatNumber(value);
}
Infinity и
-InfinityВ JavaScript существуют специальные числовые значения:
Infinity
-Infinity
Они появляются:
Number.isFinitefunction formatFiniteNumber(value) {
if (!Number.isFinite(value)) {
return "∞";
}
return Globalize.formatNumber(value);
}
function formatInfinity(value) {
if (value === Infinity) {
return "+∞";
}
if (value === -Infinity) {
return "-∞";
}
return Globalize.formatNumber(value);
}
Пустая строка:
""
не является отсутствующим значением.
Однако в формах и API она часто эквивалентна отсутствию данных.
function normalizeValue(value) {
if (value === "") {
return null;
}
return value;
}
Использование:
const value = normalizeValue("");
if (value === null) {
console.log("Значение отсутствует");
}
Globalize активно использует словари переводов.
Пример:
Globalize.loadMessages({
en: {
hello: "Hello"
}
});
Если перевод отсутствует:
Globalize.formatMessage("welcome");
может возникнуть исключение.
function safeMessage(key, locale = "en") {
try {
return Globalize(locale).formatMessage(key);
} catch (e) {
return `[missing:${key}]`;
}
}
Распространённый подход:
function messageWithFallback(key, locale) {
try {
return Globalize(locale).formatMessage(key);
} catch {
return Globalize("en").formatMessage(key);
}
}
Проблема:
new Globalize("xx-UNKNOWN");
Если CLDR-данные отсутствуют, форматирование завершится ошибкой.
const supportedLocales = ["en", "ru", "de"];
function resolveLocale(locale) {
if (!supportedLocales.includes(locale)) {
return "en";
}
return locale;
}
function createGlobalize(locale) {
const safeLocale = resolveLocale(locale);
return new Globalize(safeLocale);
}
JavaScript позволяет создавать невалидные даты:
const date = new Date("invalid");
function isValidDate(date) {
return date instanceof Date &&
!isNaN(date.getTime());
}
function safeFormatDate(date, locale = "ru") {
if (!isValidDate(date)) {
return "Неверная дата";
}
return Globalize(locale).formatDate(date);
}
Globalize использует ISO-коды валют:
Globalize.currencyFormatter("USD");
Некорректный код:
Globalize.currencyFormatter("INVALID");
может привести к исключению.
const allowedCurrencies = [
"USD",
"EUR",
"KZT",
"RUB"
];
function safeCurrencyFormatter(currency) {
if (!allowedCurrencies.includes(currency)) {
currency = "USD";
}
return Globalize.currencyFormatter(currency);
}
try/catchНекоторые методы Globalize выбрасывают исключения:
Пример безопасного вызова:
function safeExecute(callback) {
try {
return callback();
} catch (error) {
console.error(error);
return null;
}
}
Использование:
const result = safeExecute(() => {
return Globalize("ru").formatNumber(5000);
});
В крупных приложениях не рекомендуется вызывать Globalize напрямую из компонентов.
Лучше создать отдельный слой:
class FormatterService {
constructor(locale = "en") {
this.globalize = new Globalize(locale);
}
formatNumber(value) {
if (value == null) {
return "—";
}
if (!Number.isFinite(value)) {
return "∞";
}
return this.globalize.formatNumber(value);
}
formatDate(date) {
if (!isValidDate(date)) {
return "Неверная дата";
}
return this.globalize.formatDate(date);
}
}
Такой подход обеспечивает:
Globalize не должен получать “грязные” данные напрямую.
Часто используется слой нормализации:
function normalizeApiData(data) {
return {
amount: Number(data.amount) || 0,
name: data.name ?? "Unknown",
createdAt: new Date(data.createdAt)
};
}
Рекомендуемая архитектура:
| Слой | Ответственность |
|---|---|
| API | Получение данных |
| Normalizer | Очистка и приведение |
| Formatter | Локализация |
| UI | Отображение |
formatNumberGlobalize.formatNumber(NaN);
Globalize.formatNumber(Infinity);
Поведение зависит от:
Поэтому проверка значений должна выполняться заранее.
JavaScript содержит специальное значение:
-0
Проверка:
Object.is(value, -0)
function normalizeNegativeZero(value) {
if (Object.is(value, -0)) {
return 0;
}
return value;
}
При локализации списков возможны ситуации:
[]
function formatItems(items) {
if (!Array.isArray(items) || items.length === 0) {
return "Список пуст";
}
return items.join(", ");
}
Желательно выносить специальные строки в словари:
Globalize.loadMessages({
ru: {
errors: {
invalidDate: "Неверная дата",
invalidNumber: "Некорректное число",
noData: "Нет данных"
}
}
});
function getErrorMessage(key, locale = "ru") {
return Globalize(locale).formatMessage(
`errors/${key}`
);
}
function safeFormat(value, formatter, options = {}) {
const {
nullValue = "—",
nanValue = "NaN",
infinityValue = "∞"
} = options;
if (value == null) {
return nullValue;
}
if (Number.isNaN(value)) {
return nanValue;
}
if (!Number.isFinite(value)) {
return infinityValue;
}
try {
return formatter(value);
} catch {
return nullValue;
}
}
const formatter = Globalize("ru")
.numberFormatter();
console.log(
safeFormat(1500, formatter)
);
console.log(
safeFormat(NaN, formatter)
);
console.log(
safeFormat(Infinity, formatter)
);
Ошибка:
Globalize.formatDate(data.date);
Без проверки data.date.
Плохой пример:
value || 0
Проблема:
0false""тоже будут заменены.
Нельзя предполагать, что:
Опасный код:
formatter(apiResponse.total);
Без:
function formatUserBalance(balance, locale) {
if (balance == null) {
return "Нет данных";
}
if (Number.isNaN(balance)) {
return "Ошибка";
}
if (!Number.isFinite(balance)) {
return "∞";
}
try {
return Globalize(locale)
.currencyFormatter("USD")(balance);
} catch {
return "Ошибка форматирования";
}
}
Globalize не должен получать неподготовленные данные.
Форматирование должно находиться в сервисах, а не в UI-компонентах.
Минимальный набор:
Ошибки форматирования не должны ломать интерфейс.
Строки:
должны быть переведены так же, как и остальные сообщения приложения.