Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to customize validation messages globally? #169

Open
chanlito opened this issue Feb 6, 2018 · 63 comments · May be fixed by #2564
Open

How to customize validation messages globally? #169

chanlito opened this issue Feb 6, 2018 · 63 comments · May be fixed by #2564
Labels
type: feature Issues related to new features.

Comments

@chanlito
Copy link

chanlito commented Feb 6, 2018

Instead of passing custom message to each decorator.

@kanatkubash
Copy link

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);
}

@NoNameProvided NoNameProvided added the type: feature Issues related to new features. label Feb 6, 2018
@NoNameProvided
Copy link
Member

NoNameProvided commented Feb 6, 2018

Hey, this is on my list, I will add it.

@MichalLytek
Copy link
Contributor

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) 😉

@mendrik
Copy link

mendrik commented Aug 29, 2018

@chanlito as work around I do it by delegating new decorators to existing ones:
export const IsRequired: Function = () => IsNotEmpty({ message: () => translate('validation.required') });

@bruno-lombardi
Copy link

bruno-lombardi commented Oct 25, 2019

@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.

@josephbuchma
Copy link

@bruno-lombardi translate is not a decorator in example above, it's just a regular function that returns translated string by given key.

@vlapo vlapo mentioned this issue Nov 11, 2019
@adamscybot
Copy link

Trouvble is I want all the constraints back not just key code. You cant get the contraints in the error for rendering later.

@borjadev
Copy link

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

@tmtron
Copy link
Contributor

tmtron commented Jan 7, 2020

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.

@ChrisKatsaras
Copy link

@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 😄

@tmtron
Copy link
Contributor

tmtron commented Mar 23, 2020

@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...

@ChrisKatsaras
Copy link

@tmtron fair enough! Thanks for clarifying

@behruzz
Copy link

behruzz commented Mar 24, 2020

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

@tmtron
Copy link
Contributor

tmtron commented Apr 20, 2020

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.

@oney
Copy link

oney commented Jul 3, 2020

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.

@lazarljubenovic
Copy link

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.

@jbjhjm
Copy link

jbjhjm commented Aug 18, 2020

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. #238

@tiamo
Copy link

tiamo commented Aug 20, 2020

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",
}

@EndyKaufman
Copy link

i create PR basic support i18n: #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
});

@Beej126
Copy link

Beej126 commented Feb 5, 2021

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`});

@pmirand6
Copy link

image

Perfect @EndyKaufman thanks!

@EndyKaufman
Copy link

@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

@john-wennstrom
Copy link

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();

@netojose
Copy link

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.

@pedrosodre
Copy link

pedrosodre commented Jul 10, 2022

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 :)

@netojose
Copy link

Thanks, @pedrosodre :)

@buddh4
Copy link

buddh4 commented Oct 24, 2022

I've described two possible solutions here #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...

@olawalejuwonm
Copy link

Hello, Please i just want to define my error messages globally for each decorator is that possible now?

@netojose
Copy link

netojose commented Jul 4, 2023

@olawalejuwonm , I guess is not possible. It's crazy how a simple feature is not possible.

@olawalejuwonm
Copy link

@netojose is pr accepted for this?

@picardplaisimond
Copy link

I can't believe this is still an issue today...

@netojose
Copy link

netojose commented Aug 5, 2023

@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.

@mikila85
Copy link

mikila85 commented Aug 5, 2023

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

@lazarljubenovic
Copy link

What's so hard about understanding that a project is dead? Certainly your all-caps rage ain't gonna bring it back. npm init and start working on your own instead.

@Beej126
Copy link

Beej126 commented Aug 5, 2023

nice one @lazarljubenovic =) it's a quiet saturday afternoon where i'm at so why not pile on a little...

@mikila85, @picardplaisimond, @olawalejuwonm, etc... we did the wrappers for custom messaging that i mentioned up in this thread over 2 years ago and those have been rock solid without need for change still cranking great for tons of production usage every day, making it very much not dead to us... the customizing is not hard code... if you look at this library as a nice core engine for managing typescript decorators and then customizing to your needs on top of that, it is very useful... even for adding in your own new validation routines that can be called the same as those it comes with, very easy... it's of course your choice where you want to invest your time and it doesn't have to be here if you can find exactly what you need elsewhere by all means, but i just wanted to chime in and maybe provide a little "faith" that this might very well work for you without that much effort if you dig in on the samples provided above and ask questions to folks that are still actively using this lib... i'll help where i can answer a direct question, our needs are pretty simple, e.g. we're not doing multi-language so you'd have to get that kind of help from the other samples posted.

i realized a main thing i wanted to point out is that some library communities can be quiet because they're dead and it's best to take that as a sign to move on, but this one is quiet because it has achieved feature maturity (for our needs at least) which is a completely opposite reason to stick around and learn more.

@picardplaisimond
Copy link

picardplaisimond commented Aug 6, 2023

I've seen the solution you suggested @Beej126. I almost went for it. It seems solid.

But we finally decided to drop this feature. We use this library in our backend with NestJS. I was looking for a way to add translation to error messages. But the default messages are good enough (and it's more for a debug purpose since the user will not see it)

Indeed, since we also validate the forms before sending them to the backend, we can generate the error messages directly on the frontend client (with the translations).

For more complex validations requiring this library, we can at least change the message to return an error code (that we can format on the frontend).

Finally, there's a library called NestJs-I18n that has a function for translating error messages. It's not global, but it's already very good. But we've decided not to use it, since we're now handling all this on the frontend.

On the other hand, I'm still very disappointed that such a simple feature isn't available... but like @Beej126 said, this library reached maturity. So we shouldn't wait for an update to happen tomorrow. It does an excellent job in all other aspects of data validation.

I hope this reflexion will help other people.

@jorge-koki
Copy link

jorge-koki commented Oct 25, 2023

Funciono para mi, en express

import i18next from "i18next";
import i18nextBackend from "i18next-fs-backend";
import i18nextMiddleware from "i18next-http-middleware";

i18next
  .use(i18nextBackend)
  .use(i18nextMiddleware.LanguageDetector)
  .init({
    fallbackLng: "es",
    backend: {
      loadPath: _dirname + "/../locales/{{lng}}/traduccion.json",
    },
  });

const app: Application = express();
app.use(i18nextMiddleware.handle(i18next));
@IsEmail(
    {},
    {
      message: () => {
        return i18next.t("is_email");
      },
    }
  )

p1

Nose porque pero si lo pongo asi:
message: i18next.t("is_email")
No funciona

Pero si lo coloco asi, funciona bien

message: () => i18next.t("is_email"),

o que seria lo mismo

message: () => 
{
return i18next.t("is_email")
}

@nameer
Copy link

nameer commented Jan 1, 2024

A validation message without the property name will be beneficial. The actual field name may not be the same as the variable name.

@ytetsuro
Copy link
Contributor

We have developed a wrapper that thinly wraps the class-validator to make the default error messages in a specific language.
It is not possible to translate the messages according to the client's language like i18next does, but we would be happy if you could use it if you just want to translate the messages into your own language.
It is just wrapping the class-validator rules, so it won't break your environment.

https://github.com/ytetsuro/translated-class-validator

@olawalejuwonm
Copy link

This library just had a new release on jan 12.

Any update as regards this issue?

@aritradevelops
Copy link

Will it be ever happening?

@aritradevelops
Copy link

FYI: I did some tweaks with the package which enables you to pass a transformation function at the time of validation to modify the validation error message. you can write your own implementation of the transformation function (which will be called with the name or type of validation, example: isString, isArray and so on) to handle the message as you want, but you can also pass i18next.t or req.t t to the validatorOptions as the transformation function which will pick the validation message from the translation.json based on the language.

image

Also incase you want to provide any specific message for any specific key in your schema then you can pass a transformKey to the validationOptions of that specific property and write it's implementation in the translation.json

image

You can play with the live demo here
or see the documentation here
I hope that helps 🙌

@musaevonline
Copy link

musaevonline commented Apr 6, 2024

My workaround. It seems to be working fine. No need any patch.

import { ValidationUtils } from 'class-validator/cjs/validation/ValidationUtils';

export const ErrorMessages = {
arrayContains: 'поле $property должно содержать $constraint1 значений',
arrayMaxSize:
    'поле $property должно содержать не более $constraint1 элементов',
arrayMinSize:
    'поле $property должно содержать не менее $constraint1 элементов',
arrayNotContains: 'поле $property не должно содержать $constraint1 значений',
arrayNotEmpty: 'поле $property не должно быть пустым',
contains: 'поле $property должно содержать строку $constraint1',
equals: 'поле $property должно быть равно $constraint1',
isAlpha: 'поле $property должно содержать только буквы (a-zA-Z)',
isAlphanumeric: 'поле $property должно содержать только буквы и цифры',
isArray: 'поле $property должно быть массивом',
isAscii: 'поле $property должно содержать только символы ASCII',
isBIC: 'поле $property должно быть BIC или SWIFT кодом',
isBase32: 'поле $property должно быть закодировано в формате base32',
isBase58: 'поле $property должно быть закодировано в формате base58',
isBase64: 'поле $property должно быть закодировано в формате base64',
isBoolean: 'поле $property должно быть логическим значением',
isBooleanString:
    'поле $property должно быть строкой, представляющей логическое значение',
isBtcAddress: 'поле $property должно быть адресом BTC',
isCreditCard: 'поле $property должно быть кредитной картой',
isCurrency: 'поле $property должно быть валютой',
isDataURI: 'поле $property должно быть в формате data URI',
isDate: 'поле $property должно быть экземпляром Date',
isDateString:
    'поле $property должно быть корректной строкой даты в формате ISO 8601',
isDecimal: 'поле $property не является действительным десятичным числом.',
isDefined: 'поле $property не должно быть null или undefined',
isDivisibleBy: 'поле $property должно быть кратным $constraint1',
isEAN: 'поле $property должно быть EAN (европейским артикулом)',
isEmail: 'поле $property должно быть адресом электронной почты',
isEmpty: 'поле $property должно быть пустым',
isEnum:
    'поле $property должно быть одним из следующих значений: $constraint2',
isEthereumAddress: 'поле $property должно быть адресом Ethereum',
isFQDN: 'поле $property должно быть допустимым доменным именем',
isFirebasePushId: 'поле $property должно быть идентификатором Firebase Push',
isFullWidth: 'поле $property должно содержать символы полной ширины',
isHSL: 'поле $property должно быть цветом в формате HSL',
isHalfWidth: 'поле $property должно содержать символы половины ширины',
isHash: 'поле $property должно быть хешем типа $constraint1',
isHexColor: 'поле $property должно быть шестнадцатеричным цветом',
isHexadecimal: 'поле $property должно быть шестнадцатеричным числом',
isIBAN: 'поле $property должно быть IBAN',
isIP: 'поле $property должно быть IP-адресом',
isISBN: 'поле $property должно быть ISBN',
isISIN:
    'поле $property должно быть ISIN (идентификатором ценной бумаги/акции)',
isISO31661Alpha2:
    'поле $property должно быть допустимым кодом ISO31661 Alpha2',
isISO31661Alpha3:
    'поле $property должно быть допустимым кодом ISO31661 Alpha3',
isISO8601:
    'поле $property должно быть корректной строкой даты в формате ISO 8601',
isISRC: 'поле $property должно быть ISRC',
isISSN: 'поле $property должно быть ISSN',
isIdentityCard: 'поле $property должно быть номером удостоверения личности',
isIn: 'поле $property должно быть одним из следующих значений: $constraint1',
isInt: 'поле $property должно быть целым числом',
isJSON: 'поле $property должно быть строкой JSON',
isJWT: 'поле $property должно быть строкой JWT',
isLatLong: 'поле $property должно быть строкой широты,долготы',
isLatitude: 'поле $property должно быть строкой или числом широты',
isLocale: 'поле $property должно быть локалью',
isLongitude: 'поле $property должно быть строкой или числом долготы',
isLowercase: 'поле $property должно быть строкой в нижнем регистре',
isMacAddress: 'поле $property должно быть MAC-адресом',
isMagnetURI: 'поле $property должно быть в формате magnet URI',
isMilitaryTime:
    'поле $property должно быть корректным представлением времени военного времени в формате HH:MM',
isMimeType: 'поле $property должно быть форматом MIME типа',
isMobilePhone: 'поле $property должно быть номером телефона',
isMongoId: 'поле $property должно быть идентификатором MongoDB',
isMultibyte:
    'поле $property должно содержать один или более многобайтовых символов',
isNegative: 'поле $property должно быть отрицательным числом',
isNotEmpty: 'поле $property не должно быть пустым',
isNotEmptyObject: 'поле $property должно быть непустым объектом',
isNotIn:
    'поле $property не должно быть одним из следующих значений: $constraint1',
isNumber:
    'поле $property должно быть числом, соответствующим указанным ограничениям',
isNumberString: 'поле $property должно быть строкой числа',
isObject: 'поле $property должно быть объектом',
isOctal: 'поле $property должно быть допустимым восьмеричным числом',
isPassportNumber:
    'поле $property должно быть действительным номером паспорта',
isPhoneNumber: 'поле $property должно быть действительным номером телефона',
isPort: 'поле $property должно быть портом',
isPositive: 'поле $property должно быть положительным числом',
isPostalCode: 'поле $property должно быть почтовым индексом',
isRFC3339: 'поле $property должно быть датой в формате RFC 3339',
isRgbColor: 'поле $property должно быть цветом RGB',
isSemVer:
    'поле $property должно соответствовать спецификации семантического версионирования',
isString: 'поле $property должно быть строкой',
isStrongPassword: 'поле $property недостаточно надёжно',
isSurrogatePair:
    'поле $property должно содержать любые символы пары замещения',
isTimeZone: 'поле $property должно быть допустимой временной зоной IANA',
isUUID: 'поле $property должно быть UUID',
isUppercase: 'поле $property должно быть строкой в верхнем регистре',
isUrl: 'поле $property должно быть адресом URL',
isVariableWidth:
    'поле $property должно содержать символы полной и половинной ширины',
matches:
    'поле $property должно соответствовать регулярному выражению $constraint1',
max: 'поле $property не должно быть больше $constraint1',
maxDate: 'поле $property - это $constraint1',
maxLength:
    'поле $property должно быть короче или равно $constraint1 символам',
min: 'поле $property не должно быть меньше $constraint1',
minDate: 'минимальная допустимая дата для $property - это $constraint1',
minLength:
    'поле $property должно быть длиннее или равно $constraint1 символам',
notContains: 'поле $property не должно содержать строку $constraint1',
notEquals: 'поле $property не должно быть равно $constraint1',
isISO4217CurrencyCode:
    'поле $property должно быть допустимым кодом валюты ISO4217',
isTaxId: 'поле $property должно быть номером налоговой идентификации',
};





app.useGlobalPipes(
new ValidationPipe({
  validationError: {
    value: true,
  },
  exceptionFactory(errors) {
    const result = [];
    errors.forEach((error) => {
      const validationMetas =
        getMetadataStorage().getTargetValidationMetadatas(
          error.target.constructor,
          error.target.constructor.name,
          true,
          false,
        );
      const validationMeta = validationMetas.find(
        (meta) => meta.propertyName === error.property,
      );
      const validationArguments = {
        targetName: error.target.constructor.name,
        property: error.property,
        value: error.value,
        constraints: validationMeta.constraints,
      };

      const message = ValidationUtils.replaceMessageSpecialTokens(
        ErrorMessages[Object.keys(error.constraints)[0]],
        validationArguments,
      );
      result.push(message);
    });
    return new BadRequestException(result);
  },
}),
);

@btd1337
Copy link

btd1337 commented Jun 7, 2024

Enhancing @musaevonline suggestion, allowing for a customizable field name in the response message:

Example:

@IsString({ context: { fieldName: 'nome do usuário' } })    <= optional custom field name
userName: string;

Code:

/**
 * Arguments being sent to message builders - user can create message either by simply returning a string,
 * either by returning a function that accepts MessageArguments and returns a message string built based on these arguments.
 */
export interface ValidationArguments {
  /**
   * Constraints set by this validation type.
   */
  constraints: any[];

  /**
   * Name of the field that is being validated
   */
  fieldName?: string;    <= Added this field

  /**
   * Object that is being validated.
   */
  object?: object;

  /**
   * Name of the object's property being validated.
   */
  property: string;

  /**
   * Name of the target that is being validated.
   */
  targetName: string;

  /**
   * Validating value.
   */
  value: any;
}
export class ValidationUtils {
  static replaceMessageSpecialTokens(
    message: ((args: ValidationArguments) => string) | string,
    validationArguments: ValidationArguments,
  ): string {
    let messageString: string;

    if (message instanceof Function) {
      messageString = (message as (args: ValidationArguments) => string)(validationArguments);
    } else if (typeof message === 'string') {
      messageString = message;
    }

    if (messageString && Array.isArray(validationArguments.constraints)) {
      validationArguments.constraints.forEach((constraint, index) => {
        messageString = messageString.replace(
          new RegExp(`\\$constraint${index + 1}`, 'g'),
          constraintToString(constraint),
        );
      });
    }

    if (
      messageString &&
      validationArguments?.value &&
      ['boolean', 'number', 'string'].includes(typeof validationArguments.value)
    )
      messageString = messageString.replace(/\$value/g, validationArguments.value);
    if (messageString) {
      messageString = messageString.replace(
        /\$property/g,
        validationArguments.fieldName || validationArguments.property,    <= Added this check
      );
    }
    if (messageString) messageString = messageString.replace(/\$target/g, validationArguments.targetName);

    return messageString;
  }
}
// main.ts

app.useGlobalPipes( new ValidationPipe({
    exceptionFactory(errors): BadRequestException {
      const result: string[] = [];
      errors.forEach((error) => {
        const validationMetas = getMetadataStorage().getTargetValidationMetadatas(
          error.target.constructor,
          error.target.constructor.name,
          true,
          false,
        );
        const validationMeta = validationMetas.find((meta) => meta.propertyName === error.property);

        let errorKey = Object.keys(error.constraints)[0];
        const errorMessage = error.constraints[errorKey];

          // fix isLength ambiguous message
          if (errorKey === 'isLength') {
            if (errorMessage.includes('be longer')) {
              errorKey = 'minLength';
            } else if (errorMessage.includes('be shorter')) {
              errorKey = 'maxLength';
            } else {
              Logger.warn(`Mensagem inesperada para a propriedade 'isLength': ${errorMessage}`);
            }
          }

        const validationArguments: ValidationArguments = {
          constraints: validationMeta.constraints,
          fieldName: error?.contexts?.[errorKey]?.fieldName,
          property: error.property,
          targetName: error.target.constructor.name,
          value: error.value,
        };

        const messageTemplate = errorMessages[errorKey];

        if (!messageTemplate) {
          Logger.warn(`Chave de erro não encontrada na lista de Mensagens de Erro: ${errorKey}`);
        }

        const message = ValidationUtils.replaceMessageSpecialTokens(messageTemplate, validationArguments);
        result.push(message);
      });

      return new BadRequestException(result);
    },
    validationError: {
      value: true,
    },
  })
)
export const errorMessages = {
  arrayContains: 'o campo $property deve conter $constraint1 valores',
  arrayMaxSize: 'o campo $property deve conter no máximo $constraint1 elementos',
  arrayMinSize: 'o campo $property deve conter pelo menos $constraint1 elementos',
  arrayNotContains: 'o campo $property não deve conter $constraint1 valores',
  arrayNotEmpty: 'o campo $property não deve estar vazio',
  contains: 'o campo $property deve conter a string $constraint1',
  equals: 'o campo $property deve ser igual a $constraint1',
  isAlpha: 'o campo $property deve conter apenas letras (a-zA-Z)',
  isAlphanumeric: 'o campo $property deve conter apenas letras e números',
  isArray: 'o campo $property deve ser um array',
  isAscii: 'o campo $property deve conter apenas caracteres ASCII',
  isBIC: 'o campo $property deve ser um código BIC ou SWIFT',
  isBase32: 'o campo $property deve estar codificado no formato base32',
  isBase58: 'o campo $property deve estar codificado no formato base58',
  isBase64: 'o campo $property deve estar codificado no formato base64',
  isBoolean: 'o campo $property deve ser um valor booleano',
  isBooleanString: 'o campo $property deve ser uma string que representa um valor booleano',
  isBtcAddress: 'o campo $property deve ser um endereço BTC',
  isCreditCard: 'o campo $property deve ser um cartão de crédito',
  isCurrency: 'o campo $property deve ser uma moeda',
  isDataURI: 'o campo $property deve estar no formato data URI',
  isDate: 'o campo $property deve ser uma instância de Date',
  isDateString: 'o campo $property deve ser uma string de data válida no formato ISO 8601',
  isDecimal: 'o campo $property não é um número decimal válido',
  isDefined: 'o campo $property não deve ser null ou undefined',
  isDivisibleBy: 'o campo $property deve ser divisível por $constraint1',
  isEAN: 'o campo $property deve ser um EAN (código de artigo europeu)',
  isEmail: 'o campo $property deve ser um endereço de email',
  isEmpty: 'o campo $property deve estar vazio',
  isEnum: 'o campo $property deve ser um dos seguintes valores: $constraint2',
  isEthereumAddress: 'o campo $property deve ser um endereço Ethereum',
  isFQDN: 'o campo $property deve ser um nome de domínio válido',
  isFirebasePushId: 'o campo $property deve ser um identificador Firebase Push',
  isFullWidth: 'o campo $property deve conter caracteres de largura total',
  isHSL: 'o campo $property deve ser uma cor no formato HSL',
  isHalfWidth: 'o campo $property deve conter caracteres de meia largura',
  isHash: 'o campo $property deve ser um hash do tipo $constraint1',
  isHexColor: 'o campo $property deve ser uma cor hexadecimal',
  isHexadecimal: 'o campo $property deve ser um número hexadecimal',
  isIBAN: 'o campo $property deve ser um IBAN',
  isIP: 'o campo $property deve ser um endereço IP',
  isISBN: 'o campo $property deve ser um ISBN',
  isISIN: 'o campo $property deve ser um ISIN (identificador de título/ação)',
  isISO4217CurrencyCode: 'o campo $property deve ser um código de moeda ISO4217 válido',
  isISO8601: 'o campo $property deve ser uma string de data válida no formato ISO 8601',
  isISO31661Alpha2: 'o campo $property deve ser um código ISO31661 Alpha2 válido',
  isISO31661Alpha3: 'o campo $property deve ser um código ISO31661 Alpha3 válido',
  isISRC: 'o campo $property deve ser um ISRC',
  isISSN: 'o campo $property deve ser um ISSN',
  isIdentityCard: 'o campo $property deve ser um número de documento de identidade',
  isIn: 'o campo $property deve ser um dos seguintes valores: $constraint1',
  isInt: 'o campo $property deve ser um número inteiro',
  isJSON: 'o campo $property deve ser uma string JSON',
  isJWT: 'o campo $property deve ser uma string JWT',
  isLatLong: 'o campo $property deve ser uma string de latitude e longitude',
  isLatitude: 'o campo $property deve ser uma string ou número de latitude',
  isLocale: 'o campo $property deve ser uma localidade',
  isLongitude: 'o campo $property deve ser uma string ou número de longitude',
  isLowercase: 'o campo $property deve ser uma string em minúsculas',
  isMacAddress: 'o campo $property deve ser um endereço MAC',
  isMagnetURI: 'o campo $property deve estar no formato magnet URI',
  isMilitaryTime: 'o campo $property deve ser uma representação de hora militar válida no formato HH:MM',
  isMimeType: 'o campo $property deve ser um tipo MIME válido',
  isMobilePhone: 'o campo $property deve ser um número de telefone válido',
  isMongoId: 'o campo $property deve ser um identificador MongoDB',
  isMultibyte: 'o campo $property deve conter um ou mais caracteres multibyte',
  isNegative: 'o campo $property deve ser um número negativo',
  isNotEmpty: 'o campo $property não deve estar vazio',
  isNotEmptyObject: 'o campo $property deve ser um objeto não vazio',
  isNotIn: 'o campo $property não deve ser um dos seguintes valores: $constraint1',
  isNumber: 'o campo $property deve ser um número que atenda às restrições especificadas',
  isNumberString: 'o campo $property deve ser uma string numérica',
  isObject: 'o campo $property deve ser um objeto',
  isOctal: 'o campo $property deve ser um número octal válido',
  isPassportNumber: 'o campo $property deve ser um número de passaporte válido',
  isPhoneNumber: 'o campo $property deve ser um número de telefone válido',
  isPort: 'o campo $property deve ser uma porta',
  isPositive: 'o campo $property deve ser um número positivo',
  isPostalCode: 'o campo $property deve ser um código postal',
  isRFC3339: 'o campo $property deve ser uma data no formato RFC 3339',
  isRgbColor: 'o campo $property deve ser uma cor RGB',
  isSemVer: 'o campo $property deve estar em conformidade com a especificação de versionamento semântico',
  isString: 'o campo $property deve ser uma string',
  isStrongPassword: 'o campo $property não é seguro o suficiente',
  isSurrogatePair: 'o campo $property deve conter qualquer caractere de par substituto',
  isTaxId: 'o campo $property deve ser um número de identificação fiscal',
  isTimeZone: 'o campo $property deve ser um fuso horário IANA válido',
  isUppercase: 'o campo $property deve ser uma string em maiúsculas',
  isUrl: 'o campo $property deve ser um URL',
  isUuid: 'o campo $property deve ser um UUID',
  isVariableWidth: 'o campo $property deve conter caracteres de largura total e meia largura',
  matches: 'o campo $property deve corresponder à expressão regular $constraint1',
  max: 'o campo $property não deve ser maior que $constraint1',
  maxDate: 'a data máxima para o campo $property é $constraint1',
  maxLength: 'o campo $property deve ter no máximo $constraint1 caracteres',
  min: 'o campo $property não deve ser menor que $constraint1',
  minDate: 'a data mínima permitida para o campo $property é $constraint1',
  minLength: 'o campo $property deve ter no mínimo $constraint1 caracteres',
  notContains: 'o campo $property não deve conter a string $constraint1',
  notEquals: 'o campo $property não deve ser igual a $constraint1',
};

@gabrielbs-dev gabrielbs-dev linked a pull request Dec 13, 2024 that will close this issue
6 tasks
@gabrielbs-dev
Copy link

I was facing the same problem.

The package is excellent and easy to use, but the lack of translation support ends up making it difficult to use for those who are developing systems that need to support multiple languages ​​(like in my case).

Considering the needs of everyone in this issue, I decided to dedicate some of my time to implement a translation system that supports some languages ​​by default (English, Portuguese (Brazil) and Spanish) and that is extensible, allowing users to customize the feature or integrate with other packages, such as i18n.

I've sent a pull request to the developers and am waiting for their approval.

@NewGrafon
Copy link

My workaround. It seems to be working fine. No need any patch.

import { ValidationUtils } from 'class-validator/cjs/validation/ValidationUtils';

export const ErrorMessages = {
arrayContains: 'поле $property должно содержать $constraint1 значений',
arrayMaxSize:
    'поле $property должно содержать не более $constraint1 элементов',
arrayMinSize:
    'поле $property должно содержать не менее $constraint1 элементов',
arrayNotContains: 'поле $property не должно содержать $constraint1 значений',
arrayNotEmpty: 'поле $property не должно быть пустым',
contains: 'поле $property должно содержать строку $constraint1',
equals: 'поле $property должно быть равно $constraint1',
isAlpha: 'поле $property должно содержать только буквы (a-zA-Z)',
isAlphanumeric: 'поле $property должно содержать только буквы и цифры',
isArray: 'поле $property должно быть массивом',
isAscii: 'поле $property должно содержать только символы ASCII',
isBIC: 'поле $property должно быть BIC или SWIFT кодом',
isBase32: 'поле $property должно быть закодировано в формате base32',
isBase58: 'поле $property должно быть закодировано в формате base58',
isBase64: 'поле $property должно быть закодировано в формате base64',
isBoolean: 'поле $property должно быть логическим значением',
isBooleanString:
    'поле $property должно быть строкой, представляющей логическое значение',
isBtcAddress: 'поле $property должно быть адресом BTC',
isCreditCard: 'поле $property должно быть кредитной картой',
isCurrency: 'поле $property должно быть валютой',
isDataURI: 'поле $property должно быть в формате data URI',
isDate: 'поле $property должно быть экземпляром Date',
isDateString:
    'поле $property должно быть корректной строкой даты в формате ISO 8601',
isDecimal: 'поле $property не является действительным десятичным числом.',
isDefined: 'поле $property не должно быть null или undefined',
isDivisibleBy: 'поле $property должно быть кратным $constraint1',
isEAN: 'поле $property должно быть EAN (европейским артикулом)',
isEmail: 'поле $property должно быть адресом электронной почты',
isEmpty: 'поле $property должно быть пустым',
isEnum:
    'поле $property должно быть одним из следующих значений: $constraint2',
isEthereumAddress: 'поле $property должно быть адресом Ethereum',
isFQDN: 'поле $property должно быть допустимым доменным именем',
isFirebasePushId: 'поле $property должно быть идентификатором Firebase Push',
isFullWidth: 'поле $property должно содержать символы полной ширины',
isHSL: 'поле $property должно быть цветом в формате HSL',
isHalfWidth: 'поле $property должно содержать символы половины ширины',
isHash: 'поле $property должно быть хешем типа $constraint1',
isHexColor: 'поле $property должно быть шестнадцатеричным цветом',
isHexadecimal: 'поле $property должно быть шестнадцатеричным числом',
isIBAN: 'поле $property должно быть IBAN',
isIP: 'поле $property должно быть IP-адресом',
isISBN: 'поле $property должно быть ISBN',
isISIN:
    'поле $property должно быть ISIN (идентификатором ценной бумаги/акции)',
isISO31661Alpha2:
    'поле $property должно быть допустимым кодом ISO31661 Alpha2',
isISO31661Alpha3:
    'поле $property должно быть допустимым кодом ISO31661 Alpha3',
isISO8601:
    'поле $property должно быть корректной строкой даты в формате ISO 8601',
isISRC: 'поле $property должно быть ISRC',
isISSN: 'поле $property должно быть ISSN',
isIdentityCard: 'поле $property должно быть номером удостоверения личности',
isIn: 'поле $property должно быть одним из следующих значений: $constraint1',
isInt: 'поле $property должно быть целым числом',
isJSON: 'поле $property должно быть строкой JSON',
isJWT: 'поле $property должно быть строкой JWT',
isLatLong: 'поле $property должно быть строкой широты,долготы',
isLatitude: 'поле $property должно быть строкой или числом широты',
isLocale: 'поле $property должно быть локалью',
isLongitude: 'поле $property должно быть строкой или числом долготы',
isLowercase: 'поле $property должно быть строкой в нижнем регистре',
isMacAddress: 'поле $property должно быть MAC-адресом',
isMagnetURI: 'поле $property должно быть в формате magnet URI',
isMilitaryTime:
    'поле $property должно быть корректным представлением времени военного времени в формате HH:MM',
isMimeType: 'поле $property должно быть форматом MIME типа',
isMobilePhone: 'поле $property должно быть номером телефона',
isMongoId: 'поле $property должно быть идентификатором MongoDB',
isMultibyte:
    'поле $property должно содержать один или более многобайтовых символов',
isNegative: 'поле $property должно быть отрицательным числом',
isNotEmpty: 'поле $property не должно быть пустым',
isNotEmptyObject: 'поле $property должно быть непустым объектом',
isNotIn:
    'поле $property не должно быть одним из следующих значений: $constraint1',
isNumber:
    'поле $property должно быть числом, соответствующим указанным ограничениям',
isNumberString: 'поле $property должно быть строкой числа',
isObject: 'поле $property должно быть объектом',
isOctal: 'поле $property должно быть допустимым восьмеричным числом',
isPassportNumber:
    'поле $property должно быть действительным номером паспорта',
isPhoneNumber: 'поле $property должно быть действительным номером телефона',
isPort: 'поле $property должно быть портом',
isPositive: 'поле $property должно быть положительным числом',
isPostalCode: 'поле $property должно быть почтовым индексом',
isRFC3339: 'поле $property должно быть датой в формате RFC 3339',
isRgbColor: 'поле $property должно быть цветом RGB',
isSemVer:
    'поле $property должно соответствовать спецификации семантического версионирования',
isString: 'поле $property должно быть строкой',
isStrongPassword: 'поле $property недостаточно надёжно',
isSurrogatePair:
    'поле $property должно содержать любые символы пары замещения',
isTimeZone: 'поле $property должно быть допустимой временной зоной IANA',
isUUID: 'поле $property должно быть UUID',
isUppercase: 'поле $property должно быть строкой в верхнем регистре',
isUrl: 'поле $property должно быть адресом URL',
isVariableWidth:
    'поле $property должно содержать символы полной и половинной ширины',
matches:
    'поле $property должно соответствовать регулярному выражению $constraint1',
max: 'поле $property не должно быть больше $constraint1',
maxDate: 'поле $property - это $constraint1',
maxLength:
    'поле $property должно быть короче или равно $constraint1 символам',
min: 'поле $property не должно быть меньше $constraint1',
minDate: 'минимальная допустимая дата для $property - это $constraint1',
minLength:
    'поле $property должно быть длиннее или равно $constraint1 символам',
notContains: 'поле $property не должно содержать строку $constraint1',
notEquals: 'поле $property не должно быть равно $constraint1',
isISO4217CurrencyCode:
    'поле $property должно быть допустимым кодом валюты ISO4217',
isTaxId: 'поле $property должно быть номером налоговой идентификации',
};





app.useGlobalPipes(
new ValidationPipe({
  validationError: {
    value: true,
  },
  exceptionFactory(errors) {
    const result = [];
    errors.forEach((error) => {
      const validationMetas =
        getMetadataStorage().getTargetValidationMetadatas(
          error.target.constructor,
          error.target.constructor.name,
          true,
          false,
        );
      const validationMeta = validationMetas.find(
        (meta) => meta.propertyName === error.property,
      );
      const validationArguments = {
        targetName: error.target.constructor.name,
        property: error.property,
        value: error.value,
        constraints: validationMeta.constraints,
      };

      const message = ValidationUtils.replaceMessageSpecialTokens(
        ErrorMessages[Object.keys(error.constraints)[0]],
        validationArguments,
      );
      result.push(message);
    });
    return new BadRequestException(result);
  },
}),
);

бомбезно чювак, спасибо за перевод ошибок и реализацию пайпа, только надо заменить

  const validationMeta = validationMetas.find(
    (meta) => meta.propertyName === error.property,
  );

на

  const validationMeta = validationMetas.find(
     (meta) => meta.propertyName === error.property && meta.name === Object.keys(error.constraints || {})[0],
  );

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature Issues related to new features.