Валидация пользовательского ввода

Валидация данных в многоязычных приложениях перестаёт быть формальной проверкой соответствия регулярным выражениям и превращается в задачу согласования пользовательского ввода с локальными правилами представления чисел, дат, строк и списков. Библиотека Intl в JavaScript предоставляет стандартизированный слой для работы с культурно-зависимыми форматами, позволяя строить валидацию, основанную на реальных правилах отображения данных, а не на предположениях о единственном универсальном формате.

Ключевой принцип заключается в том, что корректность ввода определяется не синтаксисом строки как таковой, а её соответствием форматам конкретной локали. Это особенно критично для чисел, дат, валют и текстовых сравнений.


Числовая валидация через Intl.NumberFormat

Обычные регулярные выражения для чисел быстро становятся неустойчивыми при переходе между локалями: разделители дробной и тысячной части, использование пробелов, неразрывных пробелов и разных символов запятой и точки создают множество вариаций одного и того же значения.

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

Более надёжный способ — анализ структуры числа через formatToParts:

const nf = new Intl.NumberFormat("fr-FR");

nf.formatToParts(12345.6);

Результат содержит структурированные токены:

  • integer
  • group
  • decimal
  • fraction

Это позволяет валидировать ввод не как строку, а как последовательность допустимых компонентов:

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));
}

Валидация дат с Intl.DateTimeFormat

Парсинг дат в JavaScript традиционно опирается на Date.parse, который не гарантирует стабильное поведение для локализованных строк. Intl API позволяет перейти к обратному подходу: проверка соответствия формату, а не разбор строки.

const dtf = new Intl.DateTimeFormat("en-GB");

dtf.format(new Date(2024, 0, 15));
// "15/01/2024"

Проверка структуры через formatToParts

const dtf = new Intl.DateTimeFormat("ru-RU");

dtf.formatToParts(new Date());

Типичная структура включает:

  • day
  • month
  • year
  • literal (разделители)

Валидация пользовательского ввода даты

Подход основан на сопоставлении структуры:

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

Валидация строк часто сводится к проверке равенства или порядка. Однако простое сравнение через === не учитывает:

  • диакритические знаки
  • регистр
  • национальные особенности сортировки

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-представлением.


Нормализация Unicode и границы символов

Текстовый ввод может содержать визуально одинаковые, но технически разные строки:

  • комбинированные символы
  • составные диакритики
  • различные формы Unicode normalization (NFC/NFD)

Нормализация перед валидацией

function normalize(input) {
  return input.normalize("NFC");
}

Без нормализации сравнение строк становится ненадёжным даже в пределах одной локали.


Сегментация текста через Intl.Segmenter

Валидация длины строки и границ слов требует корректного разбиения текста. Простое использование .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

Валидация списков часто сводится к проверке разделителей (",", ";"), что ломается при локализации.

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. значение преобразуется в тип (число, дата, список)
  2. форматируется через Intl
  3. сравнивается с исходным вводом
  4. при необходимости анализируется через formatToParts

Этот подход исключает необходимость поддерживать множество регулярных выражений для разных локалей и переносит ответственность за корректность формата на стандартную библиотеку.


Граничные случаи локализованного ввода

В реальных данных встречаются неоднозначные ситуации:

  • смешение разделителей (1,234.56 vs 1.234,56)
  • неразрывные пробелы как разделители тысяч
  • альтернативные символы валют
  • устаревшие календарные форматы
  • частично введённые даты

Intl API не выполняет парсинг таких строк напрямую, но предоставляет инструменты для построения детерминированной проверки через форматирование и декомпозицию.


Согласование визуального и семантического представления

Ключевая особенность подхода на основе Intl заключается в разделении:

  • семантики (число, дата, список)
  • представления (локализованная строка)

Валидация перестаёт быть проверкой синтаксиса и становится проверкой эквивалентности между эталонным представлением и пользовательским вводом в рамках заданной локали.