Локализация сообщений

Библиотека Zod использует декларативную модель описания схем данных и формирует ошибки валидации через унифицированную структуру ZodError. Локализация сообщений в этой системе строится не как отдельный слой, а как подмена генерации текстов на этапе формирования ошибок.


Базовая модель ошибок Zod

Каждое нарушение схемы преобразуется в объект ошибки, содержащий стандартизированное описание:

  • code — тип ошибки (invalid_type, too_small, too_big, invalid_string и др.)
  • path — путь к полю в структуре данных
  • message — текстовое описание
  • expected / received — контекстные значения (в зависимости от типа ошибки)

Пример структуры:

import { z } from "zod";

const schema = z.object({
  age: z.number().min(18)
});

schema.parse({ age: 10 });

Результирующий ZodError содержит массив issues:

[
  {
    code: "too_small",
    minimum: 18,
    type: "number",
    inclusive: true,
    message: "Number must be greater than or equal to 18",
    path: ["age"]
  }
]

Именно поле message является точкой локализации.


ZodError и структура issues

Объект ZodError агрегирует все ошибки валидации:

  • единый массив issues
  • детализированная информация по каждому узлу дерева схемы
  • сохранение вложенной структуры через path

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


errorMap как механизм глобальной локализации

Основной инструмент кастомизации сообщений — errorMap.

Он представляет собой функцию:

type ZodErrorMap = (
  issue: ZodIssue,
  ctx: { defaultError: string; dat a: any }
) => { message: string };

Глобальная установка:

import { z, setErrorMap } from "zod";

setErrorMap((issue, ctx) => {
  switch (issue.code) {
    case "invalid_type":
      return { message: "Некорректный тип данных" };

    case "too_small":
      return { message: "Значение слишком мало" };

    default:
      return { message: ctx.defaultError };
  }
});

После установки все схемы используют единый словарь сообщений.


Контекстная локализация через errorMap

Контекст ctx содержит системное сообщение, которое Zod генерирует по умолчанию. Его использование позволяет строить гибридную модель локализации:

setErrorMap((issue, ctx) => {
  if (issue.code === "invalid_string") {
    if (issue.validation === "email") {
      return { message: "Некорректный формат email" };
    }
  }

  return { message: ctx.defaultError };
});

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


Локализация на уровне схемы

Помимо глобального errorMap, поддерживается локальная переопределяемая логика через refine и superRefine.

const passwordSchema = z.string().superRefine((val, ctx) => {
  if (val.length < 8) {
    ctx.addIssue({
      code: "custom",
      message: "Пароль должен содержать минимум 8 символов"
    });
  }
});

Здесь локализация фактически инлайнится в бизнес-логику.


Использование словарей переводов

При интеграции с системой интернационализации обычно выделяется словарь:

const messages = {
  ru: {
    required: "Поле обязательно",
    invalidType: "Некорректный тип",
    min: (n: number) => `Минимальное значение: ${n}`
  },
  en: {
    required: "Field is required",
    invalidType: "Invalid type",
    min: (n: number) => `Minimum value: ${n}`
  }
};

И подключение через errorMap:

const locale = "ru";

setErrorMap((issue, ctx) => {
  const dict = messages[locale];

  if (issue.code === "too_small" && issue.type === "number") {
    return { message: dict.min(issue.minimum) };
  }

  return { message: ctx.defaultError };
});

Типизация локализованных сообщений

Типизация позволяет избежать рассинхронизации ключей переводов:

type Locale = "ru" | "en";

type MessageKeys = "required" | "invalidType" | "min";

const messages: Record<Locale, Record<MessageKeys, string>> = {
  ru: {
    required: "Поле обязательно",
    invalidType: "Некорректный тип",
    min: "Минимальное значение"
  },
  en: {
    required: "Required field",
    invalidType: "Invalid type",
    min: "Minimum value"
  }
};

Это позволяет связывать ошибки Zod с типизированными ключами.


Локализация вложенных структур

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

const schema = z.object({
  user: z.object({
    email: z.string().email()
  })
});

Ошибка:

path: ["user", "email"]

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

setErrorMap((issue) => {
  const field = issue.path.join(".");

  if (field === "user.email") {
    return { message: "Ошибка email пользователя" };
  }

  return { message: "Ошибка валидации" };
});

Локализация union и discriminated union

Union-валидации формируют множественные ошибки:

const schema = z.union([
  z.string(),
  z.number()
]);

При несоответствии типов Zod генерирует агрегированные issues.

Локализация требует анализа unionErrors:

setErrorMap((issue, ctx) => {
  if (issue.code === "invalid_union") {
    return { message: "Значение не соответствует ни одному допустимому типу" };
  }

  return { message: ctx.defaultError };
});

Интеграция с внешними системами i18n

При использовании систем наподобие ICU или i18next логика смещается в сторону ключей:

setErrorMap((issue) => {
  return {
    message: i18n.t(`validation.${issue.code}`)
  };
});

Структура переводов:

{
  "validation": {
    "invalid_type": "Некорректный тип данных",
    "too_small": "Значение меньше допустимого"
  }
}

Переиспользование локализационных стратегий

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

function createErrorMap(dict: any) {
  return (issue: any, ctx: any) => {
    const message = dict[issue.code];
    return {
      message: typeof message === "function"
        ? message(issue)
        : message || ctx.defaultError
    };
  };
}

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


Особенности поведения defaultError

ctx.defaultError формируется Zod на основе встроенной логики. Он зависит от:

  • типа примитива
  • ограничения (min, max, length)
  • режима валидации

Использование fallback-механизма предотвращает потерю сообщений при неполной локализации.


Комбинирование refine и глобальной локализации

При смешанном подходе возможна конкуренция сообщений:

  • errorMap — глобальные правила
  • ctx.addIssue — локальные переопределения
  • встроенные сообщения схем

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