Библиотека class-validator предоставляет механизм валидации объектов через декораторы и функциональные правила, а система сообщений об ошибках в ней изначально ориентирована на строковые шаблоны. При переходе к многоязычным приложениям основная сложность возникает не в самой валидации, а в привязке текстов ошибок к локалям без нарушения архитектуры доменной модели.
Репозиторий: class-validator GitHub
Каждое правило валидации в class-validator при провале генерирует объект ошибки, содержащий ключевые поля:
property — имя поляconstraints — набор сообщенийvalue — переданное значениеtarget — объект-носительconstraints является центральной частью, поскольку
именно здесь формируются строки сообщений.
Пример стандартного поведения:
import { IsEmail } from "class-validator";
class User {
@IsEmail()
email: string;
}
При ошибке:
{
"property": "email",
"constraints": {
"isEmail": "email must be an email"
}
}
Строка "email must be an email" формируется либо
библиотекой, либо пользовательским сообщением.
Каждый декоратор поддерживает опцию message, которая
позволяет переопределить текст ошибки:
import { IsEmail } from "class-validator";
class User {
@IsEmail({}, { message: "Invalid email format" })
email: string;
}
Однако ключевой момент для интернационализации заключается в том, что
message может быть функцией.
@IsEmail({}, {
message: (args) => {
return `Field ${args.property} is not valid`;
}
})
email: string;
Аргумент args содержит:
propertyvalueconstraintstargetNameЭто открывает возможность динамической подстановки переводов.
Самый прямой способ интернационализации — использование функции, возвращающей перевод из словаря.
const messages = {
en: {
emailInvalid: "Invalid email address",
},
ru: {
emailInvalid: "Некорректный email адрес",
}
};
let locale = "ru";
function t(key: string) {
return messages[locale][key];
}
Использование в декораторе:
@IsEmail({}, {
message: () => t("emailInvalid")
})
email: string;
Этот подход имеет ограничение: локаль фиксируется в момент объявления класса, а не в момент выполнения валидации.
Для реальной интернационализации локаль должна определяться во время запроса или выполнения логики.
Типовой подход:
class I18nService {
constructor(private locale: string) {}
t(key: string) {
const dict = {
ru: { emailInvalid: "Некорректный email" },
en: { emailInvalid: "Invalid email" }
};
return dict[this.locale][key];
}
}
Интеграция через фабрику сообщений:
function createValidators(i18n: I18nService) {
return {
emailMessage: () => i18n.t("emailInvalid")
};
}
Использование:
@IsEmail({}, {
message: (args) => validators.emailMessage()
})
email: string;
class-validator выполняет валидацию через функцию
validate() или validateOrReject():
import { validate } from "class-validator";
const user = new User();
user.email = "invalid";
const errors = await validate(user);
Ключевая особенность: библиотека не хранит контекст запроса, поэтому:
На практике интернационализация почти всегда строится поверх внешних решений:
Пример с i18next:
import i18next from "i18next";
i18next.init({
lng: "ru",
resources: {
ru: {
translation: {
email_invalid: "Некорректный email"
}
}
}
});
Использование:
@IsEmail({}, {
message: () => i18next.t("email_invalid")
})
email: string;
Более масштабируемый подход — отказ от строк в пользу кодов ошибок.
constraints: {
isEmail: "EMAIL_INVALID"
}
Затем на уровне API выполняется трансформация:
const errorMap = {
EMAIL_INVALID: "Некорректный email",
en: {
EMAIL_INVALID: "Invalid email"
}
};
Такой подход позволяет:
Типовая архитектура:
Пример трансформации:
function mapErrors(errors, locale) {
return errors.map(err => {
const constraints = err.constraints;
const translated = Object.fromEntries(
Object.entries(constraints).map(([key]) => {
return [key, translate(key, locale)];
})
);
return {
property: err.property,
messages: translated
};
});
}
В сложных системах создаётся фабрика валидаторов:
export function createValidationMessages(t: (key: string) => string) {
return {
isEmail: () => t("validation.email"),
isRequired: () => t("validation.required")
};
}
И затем:
@IsNotEmpty({
message: () => messages.isRequired()
})
email: string;
Этот подход позволяет:
Ограничение class-validator заключается в том, что декораторы статичны. Это означает:
Решения:
function createUserDto(t) {
class UserDto {
@IsEmail({}, {
message: () => t("email_invalid")
})
email;
}
return UserDto;
}
Использование:
const UserDtoRu = createUserDto((k) => ruDict[k]);
const UserDtoEn = createUserDto((k) => enDict[k]);
В серверных приложениях локаль часто определяется middleware:
function localeMiddleware(req, res, next) {
req.locale = req.headers["accept-language"] || "en";
next();
}
Далее сервис переводов использует req.locale, но важно
не пытаться передать req в декораторы напрямую — это
нарушает изоляцию слоя валидации.
Внутренняя структура constraints может содержать несколько ключей:
{
"isEmail": "email must be an email",
"minLength": "email is too short"
}
При интернационализации важно нормализовать ключи:
isEmail → validation.email.invalidminLength → validation.string.minЭто позволяет строить иерархические словари переводов.
Для предотвращения расхождений используется типизация:
type ValidationKey =
| "validation.email.invalid"
| "validation.required";
Это уменьшает риск несоответствия между кодом и переводами.
В связке с class-transformer часто возникает необходимость:
Порядок критичен, поскольку ошибки зависят от финального вида объекта.
Интернационализация в class-validator строится вокруг трёх моделей:
message как runtime-слояКаждая модель решает отдельный класс проблем: статичность декораторов, отсутствие контекста запроса и необходимость централизованного управления переводами.