Библиотека Zod использует декларативную модель описания схем данных и
формирует ошибки валидации через унифицированную структуру
ZodError. Локализация сообщений в этой системе строится не
как отдельный слой, а как подмена генерации текстов на этапе
формирования ошибок.
Каждое нарушение схемы преобразуется в объект ошибки, содержащий стандартизированное описание:
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 агрегирует все ошибки валидации:
issuespathЭто позволяет применять локализацию централизованно, а не на уровне каждой схемы.
Основной инструмент кастомизации сообщений —
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 };
}
});
После установки все схемы используют единый словарь сообщений.
Контекст 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-валидации формируют множественные ошибки:
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 };
});
При использовании систем наподобие 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
};
};
}
Такой подход позволяет переключать языки без изменения схем.
ctx.defaultError формируется Zod на основе встроенной
логики. Он зависит от:
min, max,
length)Использование fallback-механизма предотвращает потерю сообщений при неполной локализации.
При смешанном подходе возможна конкуренция сообщений:
errorMap — глобальные правилаctx.addIssue — локальные переопределенияПриоритет обычно определяется порядком генерации issues,
что требует консистентной стратегии построения переводов, чтобы избежать
противоречий в пользовательских сообщениях.