Локализация ошибок

Структура ошибок валидации

В библиотеке Superstruct каждая ошибка валидации представлена объектом StructError. Этот объект содержит полную информацию о том, где именно произошла ошибка и почему проверка не была пройдена.

Ключевые поля ошибки:

  • path — путь до некорректного значения (вложенные поля представлены массивом ключей)
  • value — фактическое значение, которое не прошло проверку
  • type — тип структуры, которая не была удовлетворена
  • message — текстовое описание ошибки
  • branch — полная цепочка значений от корня до ошибки
  • failure — генератор всех причин несоответствия

Пример:

import { object, string, validate } from "superstruct";

const User = object({
  name: string(),
});

const [error] = validate({ name: 123 }, User);

console.log(error);

Результат будет содержать структурированное описание, где path укажет на ["name"], а value будет равен 123.


Проблема локализации сообщений

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

Типичная проблема:

Expected a string, but received: 123

В реальных интерфейсах требуется:

  • перевод на язык пользователя
  • адаптация формулировки под контекст приложения
  • скрытие технических деталей (например, type names)

Базовый механизм кастомизации ошибок

Superstruct позволяет переопределять сообщения на уровне каждой структуры через функцию message.

import { string } from "superstruct";

const Name = string();

const NameWithMessage = string([
  (value) => typeof value === "string" || "Имя должно быть строкой",
]);

Такой подход позволяет локализовать сообщения на уровне конкретного валидатора, но становится неудобным при большом количестве полей.


Централизованная локализация через обработку StructError

Основной способ локализации — постобработка ошибок после валидации.

import { validate } from "superstruct";

const [error] = validate(data, UserStruct);

if (error) {
  const localized = localizeError(error);
}

Функция локализации:

function localizeError(error) {
  const dictionary = {
    string: "Ожидается строка",
    number: "Ожидается число",
    required: "Поле обязательно",
  };

  return {
    path: error.path.join("."),
    message: dictionary[error.type] || "Ошибка валидации",
    value: error.value,
  };
}

Такой подход отделяет бизнес-логику валидации от слоя представления.


Работа с вложенными структурами

При использовании вложенных объектов путь ошибки становится составным:

const Profile = object({
  user: object({
    name: string(),
  }),
});

Если ошибка возникает в name, путь будет:

["user", "name"]

Для локализации это важно, поскольку позволяет:

  • строить локализованные ключи
  • отображать ошибки рядом с UI-полями
  • поддерживать формы любой вложенности

Пример преобразования:

function formatPath(path) {
  return path.join(".");
}

Результат: user.name


Карта переводов (i18n подход)

Расширенный способ локализации основан на словаре переводов:

const messages = {
  ru: {
    string: "Ожидается строка",
    number: "Ожидается число",
    boolean: "Ожидается логическое значение",
  },
  en: {
    string: "Expected a string",
    number: "Expected a number",
    boolean: "Expected a boolean",
  },
};

Функция выбора языка:

function translateError(error, locale = "ru") {
  const dict = messages[locale];
  return dict[error.type] || "Ошибка валидации";
}

Такой слой позволяет масштабировать систему на множество языков без изменения схем.


Обогащение ошибок контекстом

Стандартная ошибка Superstruct не содержит пользовательских подписей полей. Для реальных приложений требуется добавление контекста:

const fieldLabels = {
  name: "Имя пользователя",
  age: "Возраст",
};

Комбинированная локализация:

function enhanceError(error) {
  const field = error.path[error.path.length - 1];
  const label = fieldLabels[field] || field;

  return `${label}: ${translateError(error)}`;
}

Результат:

Имя пользователя: Ожидается строка

Группировка ошибок

При массовой валидации удобно агрегировать ошибки по полям:

function groupErrors(errors) {
  return errors.reduce((acc, err) => {
    const key = err.path.join(".");
    acc[key] = acc[key] || [];
    acc[key].push(err.message);
    return acc;
  }, {});
}

Это упрощает интеграцию с формами и UI-библиотеками.


Кастомные структуры и локализованные сообщения

При создании пользовательских структур можно сразу внедрять локализацию:

import { define } from "superstruct";

const PositiveNumber = define("PositiveNumber", (value) => {
  if (typeof value !== "number") return "Ожидается число";
  if (value <= 0) return "Число должно быть больше нуля";
  return true;
});

Такой подход переносит ответственность за сообщение прямо в валидатор.


Перехват и трансформация ошибок

В сложных системах часто используется единый трансформер:

function transformStructError(error) {
  return {
    field: error.path.join("."),
    code: error.type,
    message: translateError(error),
    raw: error.value,
  };
}

Это позволяет унифицировать формат ошибок между разными модулями приложения.


Поведение при глубокой вложенности и массивных структурах

При работе с массивами пути ошибок включают индексы:

["users", 0, "email"]

Локализация должна учитывать числовые сегменты:

function formatPath(path) {
  return path
    .map((segment) => (typeof segment === "number" ? `[${segment}]` : segment))
    .join(".");
}

Результат:

users[0].email

Разделение технических и пользовательских сообщений

В системах с Superstruct важно разделять два уровня сообщений:

  • технические (для разработчиков)
  • пользовательские (для интерфейса)

Технические:

Expected string, received number

Пользовательские:

Введите текстовое значение

Такое разделение позволяет не смешивать внутреннюю диагностику и внешний UX.


Расширяемая модель локализации

Гибкая архитектура обычно включает три слоя:

  1. Struct layer — определение схем
  2. Validation layer — получение StructError
  3. Localization layer — преобразование ошибок в текст

Пример пайплайна:

const [error] = validate(data, Schema);

if (error) {
  const localized = transformStructError(error);
  showErrors(localized);
}

Поддержка динамических сообщений

Иногда сообщение зависит от значения:

(value) => value.length > 5 || "Максимальная длина — 5 символов";

Для локализации:

(value) =>
  value.length > 5 ||
  `Слишком длинное значение: ${value.length}`;

Унификация форматов ошибок

Для масштабных приложений формируется единый контракт:

{
  field: string,
  message: string,
  code: string,
  value: any
}

Superstruct предоставляет данные, но финальная форма всегда определяется уровнем локализации.


Особенности проектирования систем локализации ошибок

При интеграции Superstruct в крупные системы важно учитывать:

  • неизменяемость исходных ошибок
  • детерминированность перевода
  • отсутствие потерь информации при трансформации
  • возможность обратной трассировки к исходной StructError

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