Open chanlito opened 6 years ago
Maybe you could write your own function that is similar to Validator Functions definition . Then in that custom function set message and call corresponding validation function from class-validator. Something like
var CustomMatches = (pattern: RegExp, validationOptions?: ValidationOptions) => {
validationOptions = {};
validationOptions.message = "Message";
return Matches(pattern, validationOptions);
}
Hey, this is on my list, I will add it.
It would be really nice and allow for i18n of the error messages. Really useful while reusing validation classes on backend (english) and on frontend (multilanguage) 😉
@chanlito as work around I do it by delegating new decorators to existing ones:
export const IsRequired: Function = () => IsNotEmpty({ message: () => translate('validation.required') });
@mendrik how would you create that translate decorator? Which library do you use? I see that there is nestjs-i18n, but it does not provide you that kind of validator decorator.
@bruno-lombardi translate
is not a decorator in example above, it's just a regular function that returns translated string by given key.
Trouvble is I want all the constraints back not just key code. You cant get the contraints in the error for rendering later.
Any planning on the road map to add this feature? I would like to integrate i18next (i18next-express-middleware) with class-validator, but I don't know how to do this
NOTE: this only works in versions < 0.12!
As a workaround, we monkey patch ValidationTypes.getMessage()
:
export function patchClassValidatorI18n() {
const orig = ValidationTypes.getMessage.bind(ValidationTypes);
ValidationTypes.getMessage = (type: string, isEach: boolean): string | ((args: ValidationArguments) => string) => {
switch (type) {
case ValidationTypes.IS_NOT_EMPTY:
return i18next.t('msg.inputRequired');
case ValidationTypes.MAX_LENGTH:
return i18next.t('validation.inputLength.tooLong', {
threshold: '$constraint1'
});
// return the original (English) message from class-validator when a type is not handled
default:
return orig(type, isEach);
}
};
}
Then call patchClassValidatorI18n
at the start of your entry-point (e.g. in main.ts
, test-setup.ts
, ..).
We use i18next for translations, but you can simple replace i18next.t
with your own custom translation function.
@tmtron the only thing to note with your fix is that if the function name was to ever change in a future version, you would have to update your reference. Not a big deal for most but something to keep in mind for anyone looking to implement your solution 😄
@ChrisKatsaras since we use typescript, such a change will cause a compilation error. In addition to that we have unit tests for each message. which would fail (in the unlikely case, that the typedefs are wrong): so there is nothing to worry about...
@tmtron fair enough! Thanks for clarifying
Hey, this is on my list, I will add it.
Hey, Did you add this? When are you planning to add? There is a PR from @HonoluluHenk
I think for translations on the backend (node.js) we need to pass some context (e.g. language/locale of the current user) to the message function.
e.g. ValidatorOptions
should get an invocationContext
and this context must be passed via the ValidationArguments
to the message function
note, that this invocationContext
is different than the existing ValidationArguments.context
, because this can be different for each validate*
call
use case: e.g. some automatic task on the backend which sends emails to the users - each user may have a different locale/language. We cannot simply set a global variable due to the async nature of nodejs.
On backend, it's important that i18n should NOT be handled in message function directly
class Post {
@Length(10, 20, {
message: (args: ValidationArguments) => i18n.t("LengthTranslation", args),
})
title!: string;
The above is wrong on backend because when you validate an object, it's undetermined that WHO will see the error message.
A better way is only doing the translation when you really know who will see the error messages. This means that you translate in controllers, GraphQL formatError, sending push notifications or even socket.io emit.
But one requirement to make translation easier is that the error should carry enough information to translate.
When you do validate(post).then((errors) => {
, errors is ValidationError[]
export declare class ValidationError {
target?: Object;
property: string;
value?: any;
constraints?: {
[type: string]: string;
};
children: ValidationError[];
contexts?: {
[type: string]: any;
};
}
ValidationError actually carries many information for translation, but it still lacks for specific translation keys.
The solution is
class Post {
@Length(10, 20, {
context: {
i18n: "LengthKey",
},
})
title!: string;
}
validate(post).then((errors) => {
const msgs = errors
.map((e) => {
const collect = [];
for (const key in e.contexts) {
collect.push(
i18next.t(e.contexts[key].i18n, {
target: e.target,
property: e.property,
value: e.value,
})
);
}
return collect;
})
.flat();
However, it's unfortunate that ValidationError's constraints is message strings, not args: ValidationArguments
.
I am disappointed that defaultMessage
has to return string as per the ValidatorConstraintInterface
interface. Every API should return objects representing an error, not (only) human-readable sentences. length: { min: 1, max: 10 }
is way more useful to developers over (only) "the length must be between 1 and 10"
.
THEN this object should be used for a template syntax such as "the length must be between $min and $max"
. This library provides default messages, but doesn't allow changing the default message globally, which means it's stuck at the wording, casing and language that the developer has chosen.
Given that this is a validation library, error reporting should be its top priority. @NoNameProvided, can we get an update on this? The last word from team members is from 2018; would appreciate to know if I should expect this to be addressed soon.
Are there any news on this? My whole validation setup is stuck on old class-validator version because there's no solution to manage error messages. I saw that over here there already is a open pull request implementing a basic solution but nothing has happened since. https://github.com/typestack/class-validator/pull/238
My simple workaround
import { ValidationOptions } from 'class-validator';
import snakeCase from 'lodash/snakeCase';
import i18n from 'i18next';
export const ValidateBy = (
validator: (...args: any[]) => PropertyDecorator,
args: any[] = [],
): PropertyDecorator => {
args.push(
<ValidationOptions>
{
message: (validationArgs) => i18n.t(
'validation:' + snakeCase(validator.name),
validationArgs,
),
},
);
return validator(...args);
};
then use
@ValidateBy(IsNotEmpty)
@ValidateBy(MinLength, [6])
readonly password: string;
validation.json
{
"is_not_empty": "{{property}} should not be empty",
"min_length": "{{property}} must be longer than or equal to {{constraints.0}} characters",
}
i create PR basic support i18n: https://github.com/typestack/class-validator/pull/730
example usages:
import { IsOptional, Equals, Validator, I18N_MESSAGES } from 'class-validator';
class MyClass {
@IsOptional()
@Equals('test')
title: string = 'bad_value';
}
Object.assign(I18N_MESSAGES, {
'$property must be equal to $constraint1': '$property должно быть равно $constraint1',
});
const model = new MyClass();
validator.validate(model).then(errors => {
console.log(errors[0].constraints);
// out: title должно быть равно test
});
sharing some agreeably terse message override examples, compatible with latest v0.13.1:
import {
ValidationOptions, buildMessage, ValidateBy,
IsNotEmpty as _IsNotEmpty,
MaxLength as _MaxLength,
Min as _Min,
Max as _Max
} from "class-validator";
//lookup existing message interpolation patterns in the source:
//https://github.com/typestack/class-validator/blob/develop/src/decorator/number/Max.ts
export const IsNotEmpty = (validationOptions?: ValidationOptions): PropertyDecorator =>_IsNotEmpty({...validationOptions, message: "Required"});
export const MaxLength = (max: number, validationOptions?: ValidationOptions): PropertyDecorator =>_MaxLength(max, {...validationOptions, message: "$constraint1 chars max" });
export const Min = (minValue: number, validationOptions?: ValidationOptions): PropertyDecorator =>_Min(minValue, {...validationOptions, message: ">= $constraint1"});
export const Max = (maxValue: number, validationOptions?: ValidationOptions): PropertyDecorator =>_Max(maxValue, {...validationOptions, message: `$constraint1 max`});
Seriously, this functionality hasn't still gone out? I wonder why nestJs chooses this library...
Well, maybe a tricky way:
// OverrideOptions.ts
import { ValidationOptions, getMetadataStorage } from 'class-validator';
import type { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata';
const OverrideOptions = (options: ValidationOptions): PropertyDecorator => {
return (object, propertyName) => {
const storage = getMetadataStorage();
const metadataList: ValidationMetadata[] = Reflect.get(
storage,
'validationMetadatas',
);
(metadataList || [])
.filter((v) => v.propertyName === propertyName)
.forEach((v) => {
Object.assign(v, options);
});
};
};
export default OverrideOptions;
usage:
import { Type } from 'class-transformer';
import { MinLength, MaxLength, IsNotEmpty, validate } from 'class-validator';
import OverrideOptions from './OverrideOptions';
class TestEntity {
@OverrideOptions({ message: 'use this message' })
@MaxLength(20)
@MinLength(10)
@IsNotEmpty()
@Type(() => String)
readonly testName!: string;
}
const entity = new TestEntity();
entity.name = 'test';
validate(entity, { stopAtFirstError: true })
.then(() => {
// ...
})
Based on @kkoomen solution (https://github.com/ToonvanStrijp/nestjs-i18n/issues/97#issuecomment-826336477) I've modified it to be able to translate errors on front-end + to give me field name where the error should be visible.
The code
import {
UnprocessableEntityException,
ValidationError,
ValidationPipe,
ValidationPipeOptions,
} from '@nestjs/common';
const classValidationPatterns = [
'$IS_INSTANCE decorator expects and object as value, but got falsy value.',
'$property is not a valid decimal number.',
'$property must be a BIC or SWIFT code',
'$property must be a boolean string',
'$property must be a boolean value',
'$property must be a BTC address',
'$property must be a credit card',
'$property must be a currency',
'$property must be a data uri format',
'$property must be a Date instance',
'$property must be a Firebase Push Id',
'$property must be a hash of type (.+)',
'$property must be a hexadecimal color',
'$property must be a hexadecimal number',
'$property must be a HSL color',
'$property must be a identity card number',
'$property must be a ISSN',
'$property must be a json string',
'$property must be a jwt string',
'$property must be a latitude string or number',
'$property must be a latitude,longitude string',
'$property must be a longitude string or number',
'$property must be a lowercase string',
'$property must be a MAC Address',
'$property must be a mongodb id',
'$property must be a negative number',
'$property must be a non-empty object',
'$property must be a number conforming to the specified constraints',
'$property must be a number string',
'$property must be a phone number',
'$property must be a port',
'$property must be a positive number',
'$property must be a postal code',
'$property must be a Semantic Versioning Specification',
'$property must be a string',
'$property must be a valid domain name',
'$property must be a valid enum value',
'$property must be a valid ISO 8601 date string',
'$property must be a valid ISO31661 Alpha2 code',
'$property must be a valid ISO31661 Alpha3 code',
'$property must be a valid phone number',
'$property must be a valid representation of military time in the format HH:MM',
'$property must be an array',
'$property must be an EAN (European Article Number)',
'$property must be an email',
'$property must be an Ethereum address',
'$property must be an IBAN',
'$property must be an instance of (.+)',
'$property must be an integer number',
'$property must be an ip address',
'$property must be an ISBN',
'$property must be an ISIN (stock/security identifier)',
'$property must be an ISRC',
'$property must be an object',
'$property must be an URL address',
'$property must be an UUID',
'$property must be base32 encoded',
'$property must be base64 encoded',
'$property must be divisible by (.+)',
'$property must be empty',
'$property must be equal to (.+)',
'$property must be locale',
'$property must be longer than or equal to (\\S+) and shorter than or equal to (\\S+) characters',
'$property must be longer than or equal to (\\S+) characters',
'$property must be magnet uri format',
'$property must be MIME type format',
'$property must be one of the following values: (\\S+)',
'$property must be RFC 3339 date',
'$property must be RGB color',
'$property must be shorter than or equal to (\\S+) characters',
'$property must be shorter than or equal to (\\S+) characters',
'$property must be uppercase',
'$property must be valid octal number',
'$property must be valid passport number',
'$property must contain (\\S+) values',
'$property must contain a (\\S+) string',
'$property must contain a full-width and half-width characters',
'$property must contain a full-width characters',
'$property must contain a half-width characters',
'$property must contain any surrogate pairs chars',
'$property must contain at least (\\S+) elements',
'$property must contain not more than (\\S+) elements',
'$property must contain one or more multibyte chars',
'$property must contain only ASCII characters',
'$property must contain only letters (a-zA-Z)',
'$property must contain only letters and numbers',
'$property must match (\\S+) regular expression',
'$property must not be greater than (.+)',
'$property must not be less than (.+)',
'$property should not be empty',
'$property should not be equal to (.+)',
'$property should not be null or undefined',
'$property should not be one of the following values: (.+)',
'$property should not contain (\\S+) values',
'$property should not contain a (\\S+) string',
"$property's byte length must fall into \\((\\S+), (\\S+)\\) range",
"All $property's elements must be unique",
'each value in ',
'maximal allowed date for $property is (.+)',
'minimal allowed date for $property is (.+)',
'nested property $property must be either object or array',
];
/**
* The class-validator package does not support i18n and thus we will
* translate the error messages ourselves.
*/
function translateErrors(validationErrors: ValidationError[]) {
const errors = validationErrors.map((error: ValidationError): string[] => Object.keys(error.constraints).map((key: string): any => { /*
Real type:
{
field: string;
txt: string;
params: {
[key: string]: any;
};
}
*/
let match: string[] | null;
let constraint: string;
// Find the matching pattern.
for (const validationPattern of classValidationPatterns) {
const pattern = validationPattern.replace('$', '\\$');
constraint = error.constraints[key].replace(error.property, '$property');
match = new RegExp(pattern, 'g').exec(constraint);
if (match) {
break;
}
}
// Replace the constraints values back to the $constraintX words.
let i18nKey = constraint;
const replacements = { property: error.property };
if (match) {
for (let i = 1; i < match.length; i += 1) {
i18nKey = i18nKey.replace(match[i], `{{constraint${i}}}`);
replacements[`constraint${i}`] = match[i];
}
}
// Get the i18n text.
return {
field: error.property,
txt: i18nKey.replace('$property', '{{property}}'),
params: replacements,
};
}));
const errorsFlattened = errors.reduce((data: string[], errors) => {
data.push(...errors);
return data;
}, []);
return new UnprocessableEntityException(errorsFlattened);
}
export const createValidationPipeWithI18n = (options: ValidationPipeOptions) => new ValidationPipe({
...options,
exceptionFactory: translateErrors,
});
And the demo
validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { resolve } from 'path';
import { readFileSync } from 'fs';
import { plainToClass } from 'class-transformer';
import { validate } from "class-validator-multi-lang";
const EN_I18N_MESSAGES = JSON.parse(readFileSync(resolve(__dirname, '../../node_modules/class-validator-multi-lang/i18n/en.json')).toString());
const TR_I18N_MESSAGES = JSON.parse(readFileSync(resolve(__dirname, '../../node_modules/class-validator-multi-lang/i18n/tr.json')).toString());
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
strLang: String = "en";
async transform(value: any, { metatype }: ArgumentMetadata) {
value == "en" ? this.strLang = "en" : value == "tr" ? this.strLang = "tr" : "";
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object, { messages: this.strLang == "tr" ? TR_I18N_MESSAGES : EN_I18N_MESSAGES });
if (errors.length > 0) {
throw new BadRequestException(`${this.formatErrors(errors)}`);
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
private formatErrors(errors: any[]) {
return errors.map(err => {
for (let property in err.constraints) {
return err.constraints[property];
}
}).join(',')
}
}
I think it's better to do internationalization at the top level, means by calling validate
function or validateSync
, at first, all processes must be done and after that, internationalization in the last line of the function must be performed.
But, the settings should be flexible as much as possible, e.g. by overriding in the class as a new decorator
or as an extra argument
to the existing decorators.
validate
function, the validate
function can get a new option like i18n
with this shape:
validate(object, {
i18n: {
lang: "en", // it's important to handle each HTTP request separately
// The description of this option is at the end.
replaceValueByClassValidation: {
property: false,
value: true,
target: true,
constraints: true
},
// It seems a customizable transform function is needed. (e.g. this can be use to convert some property to count which i18n supports)
transformBeforeTranslation: (template: string) => template.replace(/\$\w+/, /* a replace function */}),
// this is the translator function
translator: (constraintKey, variables, configs) => {
i18next.t(constraintKey, {...variables, interpolation: configs});
}
}
})
decorator
point of view:
The most important thing to consider is that each decorator function (such as Min
, Max
, etc.) must have a special instance of i18n
object to override. Besides, they should access to the central dictionary of key-templates of other languages.
class Class {
@Max(100, {i18n: {/* overridden options */}})
@Min(0)
mathScore: number;
}
I18n
: It's a good idea to override the validate
settings with a new default value for a batch of decorators using a bottom-level new decorator:
class Class {
@Max(100, {i18n: {/* per decorator overridden options */}})
@Min(0)
@I18n({lang: 'fa', /* by calling this at the bottom-level, this option only overrides the validation options, but does not override the Min and Max values */})
property: number;
}
A description that why replaceByClassValidation
is needed: Consider this settings:
import {validate} from "class-validator";
class Class {
@Max(100, {i18n: {/* overridden options */}})
@Min(0)
mathScore: number;
}
const object = new Class();
object.mathScore = 102;
validate(object, {lang: 'fa-IR'}).catch(errors => { console.dir(errors) });
The output is like this:
{
target: /* object */;
property: 'mathScore';
value: 102;
constraints: {
// $property must not be greater than $constraint1
// English version
'Max': "mathScore must not be greater than 100",
// Persian version
'Max': "mathScore نباید بیشتر از 100 باشد.",
};
children: [];
}
But we don't want to use mathScore
in our languages. Because, sometimes, we want to render something in client-side. As a result, we can use replaceByClassValidation
:
validate(object, {lang: 'fa-IR', replaceByClassValidation: {property: false}}).catch(errors => {
console.dir(errors);
});
So, we have:
{
target: /* object */;
property: 'mathScore';
value: 102;
constraints: {
// $property must not be greater than $constraint1
// English version
'Max': "$property must not be greater than 100",
// Persian version
'Max': "$property نباید بیشتر از 100 باشد.",
};
children: [];
}
and we can replace it in the client side.
I think the support for internationalization is extremelly important. I would like to implement the translations on the client side. What I want is to receive the i18n key and the associated data and let the client side do the translations.
For example, for a property called username
annotated with @Length(3, 20)
, when the validation fails, I want to receive the following data:
{
"statusCode": 400,
"message": [
{"username": "validation.length", "data": {"min": 3, "max": 20}}
],
"error": "Bad Request"
}
This format is very useful for advanced and professional translations (see https://formatjs.io/).
Is there any workaround to support the above message format?
sharing some agreeably terse message override examples, compatible with latest v0.13.1:
import { ValidationOptions, buildMessage, ValidateBy, IsNotEmpty as _IsNotEmpty, MaxLength as _MaxLength, Min as _Min, Max as _Max } from "class-validator"; //lookup existing message interpolation patterns in the source: //https://github.com/typestack/class-validator/blob/develop/src/decorator/number/Max.ts export const IsNotEmpty = (validationOptions?: ValidationOptions): PropertyDecorator =>_IsNotEmpty({...validationOptions, message: "Required"}); export const MaxLength = (max: number, validationOptions?: ValidationOptions): PropertyDecorator =>_MaxLength(max, {...validationOptions, message: "$constraint1 chars max" }); export const Min = (minValue: number, validationOptions?: ValidationOptions): PropertyDecorator =>_Min(minValue, {...validationOptions, message: ">= $constraint1"}); export const Max = (maxValue: number, validationOptions?: ValidationOptions): PropertyDecorator =>_Max(maxValue, {...validationOptions, message: `$constraint1 max`});
Thank you so much for this solution, it's really concise and works great.
My workaround, based on @Beej126 solution:
export const IsOptional = _IsOptional;
export const Validate = _Validate;
function toJson(key: string, args: ValidationArguments, data?: any): string {
return JSON.stringify({key, field: args.property, data});
}
export function intlMsg(key: string, data?: any) {
return (args: ValidationArguments) => toJson(key, args, data);
}
export const IsNotEmpty = (opts?: ValidationOptions): PropertyDecorator =>
_IsNotEmpty({...opts, message: intlMsg('validation.isNotEmpty')});
export const IsDate = (opts?: ValidationOptions): PropertyDecorator =>
_IsDate({...opts, message: intlMsg('validation.isDate')});
export const IsIn = (values: readonly any[], opts?: ValidationOptions): PropertyDecorator =>
_IsIn(values, {...opts, message: intlMsg('validation.isIn', {values})});
export const IsEmail = (eOpts?: ValidatorJS.IsEmailOptions, opts?: ValidationOptions): PropertyDecorator =>
_IsEmail(eOpts, {...opts, message: intlMsg('validation.isEmail')});
export const Length = (min: number, max: number, opts?: ValidationOptions): PropertyDecorator =>
_Length(min, max, {...opts, message: intlMsg('validation.length', {min, max})});
export const MaxLength = (max: number, opts?: ValidationOptions): PropertyDecorator =>
_MaxLength(max, {...opts, message: intlMsg('validation.maxLength', {max})});
export const IsBoolean = (opts?: ValidationOptions): PropertyDecorator =>
_IsBoolean({...opts, message: intlMsg('validation.isBoolean')});
export const MaxDate = (date: Date, opts?: ValidationOptions): PropertyDecorator =>
_MaxDate(date, {...opts, message: intlMsg('validation.maxDate', {date})});
export const IsPastDate = (opts?: ValidationOptions): PropertyDecorator =>
_MaxDate(new Date(), {...opts, message: intlMsg('validation.isPastDate')});
I have created an exeption filter to convert the JSON string to JSON object before sending the error back to the client. So this is what I get now (cool!):
{
"status": 400,
"error": "Bad Request",
"messages": [
{
"key": "validation.length",
"field": "username",
"data": {
"min": 3,
"max": 20
}
},
{
"key": "validation.isPastDate",
"field": "birthDate"
},
{
"key": "validation.isValidLocale",
"field": "locale"
}
]
}
The UI can feed the key
and data
to the i18n functions. Example of i18n message templates using the ICU syntax:
"Hey, I would like to localize my validation error messages."
Every other validation library in existence: "Well, of course. Here you go!" class-validator: ¯\(ツ)/¯
Yes @juni0r , and this issue is opened since 2018, and until now... nothing :slightly_frowning_face:
I came from Laravel, and now I'm using NestJS. I can't believe that this stills an issue
I came from Laravel, and now I'm using NestJS. I can't believe that this stills an issue
NestJS module for adding translations to the application, with a pipe for translating validation errors
I came from Laravel, and now I'm using NestJS. I can't believe that this stills an issue
NestJS module for adding translations to the application, with a pipe for translating validation errors
Thanks @EndyKaufman for sharing, but I cant figured out how to implement a new message validation file and also, this would map a custom validation message?
https://github.com/EndyKaufman/class-validator-multi-lang/blob/i18n/i18n/en.json
Oh wow - so for my case, I need to put the json file in the node module folder and in that case I need to also add all spanish messages?
Perfect @EndyKaufman thanks!
@pmirand6 you may use exists Spanish locale https://github.com/EndyKaufman/class-validator-multi-lang/blob/i18n/i18n/es.json, if you found errors you may add correct translate in https://crowdin.com/project/class-validator
In Nestjs you can bootstrap the app with useGlobalPipes() and then set options for the class-validator globally. So I wanted the api to return validation errors in this format:
{
"statusCode": 400,
"message": [
{
"legalName": {
"isNotEmpty": "legalName should not be empty"
}
},
{
"organizationNo": {
"isNotUnique": "organizationNo is not unique"
}
},
{
"email": {
"isEmail": "email must be an email"
}
}
],
"error": "Bad Request"
}
What I had to do was to use the exceptionFactory option like this:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory: (errors) => {
const result = errors.map((error) => ({
[error.property]: error.constraints,
}));
return new BadRequestException(result);
},
}),
);
await app.listen(3000);
}
bootstrap();
This is a good idea, @john-wennstrom , but this doesn't allows for example, set errors messages with different languages and/or use user's language to display errors in a user language.
To solve this in NestJs I made something similar with @john-wennstrom's solution, but I've added an exception filter in my implementation as well.
I created a validation.exception.ts
file, a simple class to work as a custom exception.
import { ValidationError } from '@nestjs/common';
export class ValidationException {
constructor(public errors: ValidationError[]) {}
}
Then I used the exceptionFactory
from ValidationPipe
, like this:
app.useGlobalPipes(
new ValidationPipe({`
exceptionFactory: (errors) => new ValidationException(errors),
// ...
}),
);
I've also used @Beej126's solution as well to allow i18n to see validation parameters, like this:
import {
MinLength as _MinLength,
IsIn as _IsIn,
ValidationOptions,
} from 'class-validator';
export const MinLength = (
min: number,
validationOptions?: ValidationOptions,
): PropertyDecorator =>
_MinLength(min, {
...validationOptions,
context: { min },
// if you need to use context as well, you can do something like that: context: { ...(validationOptions.context || {}), min },
});
Allowing me to translate MinLength
error like this on i18n json file:
{
"minLength": "must be longer than or equal to {min} characters"
}
And finally I made a custom exception filter (in my case called i18n-validation-exception.filter.ts
), like this:
import {
ExceptionFilter,
Catch,
ArgumentsHost,
BadRequestException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { getI18nContextFromRequest } from 'nestjs-i18n';
import { ValidationException } from './validation.exception';
@Catch(ValidationException)
export class I18nValidationExceptionFilter implements ExceptionFilter {
private readonly i18nFile = 'validation';
catch(exception: ValidationException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const baseException = new BadRequestException(); // You don't need to use BadRequestException here as reference, but I preferred to do it like this
const i18n = getI18nContextFromRequest(request);
response.status(baseException.getStatus()).json({
statusCode: baseException.getStatus(),
message: baseException.getResponse(),
errors: exception.errors.map((error) => {
return {
property: error.property,
children: error.children,
constraints: Object.keys(error.constraints).map((constraint) =>
i18n.t(`${this.i18nFile}.${constraint}`, {
defaultValue: error.constraints[constraint],
args: {
...error,
...(error.contexts?.[constraint] || {}),
},
}),
),
};
}),
});
}
}
Hope it helps you as well, @netojose :)
Thanks, @pedrosodre :)
I've described two possible solutions here https://github.com/typestack/class-validator/issues/1758 which would both do not require major changes and would enable custom translations. I'd really love to have a solution for this issue since I do not want to wrap all available decorators only to revert all of my models once a solution is available...
Hello, Please i just want to define my error messages globally for each decorator is that possible now?
@olawalejuwonm , I guess is not possible. It's crazy how a simple feature is not possible.
@netojose is pr accepted for this?
I can't believe this is still an issue today...
@netojose is pr accepted for this?
Not yet. This issue is opened in 2018, and until now, nothing. On this issue, there are described some work arounds, maybe one of them works for you.
I just get "email must be an email" and want to change it to "email is not valid" :( WHAT IS SO HARD ABOUT THIS REQUEST??? 5 years LMFAO... @vlapo @Cyri-L @DystopianProgrammer @Kiliandeca @henrikra @edcarroll @christophercr @MichalLytek
Instead of passing custom
message
to each decorator.