Валидация данных в многоязычных приложениях перестаёт быть формальной проверкой соответствия регулярным выражениям и превращается в задачу согласования пользовательского ввода с локальными правилами представления чисел, дат, строк и списков. Библиотека Intl в JavaScript предоставляет стандартизированный слой для работы с культурно-зависимыми форматами, позволяя строить валидацию, основанную на реальных правилах отображения данных, а не на предположениях о единственном универсальном формате.
Ключевой принцип заключается в том, что корректность ввода определяется не синтаксисом строки как таковой, а её соответствием форматам конкретной локали. Это особенно критично для чисел, дат, валют и текстовых сравнений.
Обычные регулярные выражения для чисел быстро становятся неустойчивыми при переходе между локалями: разделители дробной и тысячной части, использование пробелов, неразрывных пробелов и разных символов запятой и точки создают множество вариаций одного и того же значения.
Intl.NumberFormat позволяет получить эталон
представления числа для конкретной локали.
const formatter = new Intl.NumberFormat("de-DE");
formatter.format(1234567.89);
// "1.234.567,89"
Один из устойчивых подходов — нормализация ввода через преобразование к числу и обратное форматирование:
function isValidNumber(input, locale) {
const number = Number(input.replace(/\s/g, "").replace(",", "."));
if (Number.isNaN(number)) return false;
const formatted = new Intl.NumberFormat(locale).format(number);
return formatted.replace(/\u00A0/g, " ") === input.trim();
}
Здесь используется сравнение канонического представления с исходной строкой. Важный момент — разные локали используют разные символы разделителей:
de-DE: точка для тысяч, запятая для дробейen-US: запятая для тысяч, точка для дробейБолее надёжный способ — анализ структуры числа через
formatToParts:
const nf = new Intl.NumberFormat("fr-FR");
nf.formatToParts(12345.6);
Результат содержит структурированные токены:
Это позволяет валидировать ввод не как строку, а как последовательность допустимых компонентов:
function validateNumberStructure(input, locale) {
const nf = new Intl.NumberFormat(locale);
const parts = nf.formatToParts(1000.1);
const allowedSymbols = parts
.filter(p => p.type !== "integer" && p.type !== "fraction")
.map(p => p.value);
return allowedSymbols.some(symbol => input.includes(symbol));
}
Парсинг дат в JavaScript традиционно опирается на
Date.parse, который не гарантирует стабильное поведение для
локализованных строк. Intl API позволяет перейти к обратному подходу:
проверка соответствия формату, а не разбор строки.
const dtf = new Intl.DateTimeFormat("en-GB");
dtf.format(new Date(2024, 0, 15));
// "15/01/2024"
const dtf = new Intl.DateTimeFormat("ru-RU");
dtf.formatToParts(new Date());
Типичная структура включает:
Подход основан на сопоставлении структуры:
function validateDate(input, locale) {
const date = new Date(input);
if (Number.isNaN(date.getTime())) return false;
const formatter = new Intl.DateTimeFormat(locale);
const formatted = formatter.format(date);
return formatted === input;
}
Такой подход требует строгого совпадения, что полезно для форм, но ограничивает гибкость. Более устойчивый вариант — извлечение числовых частей и проверка диапазонов:
function validateDateParts(dateString, locale) {
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) return false;
const parts = new Intl.DateTimeFormat(locale).formatToParts(date);
const hasValidDay = parts.some(p => p.type === "day");
const hasValidMonth = parts.some(p => p.type === "month");
const hasValidYear = parts.some(p => p.type === "year");
return hasValidDay && hasValidMonth && hasValidYear;
}
Intl.NumberFormat с параметром
style: "currency" вводит дополнительные ограничения,
которые полезны для валидации финансовых данных.
const nf = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
nf.format(1234.56);
// "$1,234.56"
Валюта добавляет символы, которые должны строго соответствовать локали:
function validateCurrency(input, locale, currency) {
const nf = new Intl.NumberFormat(locale, {
style: "currency",
currency
});
const parsed = Number(input.replace(/[^0-9.,-]/g, ""));
if (Number.isNaN(parsed)) return false;
return nf.format(parsed).replace(/\u00A0/g, " ") === input;
}
Особенность валютных форматов — наличие символов, которые могут быть как префиксами, так и суффиксами:
$1,000.00 (en-US)1 000,00 € (fr-FR)Это делает невозможным универсальное регулярное выражение без учёта локали.
Валидация строк часто сводится к проверке равенства или порядка.
Однако простое сравнение через === не учитывает:
Intl.Collator вводит локализованную семантику
сравнения.
const collator = new Intl.Collator("de", { sensitivity: "base" });
collator.compare("straße", "strasse");
// 0 (равны в заданной чувствительности)
function isUnique(value, list, locale) {
const collator = new Intl.Collator(locale, { sensitivity: "base" });
return !list.some(item => collator.compare(item, value) === 0);
}
Это позволяет предотвращать дублирование данных, которые визуально идентичны, но отличаются Unicode-представлением.
Текстовый ввод может содержать визуально одинаковые, но технически разные строки:
function normalize(input) {
return input.normalize("NFC");
}
Без нормализации сравнение строк становится ненадёжным даже в пределах одной локали.
Валидация длины строки и границ слов требует корректного разбиения
текста. Простое использование .length или
.split("") некорректно для многих языков.
Intl.Segmenter позволяет работать с границами графем,
слов и предложений.
const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
[...segmenter.segment("こんにちは")].length;
// корректное количество символов
function validateLength(input, locale, max) {
const segmenter = new Intl.Segmenter(locale, { granularity: "grapheme" });
const length = [...segmenter.segment(input)].length;
return length <= max;
}
Это критично для интерфейсов, где ограничения задаются в “символах”, но фактически должны учитывать графемы.
Валидация списков часто сводится к проверке разделителей
(",", ";"), что ломается при локализации.
Intl.ListFormat определяет корректный формат соединения
элементов:
const lf = new Intl.ListFormat("en", { style: "long", type: "conjunction" });
lf.format(["apples", "oranges", "bananas"]);
// "apples, oranges, and bananas"
function validateList(items, input, locale) {
const lf = new Intl.ListFormat(locale);
const formatted = lf.format(items);
return formatted === input;
}
Хотя такой подход строг, он позволяет проверять ввод, соответствующий конкретному UI-формату.
Общий принцип использования Intl API в валидации заключается в обратной проверке:
Этот подход исключает необходимость поддерживать множество регулярных выражений для разных локалей и переносит ответственность за корректность формата на стандартную библиотеку.
В реальных данных встречаются неоднозначные ситуации:
1,234.56 vs
1.234,56)Intl API не выполняет парсинг таких строк напрямую, но предоставляет инструменты для построения детерминированной проверки через форматирование и декомпозицию.
Ключевая особенность подхода на основе Intl заключается в разделении:
Валидация перестаёт быть проверкой синтаксиса и становится проверкой эквивалентности между эталонным представлением и пользовательским вводом в рамках заданной локали.