Skip to content
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
"test": "jest --config ./tests/jest-test.json",
"test:cov": "jest --config ./tests/jest-test.json --coverage",
"test:watch": "jest --config ./tests/jest-test.json --watch",
"coveralls": "npm run test:cov && cat ./coverage/lcov.info | coveralls",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
Expand Down Expand Up @@ -111,4 +112,4 @@
"last 1 safari version"
]
}
}
}
22 changes: 22 additions & 0 deletions src/filters/i18n-validation-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,34 @@ export class I18nValidationExceptionFilter implements ExceptionFilter {
error.children = this.translateErrors(error.children ?? [], i18n);
error.constraints = Object.keys(error.constraints).reduce(
(result, key) => {
// separate translation key and args
const [translationKey, argsString] =
error.constraints[key].split('|');

// jsonify args
const args = !!argsString ? JSON.parse(argsString) : {};

result[key] = i18n.t(translationKey, {
args: { property: error.property, ...args },
});

// replace error.property with translated property
const errorPropertyInString = (result[key] as string).match(
error.property,
);
let replacementKey = error.property;
if (errorPropertyInString) {
const baseKey = translationKey.split('.')[0];
const replacementKeySub = i18n.t(
baseKey + '.' + errorPropertyInString[0],
) as string;
if (!replacementKeySub.includes(baseKey)) {
replacementKey = replacementKeySub;
}
}

result[key] = result[key].replace(error.property, replacementKey);

return result;
},
{},
Expand Down
2 changes: 1 addition & 1 deletion src/i18n.context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { I18nService, TranslateOptions } from './services/i18n.service';

export class I18nContext {
constructor(readonly lang: string, private readonly service: I18nService) {}
constructor(readonly lang: string, private readonly service: I18nService) { }

public translate<T = any>(key: string, options?: TranslateOptions): T {
options = {
Expand Down
1 change: 0 additions & 1 deletion src/services/i18n.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ export class I18nService {
const pluralObject = this.getPluralObject(translation);
if (pluralObject && args && args['count'] !== undefined) {
const count = Number(args['count']);

if (count == 0 && !!pluralObject.zero) {
translation = pluralObject.zero;
} else if (count == 1 && !!pluralObject.one) {
Expand Down
12 changes: 10 additions & 2 deletions tests/app/controllers/hello.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class HelloController {
constructor(
private i18n: I18nService,
private i18nRequestScope: I18nRequestScopeService,
) {}
) { }

@Get()
hello(@I18nLang() lang: string): any {
Expand All @@ -49,7 +49,7 @@ export class HelloController {

@Get('/index2')
@Render('index2')
index2(): any {}
index2(): any { }

@Get('/short')
helloShort(@I18nLang() lang: string): any {
Expand Down Expand Up @@ -162,6 +162,12 @@ export class HelloController {
return 'This action adds a new user';
}

@Post('/validation-with-keys')
@UseFilters(new I18nValidationExceptionFilter({ detailedErrors: false }))
validationWithKeys(@Body() createUserDto: CreateUserDto): any {
return 'This action adds a new user';
}

@Post('/validation-with-custom-http-code')
@UseFilters(new I18nValidationExceptionFilter({ errorHttpStatusCode: 422 }))
validationWithCustomHttpCode(@Body() createUserDto: CreateUserDto): any {
Expand All @@ -178,6 +184,8 @@ export class HelloController {
return 'This action adds a new user';
}



@GrpcMethod('HeroesService', 'FindOne')
findOne(@Payload() data: HeroById, @I18n() i18n: I18nContext): Hero {
const items = [
Expand Down
21 changes: 18 additions & 3 deletions tests/i18n-dto.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ describe('i18n module e2e dto', () => {
],
};

it('should return translation of keys too', async () => {
const response = await request(app.getHttpServer())
.post('/hello/validation-with-keys')
.set('x-custom-lang', 'fa')
.set('Accept', 'application/json')
.send({
email: '',
password: '',
extra: { subscribeToEmail: true, min: 1, max: 100 },
});

expect(response.status).toBe(400);
expect(response.body.errors[1]).toEqual("ایمیل نباید خالی باشد");
})

it(`should translate validation messages in a custom format if specified`, async () => {
await request(app.getHttpServer())
.post('/hello/validation-custom-formatter')
Expand Down Expand Up @@ -167,7 +182,7 @@ describe('i18n module e2e dto', () => {
statusCode: 400,
message: 'Bad Request',
errors: {
email: ['email is ongeldig', 'e-mail adres mag niet leeg zijn'],
email: ['e-mail adres is ongeldig', 'e-mail adres mag niet leeg zijn'],
password: ['wachtwoord mag niet leeg zijn'],
subscribeToEmail: ['extra.subscribeToEmail is geen boolean'],
min: [
Expand Down Expand Up @@ -246,7 +261,7 @@ describe('i18n module e2e dto', () => {
statusCode: 400,
message: 'Bad Request',
errors: [
'email is ongeldig',
'e-mail adres is ongeldig',
'e-mail adres mag niet leeg zijn',
'wachtwoord mag niet leeg zijn',
'extra.subscribeToEmail is geen boolean',
Expand Down Expand Up @@ -397,7 +412,7 @@ describe('i18n module e2e dto', () => {
property: 'email',
children: [],
constraints: {
isEmail: 'email is ongeldig',
isEmail: 'e-mail adres is ongeldig',
isNotEmpty: 'e-mail adres mag niet leeg zijn',
},
},
Expand Down
7 changes: 7 additions & 0 deletions tests/i18n.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ describe('i18n module', () => {
it('i18n service should return supported languages', () => {
expect(i18nService.getSupportedLanguages()).toEqual([
'en',
'fa',
'fr',
'nl',
'pt-BR',
Expand Down Expand Up @@ -365,6 +366,7 @@ describe('i18n module with fallbacks', () => {
'en-*': 'en',
'fr-*': 'fr',
pt: 'pt-BR',
'fa': 'fa',
},
loaderOptions: {
path: path.join(__dirname, '/i18n'),
Expand All @@ -386,6 +388,7 @@ describe('i18n module with fallbacks', () => {
expect(i18nService.translate('test.HELLO', { lang: 'en-US' })).toBe(
'Hello',
);
expect(i18nService.translate('test.HELLO', { lang: 'fa' })).toBe('سلام');
});

it('i18n service should return dutch translation', () => {
Expand All @@ -407,6 +410,10 @@ describe('i18n module with fallbacks', () => {
expect(i18nService.translate('test.HELLO', { lang: 'pt-BR' })).toBe('Olá');
});

it('i18n service should return farsi translation', () => {
expect(i18nService.translate('test.HELLO', { lang: 'fa' })).toBe('سلام');
});

it('i18n service should return translation with . in key', () => {
expect(i18nService.translate('test.dot.test')).toBe('test');
});
Expand Down
31 changes: 31 additions & 0 deletions tests/i18n/fa/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"HELLO": "سلام",
"PRODUCT": {
"NEW": "New Product: {name}"
},
"ENGLISH": "English",
"ARRAY": ["ONE", "TWO", "THREE"],
"cat": "Cat",
"ONLY_EN_KEY": "this key only exists in en lang",
"cat_name": "Cat: {name}",
"set-up-password": {
"heading": "Hello, {username}",
"title": "Forgot password",
"followLink": "Please follow the link to set up your password"
},
"dot.test": "test",
"day_interval": {
"one": "Every day",
"other": "Every {count} days",
"zero": "Never"
},
"nested": "Message: $t(test.set-up-password.heading, {{\"username\": \"{username}\" }})",
"nested-no-args": "We go shopping: $t(test.dot.test)",
"nest1": {
"nest2": {
"nest3": "We go shopping: $t(test.day_interval, {{\"count\": {count} }})"
}
},
"CURRENT_LANGUAGE": "Current language: {lang}"
}

4 changes: 4 additions & 0 deletions tests/i18n/fa/validation-key.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"email": "ایمیل",
"password": "رمز عبور"
}
9 changes: 9 additions & 0 deletions tests/i18n/fa/validation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"email": "ایمیل",
"password": "رمز عبور",
"NOT_EMPTY": "{property} نباید خالی باشد",
"INVALID_EMAIL": "ایمیل نامعتبر است",
"INVALID_BOOLEAN": "{property} باید بله یا خیر باشد",
"MIN": "{property} با مقادیر: \"{value}\" باید کمتر از {constraints.0}, یا {message}",
"MAX": "{property} با مقادیر: \"{value}\" باید کمتر از {constraints.0}, یا {message}"
}