Skip to content

feat: Adds support for translating validation messages #2564

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

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Class-validator works on both browser and node.js platforms.
- [Passing options](#passing-options)
- [Validation errors](#validation-errors)
- [Validation messages](#validation-messages)
- [Translation of validation messages](#translation-of-validation-messages)
- [Validating arrays](#validating-arrays)
- [Validating sets](#validating-sets)
- [Validating maps](#validating-maps)
Expand Down Expand Up @@ -137,6 +138,8 @@ export interface ValidatorOptions {

forbidUnknownValues?: boolean;
stopAtFirstError?: boolean;

language?: string
}
```

Expand Down Expand Up @@ -262,6 +265,88 @@ Message function accepts `ValidationArguments` which contains the following info
- `object` - object that is being validated
- `property` - name of the object's property being validated

## Translation of validation messages

The package supports translating error messages into different languages. Currently, the supported languages are:

| Linguagem | Código |
|---------------------|--------|
| English | en |
| Portuguese (Brazil) | pt-br |
| Spanish | es |

To translate error messages, simply set the language code in the `language` property of the options object when calling the validate function. For example:

```ts
import { validate } from 'class-validator';

class MyClass {
@IsNotEmpty()
eicCode: string;
}

const model = new MyClass();

validate(model, { language: 'en' }).then(errors => {
//
});
```

If the language option is not specified, the default language will be English (en).

**Default and Custom Driver**

By default, the package uses the `DefaultDriver` to manage translations. However, it is possible to create a custom driver using the TranslatorDriver interface.

Example of implementing a custom translation driver:

```typescript
import { setTranslatorDriver, TranslatorDriver } from 'class-validator';

class MyTranslator implements TranslatorDriver {
translate(key: string, language?: string | null): string {
return 'Translated message';
}
}

setTranslatorDriver(new MyTranslator());
```

**Pre-built Drivers for i18n Integration**

The package also provides support for internationalization using other popular third-party packages like i18n and nestjs-i18n, with out-of-the-box drivers.

**Integration with i18n**

Use the I18nDriver to integrate with the i18n package:

```ts
import { setTranslatorDriver, I18nDriver } from 'class-validator';
import { I18n } from 'i18n';

const i18n = new I18n();

setTranslatorDriver(new I18nDriver(i18n));
```

**Integration with nestjs-i18n**

With the nestjs-i18n package, the `NestI18nDriver` can be configured directly in the bootstrap of NestJS:

```ts
import { setTranslatorDriver, NestI18nDriver } from 'class-validator';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

setTranslatorDriver(new NestI18nDriver(app.get(I18nService)));

await app.listen(3000);
}
```

In this way, the package's error messages will be automatically translated using the internationalization service configured in the project.

## Validating arrays

If your field is an array and you want to perform validation of each item in the array you must specify a
Expand Down
214 changes: 214 additions & 0 deletions locales/en.json

Large diffs are not rendered by default.

214 changes: 214 additions & 0 deletions locales/es.json

Large diffs are not rendered by default.

214 changes: 214 additions & 0 deletions locales/pt-br.json

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions src/decorator/array/ArrayContains.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const ARRAY_CONTAINS = 'arrayContains';

Expand All @@ -24,10 +26,7 @@ export function ArrayContains(values: any[], validationOptions?: ValidationOptio
constraints: [values],
validator: {
validate: (value, args): boolean => arrayContains(value, args?.constraints[0]),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must contain $constraint1 values',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'array-contains-each' : 'array-contains'),
},
},
validationOptions
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/array/ArrayMaxSize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const ARRAY_MAX_SIZE = 'arrayMaxSize';

Expand All @@ -22,10 +24,7 @@ export function ArrayMaxSize(max: number, validationOptions?: ValidationOptions)
constraints: [max],
validator: {
validate: (value, args): boolean => arrayMaxSize(value, args?.constraints[0]),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must contain no more than $constraint1 elements',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'array-max-size-each' : 'array-max-size'),
},
},
validationOptions
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/array/ArrayMinSize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const ARRAY_MIN_SIZE = 'arrayMinSize';

Expand All @@ -22,10 +24,7 @@ export function ArrayMinSize(min: number, validationOptions?: ValidationOptions)
constraints: [min],
validator: {
validate: (value, args): boolean => arrayMinSize(value, args?.constraints[0]),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must contain at least $constraint1 elements',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'array-min-size-each' : 'array-min-size'),
},
},
validationOptions
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/array/ArrayNotContains.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const ARRAY_NOT_CONTAINS = 'arrayNotContains';

Expand All @@ -24,10 +26,7 @@ export function ArrayNotContains(values: any[], validationOptions?: ValidationOp
constraints: [values],
validator: {
validate: (value, args): boolean => arrayNotContains(value, args?.constraints[0]),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property should not contain $constraint1 values',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'array-not-contains-each' : 'array-not-contains'),
},
},
validationOptions
Expand Down
6 changes: 4 additions & 2 deletions src/decorator/array/ArrayNotEmpty.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const ARRAY_NOT_EMPTY = 'arrayNotEmpty';

Expand All @@ -21,7 +23,7 @@ export function ArrayNotEmpty(validationOptions?: ValidationOptions): PropertyDe
name: ARRAY_NOT_EMPTY,
validator: {
validate: (value, args): boolean => arrayNotEmpty(value),
defaultMessage: buildMessage(eachPrefix => eachPrefix + '$property should not be empty', validationOptions),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'array-not-empty-each' : 'array-not-empty'),
},
},
validationOptions
Expand Down
6 changes: 4 additions & 2 deletions src/decorator/array/ArrayUnique.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const ARRAY_UNIQUE = 'arrayUnique';
export type ArrayUniqueIdentifier<T = any> = (o: T) => any;
Expand Down Expand Up @@ -35,7 +37,7 @@ export function ArrayUnique<T = any>(
name: ARRAY_UNIQUE,
validator: {
validate: (value, args): boolean => arrayUnique(value, identifier),
defaultMessage: buildMessage(eachPrefix => eachPrefix + "All $property's elements must be unique", options),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'array-unique-each' : 'array-unique'),
},
},
options
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/common/Equals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const EQUALS = 'equals';

Expand All @@ -20,10 +22,7 @@ export function Equals(comparison: any, validationOptions?: ValidationOptions):
constraints: [comparison],
validator: {
validate: (value, args): boolean => equals(value, args?.constraints[0]),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must be equal to $constraint1',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'equals-each' : 'equals')
},
},
validationOptions
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/common/IsDefined.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from './ValidateBy';
import { ValidateBy } from './ValidateBy';
import { ValidationTypes } from '../../validation/ValidationTypes';
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';

// isDefined is (yet) a special case
export const IS_DEFINED = ValidationTypes.IS_DEFINED;
Expand All @@ -21,10 +23,7 @@ export function IsDefined(validationOptions?: ValidationOptions): PropertyDecora
name: IS_DEFINED,
validator: {
validate: (value): boolean => isDefined(value),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property should not be null or undefined',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'is-defined-each' : 'is-defined')
},
},
validationOptions
Expand Down
6 changes: 4 additions & 2 deletions src/decorator/common/IsEmpty.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const IS_EMPTY = 'isEmpty';

Expand All @@ -19,7 +21,7 @@ export function IsEmpty(validationOptions?: ValidationOptions): PropertyDecorato
name: IS_EMPTY,
validator: {
validate: (value, args): boolean => isEmpty(value),
defaultMessage: buildMessage(eachPrefix => eachPrefix + '$property must be empty', validationOptions),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'is-empty-each' : 'is-empty')
},
},
validationOptions
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/common/IsIn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const IS_IN = 'isIn';

Expand All @@ -20,10 +22,7 @@ export function IsIn(values: readonly any[], validationOptions?: ValidationOptio
constraints: [values],
validator: {
validate: (value, args): boolean => isIn(value, args?.constraints[0]),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must be one of the following values: $constraint1',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'is-in-each' : 'is-in')
},
},
validationOptions
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/common/IsLatLong.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from './ValidateBy';
import { ValidateBy } from './ValidateBy';
import isLatLongValidator from 'validator/lib/isLatLong';

export const IS_LATLONG = 'isLatLong';
Expand All @@ -20,10 +22,7 @@ export function IsLatLong(validationOptions?: ValidationOptions): PropertyDecora
name: IS_LATLONG,
validator: {
validate: (value, args): boolean => isLatLong(value),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must be a latitude,longitude string',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'is-lat-long-each' : 'is-lat-long')
},
},
validationOptions
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/common/IsLatitude.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from './ValidateBy';
import { ValidateBy } from './ValidateBy';
import { isLatLong } from './IsLatLong';
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';

export const IS_LATITUDE = 'isLatitude';

Expand All @@ -20,10 +22,7 @@ export function IsLatitude(validationOptions?: ValidationOptions): PropertyDecor
name: IS_LATITUDE,
validator: {
validate: (value, args): boolean => isLatitude(value),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must be a latitude string or number',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'is-latitude-each' : 'is-latitude')
},
},
validationOptions
Expand Down
9 changes: 4 additions & 5 deletions src/decorator/common/IsLongitude.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from './ValidateBy';
import { ValidateBy } from './ValidateBy';
import { isLatLong } from './IsLatLong';
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';

export const IS_LONGITUDE = 'isLongitude';

Expand All @@ -20,10 +22,7 @@ export function IsLongitude(validationOptions?: ValidationOptions): PropertyDeco
name: IS_LONGITUDE,
validator: {
validate: (value, args): boolean => isLongitude(value),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must be a longitude string or number',
validationOptions
),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'is-longitude-each' : 'is-longitude')
},
},
validationOptions
Expand Down
6 changes: 4 additions & 2 deletions src/decorator/common/IsNotEmpty.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValidationArguments } from '../../validation/ValidationArguments';
import { TranslateFunction } from '../../validation/ValidationExecutor';
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import { ValidateBy } from '../common/ValidateBy';

export const IS_NOT_EMPTY = 'isNotEmpty';

Expand All @@ -19,7 +21,7 @@ export function IsNotEmpty(validationOptions?: ValidationOptions): PropertyDecor
name: IS_NOT_EMPTY,
validator: {
validate: (value, args): boolean => isNotEmpty(value),
defaultMessage: buildMessage(eachPrefix => eachPrefix + '$property should not be empty', validationOptions),
defaultMessage: (args: ValidationArguments, translate: TranslateFunction) => translate(validationOptions?.each ? 'is-not-empty-each' : 'is-not-empty')
},
},
validationOptions
Expand Down
Loading