Skip to content

feat(i18n): add language support for registration, password recovery,… Integrated i18n support in MailService for email templates #1921

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

Closed
wants to merge 1 commit into from
Closed
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
52 changes: 47 additions & 5 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Patch,
Delete,
SerializeOptions,
Headers,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
Expand All @@ -24,14 +25,20 @@ import { LoginResponseDto } from './dto/login-response.dto';
import { NullableType } from '../utils/types/nullable.type';
import { User } from '../users/domain/user';
import { RefreshResponseDto } from './dto/refresh-response.dto';
import { ConfigService } from '@nestjs/config';
import { AllConfigType } from '../config/config.type';
import { LanguageEnum } from '../i18n/language.enum';

@ApiTags('Auth')
@Controller({
path: 'auth',
version: '1',
})
export class AuthController {
constructor(private readonly service: AuthService) {}
constructor(
private readonly service: AuthService,
private readonly configService: ConfigService<AllConfigType>,
) {}

@SerializeOptions({
groups: ['me'],
Expand All @@ -47,8 +54,20 @@ export class AuthController {

@Post('email/register')
@HttpCode(HttpStatus.NO_CONTENT)
async register(@Body() createUserDto: AuthRegisterLoginDto): Promise<void> {
return this.service.register(createUserDto);
async register(
@Body() createUserDto: AuthRegisterLoginDto,
@Headers() headers: Record<string, string>,
): Promise<void> {
if (createUserDto.language) {
return this.service.register(createUserDto, createUserDto.language);
}

const headerLanguage =
this.configService.get('app.headerLanguage', { infer: true }) ||
'x-custom-lang';
const language = headers[headerLanguage.toLowerCase()] as LanguageEnum;

return this.service.register(createUserDto, language);
}

@Post('email/confirm')
Expand All @@ -71,8 +90,21 @@ export class AuthController {
@HttpCode(HttpStatus.NO_CONTENT)
async forgotPassword(
@Body() forgotPasswordDto: AuthForgotPasswordDto,
@Headers() headers: Record<string, string>,
): Promise<void> {
return this.service.forgotPassword(forgotPasswordDto.email);
if (forgotPasswordDto.language) {
return this.service.forgotPassword(
forgotPasswordDto.email,
forgotPasswordDto.language,
);
}

const headerLanguage =
this.configService.get('app.headerLanguage', { infer: true }) ||
'x-custom-lang';
const language = headers[headerLanguage.toLowerCase()] as LanguageEnum;

return this.service.forgotPassword(forgotPasswordDto.email, language);
}

@Post('reset/password')
Expand Down Expand Up @@ -138,8 +170,18 @@ export class AuthController {
public update(
@Request() request,
@Body() userDto: AuthUpdateDto,
@Headers() headers: Record<string, string>,
): Promise<NullableType<User>> {
return this.service.update(request.user, userDto);
if (userDto.language) {
return this.service.update(request.user, userDto, userDto.language);
}

const headerLanguage =
this.configService.get('app.headerLanguage', { infer: true }) ||
'x-custom-lang';
const language = headers[headerLanguage.toLowerCase()] as LanguageEnum;

return this.service.update(request.user, userDto, language);
}

@ApiBearerAuth()
Expand Down
50 changes: 32 additions & 18 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Session } from '../session/domain/session';
import { SessionService } from '../session/session.service';
import { StatusEnum } from '../statuses/statuses.enum';
import { User } from '../users/domain/user';
import { LanguageEnum } from '../i18n/language.enum';

@Injectable()
export class AuthService {
Expand Down Expand Up @@ -193,7 +194,10 @@ export class AuthService {
};
}

async register(dto: AuthRegisterLoginDto): Promise<void> {
async register(
dto: AuthRegisterLoginDto,
language?: LanguageEnum,
): Promise<void> {
const user = await this.usersService.create({
...dto,
email: dto.email,
Expand All @@ -219,12 +223,15 @@ export class AuthService {
},
);

await this.mailService.userSignUp({
to: dto.email,
data: {
hash,
await this.mailService.userSignUp(
{
to: dto.email,
data: {
hash,
},
},
});
language,
);
}

async confirmEmail(hash: string): Promise<void> {
Expand Down Expand Up @@ -310,7 +317,7 @@ export class AuthService {
await this.usersService.update(user.id, user);
}

async forgotPassword(email: string): Promise<void> {
async forgotPassword(email: string, language?: LanguageEnum): Promise<void> {
const user = await this.usersService.findByEmail(email);

if (!user) {
Expand Down Expand Up @@ -340,13 +347,16 @@ export class AuthService {
},
);

await this.mailService.forgotPassword({
to: email,
data: {
hash,
tokenExpires,
await this.mailService.forgotPassword(
{
to: email,
data: {
hash,
tokenExpires,
},
},
});
language,
);
}

async resetPassword(hash: string, password: string): Promise<void> {
Expand Down Expand Up @@ -398,6 +408,7 @@ export class AuthService {
async update(
userJwtPayload: JwtPayloadType,
userDto: AuthUpdateDto,
language?: LanguageEnum,
): Promise<NullableType<User>> {
const currentUser = await this.usersService.findById(userJwtPayload.id);

Expand Down Expand Up @@ -476,12 +487,15 @@ export class AuthService {
},
);

await this.mailService.confirmNewEmail({
to: userDto.email,
data: {
hash,
await this.mailService.confirmNewEmail(
{
to: userDto.email,
data: {
hash,
},
},
});
language,
);
}

delete userDto.email;
Expand Down
14 changes: 12 additions & 2 deletions src/auth/dto/auth-forgot-password.dto.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEmail, IsEnum, IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';
import { lowerCaseTransformer } from '../../utils/transformers/lower-case.transformer';
import { LanguageEnum } from '../../i18n/language.enum';

export class AuthForgotPasswordDto {
@ApiProperty({ example: '[email protected]', type: String })
@Transform(lowerCaseTransformer)
@IsEmail()
email: string;

@ApiPropertyOptional({
description: 'Preferred language for email',
enum: LanguageEnum,
example: LanguageEnum.English,
})
@IsOptional()
@IsEnum(LanguageEnum)
language?: LanguageEnum;
}
20 changes: 18 additions & 2 deletions src/auth/dto/auth-register-login.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsEmail,
IsEnum,
IsNotEmpty,
IsOptional,
MinLength,
} from 'class-validator';
import { Transform } from 'class-transformer';
import { lowerCaseTransformer } from '../../utils/transformers/lower-case.transformer';
import { LanguageEnum } from '../../i18n/language.enum';

export class AuthRegisterLoginDto {
@ApiProperty({ example: '[email protected]', type: String })
Expand All @@ -20,4 +27,13 @@ export class AuthRegisterLoginDto {
@ApiProperty({ example: 'Doe' })
@IsNotEmpty()
lastName: string;

@ApiPropertyOptional({
description: 'Preferred language for email',
enum: LanguageEnum,
example: LanguageEnum.English,
})
@IsOptional()
@IsEnum(LanguageEnum)
language?: LanguageEnum;
}
18 changes: 17 additions & 1 deletion src/auth/dto/auth-update.dto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsOptional, MinLength } from 'class-validator';
import {
IsEmail,
IsEnum,
IsNotEmpty,
IsOptional,
MinLength,
} from 'class-validator';
import { FileDto } from '../../files/dto/file.dto';
import { Transform } from 'class-transformer';
import { lowerCaseTransformer } from '../../utils/transformers/lower-case.transformer';
import { LanguageEnum } from '../../i18n/language.enum';

export class AuthUpdateDto {
@ApiPropertyOptional({ type: () => FileDto })
Expand Down Expand Up @@ -36,4 +43,13 @@ export class AuthUpdateDto {
@IsOptional()
@IsNotEmpty({ message: 'mustBeNotEmpty' })
oldPassword?: string;

@ApiPropertyOptional({
description: 'Preferred language for email',
enum: LanguageEnum,
example: LanguageEnum.English,
})
@IsOptional()
@IsEnum(LanguageEnum)
language?: LanguageEnum;
}
4 changes: 4 additions & 0 deletions src/i18n/ar/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"confirmEmail": "تأكيد البريد الإلكتروني",
"resetPassword": "إعادة تعيين كلمة المرور"
}
5 changes: 5 additions & 0 deletions src/i18n/ar/confirm-email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"text1": "مرحباً!",
"text2": "أنت على وشك البدء في الاستمتاع",
"text3": "ما عليك سوى النقر على الزر الأخضر الكبير أدناه للتحقق من عنوان بريدك الإلكتروني."
}
5 changes: 5 additions & 0 deletions src/i18n/ar/confirm-new-email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"text1": "مرحباً!",
"text2": "تأكيد عنوان بريدك الإلكتروني الجديد.",
"text3": "ما عليك سوى النقر على الزر الأخضر الكبير أدناه للتحقق من عنوان بريدك الإلكتروني."
}
6 changes: 6 additions & 0 deletions src/i18n/ar/reset-password.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"text1": "مشاكل في تسجيل الدخول؟",
"text2": "إعادة تعيين كلمة المرور الخاصة بك أمر سهل.",
"text3": "ما عليك سوى الضغط على الزر أدناه واتباع التعليمات. سنساعدك على العودة في أسرع وقت.",
"text4": "إذا لم تقم بهذا الطلب، فالرجاء تجاهل هذا البريد الإلكتروني."
}
4 changes: 4 additions & 0 deletions src/i18n/de/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"confirmEmail": "E-Mail bestätigen",
"resetPassword": "Passwort zurücksetzen"
}
5 changes: 5 additions & 0 deletions src/i18n/de/confirm-email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"text1": "Hallo!",
"text2": "Sie sind fast bereit, mit der Nutzung zu beginnen",
"text3": "Klicken Sie einfach auf die große grüne Schaltfläche unten, um Ihre E-Mail-Adresse zu verifizieren."
}
5 changes: 5 additions & 0 deletions src/i18n/de/confirm-new-email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"text1": "Hallo!",
"text2": "Bestätigen Sie Ihre neue E-Mail-Adresse.",
"text3": "Klicken Sie einfach auf die große grüne Schaltfläche unten, um Ihre E-Mail-Adresse zu verifizieren."
}
6 changes: 6 additions & 0 deletions src/i18n/de/reset-password.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"text1": "Probleme bei der Anmeldung?",
"text2": "Ihr Passwort zurückzusetzen ist einfach.",
"text3": "Drücken Sie einfach die Schaltfläche unten und folgen Sie den Anweisungen. Wir haben Sie in Kürze wieder am Laufen.",
"text4": "Wenn Sie diese Anfrage nicht gestellt haben, ignorieren Sie bitte diese E-Mail."
}
4 changes: 4 additions & 0 deletions src/i18n/es/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"confirmEmail": "Confirmar correo electrónico",
"resetPassword": "Restablecer contraseña"
}
5 changes: 5 additions & 0 deletions src/i18n/es/confirm-email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"text1": "¡Hola!",
"text2": "Ya casi estás listo para comenzar a disfrutar",
"text3": "Simplemente haz clic en el botón verde de abajo para verificar tu dirección de correo electrónico."
}
5 changes: 5 additions & 0 deletions src/i18n/es/confirm-new-email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"text1": "¡Hola!",
"text2": "Confirma tu nueva dirección de correo electrónico.",
"text3": "Simplemente haz clic en el botón verde de abajo para verificar tu dirección de correo electrónico."
}
6 changes: 6 additions & 0 deletions src/i18n/es/reset-password.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"text1": "¿Problemas para iniciar sesión?",
"text2": "Restablecer tu contraseña es fácil.",
"text3": "Solo presiona el botón de abajo y sigue las instrucciones. Te tendremos listo en poco tiempo.",
"text4": "Si no realizaste esta solicitud, ignora este correo electrónico."
}
4 changes: 4 additions & 0 deletions src/i18n/fr/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"confirmEmail": "Confirmer l'email",
"resetPassword": "Réinitialiser le mot de passe"
}
5 changes: 5 additions & 0 deletions src/i18n/fr/confirm-email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"text1": "Bonjour !",
"text2": "Vous êtes sur le point de commencer à profiter",
"text3": "Cliquez simplement sur le gros bouton vert ci-dessous pour vérifier votre adresse e-mail."
}
5 changes: 5 additions & 0 deletions src/i18n/fr/confirm-new-email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"text1": "Bonjour !",
"text2": "Confirmez votre nouvelle adresse e-mail.",
"text3": "Cliquez simplement sur le gros bouton vert ci-dessous pour vérifier votre adresse e-mail."
}
6 changes: 6 additions & 0 deletions src/i18n/fr/reset-password.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"text1": "Problèmes de connexion ?",
"text2": "Réinitialiser votre mot de passe est facile.",
"text3": "Il suffit d'appuyer sur le bouton ci-dessous et de suivre les instructions. Nous vous aurons opérationnel en un rien de temps.",
"text4": "Si vous n'avez pas fait cette demande, veuillez ignorer cet e-mail."
}
Loading