class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

How to customize validation messages globally?

Open chanlito opened this issue 6 years ago • 61 comments

Instead of passing custom message to each decorator.

chanlito avatar Feb 06 '18 11:02 chanlito

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

kanatkubash avatar Feb 06 '18 13:02 kanatkubash

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

NoNameProvided avatar Feb 06 '18 18:02 NoNameProvided

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

MichalLytek avatar Feb 25 '18 21:02 MichalLytek

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

mendrik avatar Aug 29 '18 11:08 mendrik

@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 avatar Oct 25 '19 10:10 bruno-lombardi

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

josephbuchma avatar Oct 25 '19 11:10 josephbuchma

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

adamscybot avatar Dec 18 '19 17:12 adamscybot

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

borjadev avatar Dec 31 '19 05:12 borjadev

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 avatar Jan 07 '20 08:01 tmtron

@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 avatar Mar 23 '20 15:03 ChrisKatsaras

@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 avatar Mar 23 '20 16:03 tmtron

@tmtron fair enough! Thanks for clarifying

ChrisKatsaras avatar Mar 23 '20 16:03 ChrisKatsaras

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

behruzz avatar Mar 24 '20 10:03 behruzz

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.

tmtron avatar Apr 20 '20 12:04 tmtron

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.

oney avatar Jul 03 '20 09:07 oney

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.

lazarljubenovic avatar Aug 01 '20 15:08 lazarljubenovic

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

jbjhjm avatar Aug 18 '20 18:08 jbjhjm

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

tiamo avatar Aug 20 '20 13:08 tiamo

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

EndyKaufman avatar Aug 28 '20 13:08 EndyKaufman

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

Beej126 avatar Feb 05 '21 03:02 Beej126

Seriously, this functionality hasn't still gone out? I wonder why nestJs chooses this library...

rafaelbrier avatar Feb 07 '21 19:02 rafaelbrier

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(() => {
    // ...
  })

icedcrow avatar Mar 31 '21 08:03 icedcrow

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 image

Ami777 avatar Apr 25 '21 14:04 Ami777

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(',')
    }
}

222222

mehmetsaitdas avatar May 08 '21 19:05 mehmetsaitdas

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.

  1. 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});
      }
   }
})
  1. 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;
}
  1. new decoration like 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.

Iran-110 avatar Jul 22 '21 10:07 Iran-110

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?

averri avatar Aug 14 '21 14:08 averri

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.

averri avatar Aug 14 '21 19:08 averri

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:

image

averri avatar Aug 15 '21 14:08 averri

"Hey, I would like to localize my validation error messages."

Every other validation library in existence: "Well, of course. Here you go!" class-validator: ¯\(ツ)

juni0r avatar Mar 07 '22 15:03 juni0r

Yes @juni0r , and this issue is opened since 2018, and until now... nothing :slightly_frowning_face:

netojose avatar Apr 23 '22 08:04 netojose