diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index dff88f7a2..597f62301 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -10,6 +10,7 @@ import { Patch, Delete, SerializeOptions, + Headers, } from '@nestjs/common'; import { AuthService } from './auth.service'; import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger'; @@ -24,6 +25,9 @@ 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({ @@ -31,7 +35,10 @@ import { RefreshResponseDto } from './dto/refresh-response.dto'; version: '1', }) export class AuthController { - constructor(private readonly service: AuthService) {} + constructor( + private readonly service: AuthService, + private readonly configService: ConfigService, + ) {} @SerializeOptions({ groups: ['me'], @@ -47,8 +54,20 @@ export class AuthController { @Post('email/register') @HttpCode(HttpStatus.NO_CONTENT) - async register(@Body() createUserDto: AuthRegisterLoginDto): Promise { - return this.service.register(createUserDto); + async register( + @Body() createUserDto: AuthRegisterLoginDto, + @Headers() headers: Record, + ): Promise { + 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') @@ -71,8 +90,21 @@ export class AuthController { @HttpCode(HttpStatus.NO_CONTENT) async forgotPassword( @Body() forgotPasswordDto: AuthForgotPasswordDto, + @Headers() headers: Record, ): Promise { - 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') @@ -138,8 +170,18 @@ export class AuthController { public update( @Request() request, @Body() userDto: AuthUpdateDto, + @Headers() headers: Record, ): Promise> { - 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() diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index d3adff954..901d4d2d0 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -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 { @@ -193,7 +194,10 @@ export class AuthService { }; } - async register(dto: AuthRegisterLoginDto): Promise { + async register( + dto: AuthRegisterLoginDto, + language?: LanguageEnum, + ): Promise { const user = await this.usersService.create({ ...dto, email: dto.email, @@ -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 { @@ -310,7 +317,7 @@ export class AuthService { await this.usersService.update(user.id, user); } - async forgotPassword(email: string): Promise { + async forgotPassword(email: string, language?: LanguageEnum): Promise { const user = await this.usersService.findByEmail(email); if (!user) { @@ -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 { @@ -398,6 +408,7 @@ export class AuthService { async update( userJwtPayload: JwtPayloadType, userDto: AuthUpdateDto, + language?: LanguageEnum, ): Promise> { const currentUser = await this.usersService.findById(userJwtPayload.id); @@ -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; diff --git a/src/auth/dto/auth-forgot-password.dto.ts b/src/auth/dto/auth-forgot-password.dto.ts index 2aac90194..fd88f9224 100644 --- a/src/auth/dto/auth-forgot-password.dto.ts +++ b/src/auth/dto/auth-forgot-password.dto.ts @@ -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: 'test1@example.com', type: String }) @Transform(lowerCaseTransformer) @IsEmail() email: string; + + @ApiPropertyOptional({ + description: 'Preferred language for email', + enum: LanguageEnum, + example: LanguageEnum.English, + }) + @IsOptional() + @IsEnum(LanguageEnum) + language?: LanguageEnum; } diff --git a/src/auth/dto/auth-register-login.dto.ts b/src/auth/dto/auth-register-login.dto.ts index 0a88afdec..378d72fb5 100644 --- a/src/auth/dto/auth-register-login.dto.ts +++ b/src/auth/dto/auth-register-login.dto.ts @@ -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: 'test1@example.com', type: String }) @@ -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; } diff --git a/src/auth/dto/auth-update.dto.ts b/src/auth/dto/auth-update.dto.ts index 08a1c2a28..dede6892a 100644 --- a/src/auth/dto/auth-update.dto.ts +++ b/src/auth/dto/auth-update.dto.ts @@ -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 }) @@ -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; } diff --git a/src/i18n/ar/common.json b/src/i18n/ar/common.json new file mode 100644 index 000000000..83cc74560 --- /dev/null +++ b/src/i18n/ar/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "تأكيد البريد الإلكتروني", + "resetPassword": "إعادة تعيين كلمة المرور" +} \ No newline at end of file diff --git a/src/i18n/ar/confirm-email.json b/src/i18n/ar/confirm-email.json new file mode 100644 index 000000000..fbaa3ed81 --- /dev/null +++ b/src/i18n/ar/confirm-email.json @@ -0,0 +1,5 @@ +{ + "text1": "مرحباً!", + "text2": "أنت على وشك البدء في الاستمتاع", + "text3": "ما عليك سوى النقر على الزر الأخضر الكبير أدناه للتحقق من عنوان بريدك الإلكتروني." +} \ No newline at end of file diff --git a/src/i18n/ar/confirm-new-email.json b/src/i18n/ar/confirm-new-email.json new file mode 100644 index 000000000..e4dc4e1ee --- /dev/null +++ b/src/i18n/ar/confirm-new-email.json @@ -0,0 +1,5 @@ +{ + "text1": "مرحباً!", + "text2": "تأكيد عنوان بريدك الإلكتروني الجديد.", + "text3": "ما عليك سوى النقر على الزر الأخضر الكبير أدناه للتحقق من عنوان بريدك الإلكتروني." +} \ No newline at end of file diff --git a/src/i18n/ar/reset-password.json b/src/i18n/ar/reset-password.json new file mode 100644 index 000000000..657ae2a5d --- /dev/null +++ b/src/i18n/ar/reset-password.json @@ -0,0 +1,6 @@ +{ + "text1": "مشاكل في تسجيل الدخول؟", + "text2": "إعادة تعيين كلمة المرور الخاصة بك أمر سهل.", + "text3": "ما عليك سوى الضغط على الزر أدناه واتباع التعليمات. سنساعدك على العودة في أسرع وقت.", + "text4": "إذا لم تقم بهذا الطلب، فالرجاء تجاهل هذا البريد الإلكتروني." +} \ No newline at end of file diff --git a/src/i18n/de/common.json b/src/i18n/de/common.json new file mode 100644 index 000000000..93301344d --- /dev/null +++ b/src/i18n/de/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "E-Mail bestätigen", + "resetPassword": "Passwort zurücksetzen" +} \ No newline at end of file diff --git a/src/i18n/de/confirm-email.json b/src/i18n/de/confirm-email.json new file mode 100644 index 000000000..3117873a2 --- /dev/null +++ b/src/i18n/de/confirm-email.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/de/confirm-new-email.json b/src/i18n/de/confirm-new-email.json new file mode 100644 index 000000000..9c3f455f9 --- /dev/null +++ b/src/i18n/de/confirm-new-email.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/de/reset-password.json b/src/i18n/de/reset-password.json new file mode 100644 index 000000000..87dcf929d --- /dev/null +++ b/src/i18n/de/reset-password.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/es/common.json b/src/i18n/es/common.json new file mode 100644 index 000000000..1a50debdb --- /dev/null +++ b/src/i18n/es/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "Confirmar correo electrónico", + "resetPassword": "Restablecer contraseña" +} \ No newline at end of file diff --git a/src/i18n/es/confirm-email.json b/src/i18n/es/confirm-email.json new file mode 100644 index 000000000..c81a1da46 --- /dev/null +++ b/src/i18n/es/confirm-email.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/es/confirm-new-email.json b/src/i18n/es/confirm-new-email.json new file mode 100644 index 000000000..7cb005f58 --- /dev/null +++ b/src/i18n/es/confirm-new-email.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/es/reset-password.json b/src/i18n/es/reset-password.json new file mode 100644 index 000000000..7575fc62e --- /dev/null +++ b/src/i18n/es/reset-password.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/fr/common.json b/src/i18n/fr/common.json new file mode 100644 index 000000000..86d26468a --- /dev/null +++ b/src/i18n/fr/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "Confirmer l'email", + "resetPassword": "Réinitialiser le mot de passe" +} \ No newline at end of file diff --git a/src/i18n/fr/confirm-email.json b/src/i18n/fr/confirm-email.json new file mode 100644 index 000000000..893386770 --- /dev/null +++ b/src/i18n/fr/confirm-email.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/fr/confirm-new-email.json b/src/i18n/fr/confirm-new-email.json new file mode 100644 index 000000000..c0799788a --- /dev/null +++ b/src/i18n/fr/confirm-new-email.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/fr/reset-password.json b/src/i18n/fr/reset-password.json new file mode 100644 index 000000000..e9fa6325d --- /dev/null +++ b/src/i18n/fr/reset-password.json @@ -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." +} \ No newline at end of file diff --git a/src/i18n/it/common.json b/src/i18n/it/common.json new file mode 100644 index 000000000..8a136760b --- /dev/null +++ b/src/i18n/it/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "Conferma email", + "resetPassword": "Reimposta password" +} \ No newline at end of file diff --git a/src/i18n/it/confirm-email.json b/src/i18n/it/confirm-email.json new file mode 100644 index 000000000..6cb7dddf8 --- /dev/null +++ b/src/i18n/it/confirm-email.json @@ -0,0 +1,5 @@ +{ + "text1": "Ciao!", + "text2": "Sei quasi pronto per iniziare a utilizzare", + "text3": "Basta cliccare sul grande pulsante verde qui sotto per verificare il tuo indirizzo email." +} \ No newline at end of file diff --git a/src/i18n/it/confirm-new-email.json b/src/i18n/it/confirm-new-email.json new file mode 100644 index 000000000..b669567ed --- /dev/null +++ b/src/i18n/it/confirm-new-email.json @@ -0,0 +1,5 @@ +{ + "text1": "Ciao!", + "text2": "Conferma il tuo nuovo indirizzo email.", + "text3": "Basta cliccare sul grande pulsante verde qui sotto per verificare il tuo indirizzo email." +} \ No newline at end of file diff --git a/src/i18n/it/reset-password.json b/src/i18n/it/reset-password.json new file mode 100644 index 000000000..5f5d0d8f7 --- /dev/null +++ b/src/i18n/it/reset-password.json @@ -0,0 +1,6 @@ +{ + "text1": "Problemi con l'accesso?", + "text2": "Reimpostare la password è facile.", + "text3": "Basta premere il pulsante qui sotto e seguire le istruzioni. Ti aiuteremo a tornare operativo in pochissimo tempo.", + "text4": "Se non hai fatto questa richiesta, ignora questa email." +} \ No newline at end of file diff --git a/src/i18n/ja/common.json b/src/i18n/ja/common.json new file mode 100644 index 000000000..7c9b1fa35 --- /dev/null +++ b/src/i18n/ja/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "メールの確認", + "resetPassword": "パスワードをリセット" +} \ No newline at end of file diff --git a/src/i18n/ja/confirm-email.json b/src/i18n/ja/confirm-email.json new file mode 100644 index 000000000..2aaff375d --- /dev/null +++ b/src/i18n/ja/confirm-email.json @@ -0,0 +1,5 @@ +{ + "text1": "こんにちは!", + "text2": "利用開始まであと少しです", + "text3": "下の緑色のボタンをクリックして、メールアドレスを確認してください。" +} \ No newline at end of file diff --git a/src/i18n/ja/confirm-new-email.json b/src/i18n/ja/confirm-new-email.json new file mode 100644 index 000000000..0619dc498 --- /dev/null +++ b/src/i18n/ja/confirm-new-email.json @@ -0,0 +1,5 @@ +{ + "text1": "こんにちは!", + "text2": "新しいメールアドレスを確認してください。", + "text3": "下の緑色のボタンをクリックして、メールアドレスを確認してください。" +} \ No newline at end of file diff --git a/src/i18n/ja/reset-password.json b/src/i18n/ja/reset-password.json new file mode 100644 index 000000000..8638f1d8d --- /dev/null +++ b/src/i18n/ja/reset-password.json @@ -0,0 +1,6 @@ +{ + "text1": "ログインに問題がありますか?", + "text2": "パスワードのリセットは簡単です。", + "text3": "下のボタンを押して指示に従ってください。すぐに利用できるようになります。", + "text4": "このリクエストをしていない場合は、このメールを無視してください。" +} \ No newline at end of file diff --git a/src/i18n/language.enum.ts b/src/i18n/language.enum.ts new file mode 100644 index 000000000..f5aff412a --- /dev/null +++ b/src/i18n/language.enum.ts @@ -0,0 +1,16 @@ +/** + * Enumeration of supported languages in the application + */ +export enum LanguageEnum { + English = 'en', + Chinese = 'zh', + Spanish = 'es', + French = 'fr', + German = 'de', + Japanese = 'ja', + Russian = 'ru', + Portuguese = 'pt', + Arabic = 'ar', + Italian = 'it', + Ukrainian = 'uk', +} diff --git a/src/i18n/pt/common.json b/src/i18n/pt/common.json new file mode 100644 index 000000000..ca6f2612f --- /dev/null +++ b/src/i18n/pt/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "Confirmar e-mail", + "resetPassword": "Redefinir senha" +} \ No newline at end of file diff --git a/src/i18n/pt/confirm-email.json b/src/i18n/pt/confirm-email.json new file mode 100644 index 000000000..1199bb4b5 --- /dev/null +++ b/src/i18n/pt/confirm-email.json @@ -0,0 +1,5 @@ +{ + "text1": "Olá!", + "text2": "Você está quase pronto para começar a aproveitar", + "text3": "Basta clicar no botão verde abaixo para verificar seu endereço de e-mail." +} \ No newline at end of file diff --git a/src/i18n/pt/confirm-new-email.json b/src/i18n/pt/confirm-new-email.json new file mode 100644 index 000000000..9dacea8fb --- /dev/null +++ b/src/i18n/pt/confirm-new-email.json @@ -0,0 +1,5 @@ +{ + "text1": "Olá!", + "text2": "Confirme seu novo endereço de e-mail.", + "text3": "Basta clicar no botão verde abaixo para verificar seu endereço de e-mail." +} \ No newline at end of file diff --git a/src/i18n/pt/reset-password.json b/src/i18n/pt/reset-password.json new file mode 100644 index 000000000..5f6f13070 --- /dev/null +++ b/src/i18n/pt/reset-password.json @@ -0,0 +1,6 @@ +{ + "text1": "Problemas para entrar?", + "text2": "Redefinir sua senha é fácil.", + "text3": "Basta pressionar o botão abaixo e seguir as instruções. Nós te ajudaremos a voltar rapidamente.", + "text4": "Se você não fez esta solicitação, por favor ignore este e-mail." +} \ No newline at end of file diff --git a/src/i18n/ru/common.json b/src/i18n/ru/common.json new file mode 100644 index 000000000..fab07a897 --- /dev/null +++ b/src/i18n/ru/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "Подтвердить email", + "resetPassword": "Сбросить пароль" +} \ No newline at end of file diff --git a/src/i18n/ru/confirm-email.json b/src/i18n/ru/confirm-email.json new file mode 100644 index 000000000..868f092b2 --- /dev/null +++ b/src/i18n/ru/confirm-email.json @@ -0,0 +1,5 @@ +{ + "text1": "Привет!", + "text2": "Вы почти готовы начать пользоваться", + "text3": "Просто нажмите на большую зеленую кнопку ниже, чтобы подтвердить адрес электронной почты." +} \ No newline at end of file diff --git a/src/i18n/ru/confirm-new-email.json b/src/i18n/ru/confirm-new-email.json new file mode 100644 index 000000000..c3eb42ef0 --- /dev/null +++ b/src/i18n/ru/confirm-new-email.json @@ -0,0 +1,5 @@ +{ + "text1": "Привет!", + "text2": "Подтвердите ваш новый адрес электронной почты.", + "text3": "Просто нажмите на большую зеленую кнопку ниже, чтобы подтвердить адрес электронной почты." +} \ No newline at end of file diff --git a/src/i18n/ru/reset-password.json b/src/i18n/ru/reset-password.json new file mode 100644 index 000000000..38b75604b --- /dev/null +++ b/src/i18n/ru/reset-password.json @@ -0,0 +1,6 @@ +{ + "text1": "Проблемы со входом?", + "text2": "Сбросить пароль легко.", + "text3": "Просто нажмите на кнопку ниже и следуйте инструкциям. Вы сможете продолжить работу в кратчайшие сроки.", + "text4": "Если вы не делали этот запрос, пожалуйста, проигнорируйте это письмо." +} \ No newline at end of file diff --git a/src/i18n/uk/common.json b/src/i18n/uk/common.json new file mode 100644 index 000000000..0a09f078f --- /dev/null +++ b/src/i18n/uk/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "Підтвердити електронну пошту", + "resetPassword": "Скинути пароль" +} \ No newline at end of file diff --git a/src/i18n/uk/confirm-email.json b/src/i18n/uk/confirm-email.json new file mode 100644 index 000000000..ee8aa7699 --- /dev/null +++ b/src/i18n/uk/confirm-email.json @@ -0,0 +1,5 @@ +{ + "text1": "Привіт!", + "text2": "Ви майже готові почати користуватися", + "text3": "Просто натисніть на велику зелену кнопку нижче, щоб підтвердити свою електронну адресу." +} \ No newline at end of file diff --git a/src/i18n/uk/confirm-new-email.json b/src/i18n/uk/confirm-new-email.json new file mode 100644 index 000000000..e951e9265 --- /dev/null +++ b/src/i18n/uk/confirm-new-email.json @@ -0,0 +1,5 @@ +{ + "text1": "Привіт!", + "text2": "Підтвердіть свою нову електронну адресу.", + "text3": "Просто натисніть на велику зелену кнопку нижче, щоб підтвердити свою електронну адресу." +} \ No newline at end of file diff --git a/src/i18n/uk/reset-password.json b/src/i18n/uk/reset-password.json new file mode 100644 index 000000000..590f029e3 --- /dev/null +++ b/src/i18n/uk/reset-password.json @@ -0,0 +1,6 @@ +{ + "text1": "Проблеми з входом?", + "text2": "Скинути пароль дуже просто.", + "text3": "Просто натисніть кнопку нижче та дотримуйтесь інструкцій. Ми допоможемо вам повернутися до роботи дуже швидко.", + "text4": "Якщо ви не робили цього запиту, будь ласка, проігноруйте цей email." +} \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json new file mode 100644 index 000000000..2f90aa3cf --- /dev/null +++ b/src/i18n/zh/common.json @@ -0,0 +1,4 @@ +{ + "confirmEmail": "确认邮箱", + "resetPassword": "重置密码" +} \ No newline at end of file diff --git a/src/i18n/zh/confirm-email.json b/src/i18n/zh/confirm-email.json new file mode 100644 index 000000000..6e100ffeb --- /dev/null +++ b/src/i18n/zh/confirm-email.json @@ -0,0 +1,5 @@ +{ + "text1": "您好!", + "text2": "您即将开始使用我们的服务", + "text3": "请点击下方的绿色按钮来验证您的邮箱地址。" +} \ No newline at end of file diff --git a/src/i18n/zh/confirm-new-email.json b/src/i18n/zh/confirm-new-email.json new file mode 100644 index 000000000..1b59a0b77 --- /dev/null +++ b/src/i18n/zh/confirm-new-email.json @@ -0,0 +1,5 @@ +{ + "text1": "您好!", + "text2": "请确认您的新邮箱地址。", + "text3": "请点击下方的绿色按钮来验证您的邮箱地址。" +} \ No newline at end of file diff --git a/src/i18n/zh/reset-password.json b/src/i18n/zh/reset-password.json new file mode 100644 index 000000000..a1bed367c --- /dev/null +++ b/src/i18n/zh/reset-password.json @@ -0,0 +1,6 @@ +{ + "text1": "登录遇到问题?", + "text2": "重置密码很简单。", + "text3": "只需点击下方按钮并按照说明操作。我们会尽快帮您恢复访问。", + "text4": "如果您没有请求此操作,请忽略此邮件。" +} \ No newline at end of file diff --git a/src/mail/mail-templates/activation.hbs b/src/mail/mail-templates/activation.hbs index edee05e66..4497f11f2 100644 --- a/src/mail/mail-templates/activation.hbs +++ b/src/mail/mail-templates/activation.hbs @@ -4,7 +4,7 @@ - {{title}} + {{t 'common.confirmEmail' lang=lang}} @@ -16,15 +16,15 @@ - {{text1}}
- {{text2}} {{app_name}}.
- {{text3}} + {{t 'confirm-email.text1' lang=lang}}
+ {{t 'confirm-email.text2' lang=lang}}
+ {{t 'confirm-email.text3' lang=lang}} {{actionTitle}} + style="display:inline-block;padding:20px;background:#00838f;text-decoration:none;color:#ffffff">{{t 'common.confirmEmail' lang=lang}} diff --git a/src/mail/mail-templates/confirm-new-email.hbs b/src/mail/mail-templates/confirm-new-email.hbs index b0c26dbed..ea1cabcc4 100644 --- a/src/mail/mail-templates/confirm-new-email.hbs +++ b/src/mail/mail-templates/confirm-new-email.hbs @@ -4,7 +4,7 @@ - {{title}} + {{t 'common.confirmEmail' lang=lang}} @@ -16,15 +16,15 @@ - {{text1}}
- {{text2}}
- {{text3}} + {{t 'confirm-new-email.text1' lang=lang}}
+ {{t 'confirm-new-email.text2' lang=lang}}
+ {{t 'confirm-new-email.text3' lang=lang}} {{actionTitle}} + style="display:inline-block;padding:20px;background:#00838f;text-decoration:none;color:#ffffff">{{t 'common.confirmEmail' lang=lang}} diff --git a/src/mail/mail-templates/reset-password.hbs b/src/mail/mail-templates/reset-password.hbs index 3c0e4050d..f91fbd8e8 100644 --- a/src/mail/mail-templates/reset-password.hbs +++ b/src/mail/mail-templates/reset-password.hbs @@ -4,7 +4,7 @@ - {{title}} + {{t 'common.resetPassword' lang=lang}} @@ -16,20 +16,16 @@ - {{text1}}
- {{text2}}
- {{text3}} + {{t 'reset-password.text1' lang=lang}}
+ {{t 'reset-password.text2' lang=lang}}
+ {{t 'reset-password.text3' lang=lang}}

+ {{t 'reset-password.text4' lang=lang}} {{actionTitle}} - - - - - {{text4}} + style="display:inline-block;padding:20px;background:#00838f;text-decoration:none;color:#ffffff">{{t 'common.resetPassword' lang=lang}} diff --git a/src/mail/mail.service.ts b/src/mail/mail.service.ts index 3bdfd44a2..1880c8f6f 100644 --- a/src/mail/mail.service.ts +++ b/src/mail/mail.service.ts @@ -1,168 +1,146 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { I18nContext } from 'nestjs-i18n'; +import { I18nContext, I18nService } from 'nestjs-i18n'; import { MailData } from './interfaces/mail-data.interface'; -import { MaybeType } from '../utils/types/maybe.type'; import { MailerService } from '../mailer/mailer.service'; import path from 'path'; import { AllConfigType } from '../config/config.type'; +import { LanguageEnum } from '../i18n/language.enum'; @Injectable() export class MailService { constructor( private readonly mailerService: MailerService, private readonly configService: ConfigService, + private readonly i18nService: I18nService, ) {} - async userSignUp(mailData: MailData<{ hash: string }>): Promise { + // Helper to get language with proper typing + private getLanguage(language?: LanguageEnum): LanguageEnum { const i18n = I18nContext.current(); - let emailConfirmTitle: MaybeType; - let text1: MaybeType; - let text2: MaybeType; - let text3: MaybeType; - - if (i18n) { - [emailConfirmTitle, text1, text2, text3] = await Promise.all([ - i18n.t('common.confirmEmail'), - i18n.t('confirm-email.text1'), - i18n.t('confirm-email.text2'), - i18n.t('confirm-email.text3'), - ]); + const fallback = this.configService.get('app.fallbackLanguage', { + infer: true, + }) as LanguageEnum; + return (language || i18n?.lang || fallback) as LanguageEnum; + } + + // Helper to get email subject with i18n support + private async getSubject( + key: string, + language?: LanguageEnum, + ): Promise { + const lang = this.getLanguage(language); + try { + const translation = await this.i18nService.translate(key, { lang }); + return translation as string; + } catch { + // Fallback if translation fails + return key.split('.').pop() || 'Email Notification'; } + } + // Helper to get template path + private getTemplatePath(templateName: string): string { + return path.join( + this.configService.getOrThrow('app.workingDirectory', { infer: true }), + 'src', + 'mail', + 'mail-templates', + templateName, + ); + } + + async userSignUp( + mailData: MailData<{ hash: string }>, + language?: LanguageEnum, + ): Promise { + // Get language + const lang = this.getLanguage(language); + + // Generate URL const url = new URL( - this.configService.getOrThrow('app.frontendDomain', { - infer: true, - }) + '/confirm-email', + this.configService.getOrThrow('app.frontendDomain', { infer: true }) + + '/confirm-email', ); url.searchParams.set('hash', mailData.data.hash); + // Get translated subject + const subject = await this.getSubject('common.confirmEmail', language); + + // Send email await this.mailerService.sendMail({ to: mailData.to, - subject: emailConfirmTitle, - text: `${url.toString()} ${emailConfirmTitle}`, - templatePath: path.join( - this.configService.getOrThrow('app.workingDirectory', { - infer: true, - }), - 'src', - 'mail', - 'mail-templates', - 'activation.hbs', - ), + subject, + text: url.toString(), + templatePath: this.getTemplatePath('activation.hbs'), context: { - title: emailConfirmTitle, url: url.toString(), - actionTitle: emailConfirmTitle, app_name: this.configService.get('app.name', { infer: true }), - text1, - text2, - text3, + lang, }, }); } async forgotPassword( mailData: MailData<{ hash: string; tokenExpires: number }>, + language?: LanguageEnum, ): Promise { - const i18n = I18nContext.current(); - let resetPasswordTitle: MaybeType; - let text1: MaybeType; - let text2: MaybeType; - let text3: MaybeType; - let text4: MaybeType; - - if (i18n) { - [resetPasswordTitle, text1, text2, text3, text4] = await Promise.all([ - i18n.t('common.resetPassword'), - i18n.t('reset-password.text1'), - i18n.t('reset-password.text2'), - i18n.t('reset-password.text3'), - i18n.t('reset-password.text4'), - ]); - } + // Get language + const lang = this.getLanguage(language); + // Generate URL const url = new URL( - this.configService.getOrThrow('app.frontendDomain', { - infer: true, - }) + '/password-change', + this.configService.getOrThrow('app.frontendDomain', { infer: true }) + + '/password-change', ); url.searchParams.set('hash', mailData.data.hash); url.searchParams.set('expires', mailData.data.tokenExpires.toString()); + // Get translated subject + const subject = await this.getSubject('common.resetPassword', language); + + // Send email await this.mailerService.sendMail({ to: mailData.to, - subject: resetPasswordTitle, - text: `${url.toString()} ${resetPasswordTitle}`, - templatePath: path.join( - this.configService.getOrThrow('app.workingDirectory', { - infer: true, - }), - 'src', - 'mail', - 'mail-templates', - 'reset-password.hbs', - ), + subject, + text: url.toString(), + templatePath: this.getTemplatePath('reset-password.hbs'), context: { - title: resetPasswordTitle, url: url.toString(), - actionTitle: resetPasswordTitle, - app_name: this.configService.get('app.name', { - infer: true, - }), - text1, - text2, - text3, - text4, + app_name: this.configService.get('app.name', { infer: true }), + lang, }, }); } - async confirmNewEmail(mailData: MailData<{ hash: string }>): Promise { - const i18n = I18nContext.current(); - let emailConfirmTitle: MaybeType; - let text1: MaybeType; - let text2: MaybeType; - let text3: MaybeType; - - if (i18n) { - [emailConfirmTitle, text1, text2, text3] = await Promise.all([ - i18n.t('common.confirmEmail'), - i18n.t('confirm-new-email.text1'), - i18n.t('confirm-new-email.text2'), - i18n.t('confirm-new-email.text3'), - ]); - } + async confirmNewEmail( + mailData: MailData<{ hash: string }>, + language?: LanguageEnum, + ): Promise { + // Get language + const lang = this.getLanguage(language); + // Generate URL const url = new URL( - this.configService.getOrThrow('app.frontendDomain', { - infer: true, - }) + '/confirm-new-email', + this.configService.getOrThrow('app.frontendDomain', { infer: true }) + + '/confirm-new-email', ); url.searchParams.set('hash', mailData.data.hash); + // Get translated subject + const subject = await this.getSubject('common.confirmEmail', language); + + // Send email await this.mailerService.sendMail({ to: mailData.to, - subject: emailConfirmTitle, - text: `${url.toString()} ${emailConfirmTitle}`, - templatePath: path.join( - this.configService.getOrThrow('app.workingDirectory', { - infer: true, - }), - 'src', - 'mail', - 'mail-templates', - 'confirm-new-email.hbs', - ), + subject, + text: url.toString(), + templatePath: this.getTemplatePath('confirm-new-email.hbs'), context: { - title: emailConfirmTitle, url: url.toString(), - actionTitle: emailConfirmTitle, app_name: this.configService.get('app.name', { infer: true }), - text1, - text2, - text3, + lang, }, }); } diff --git a/src/mailer/mailer.service.ts b/src/mailer/mailer.service.ts index ce24426f3..217bef880 100644 --- a/src/mailer/mailer.service.ts +++ b/src/mailer/mailer.service.ts @@ -4,11 +4,18 @@ import { ConfigService } from '@nestjs/config'; import nodemailer from 'nodemailer'; import Handlebars from 'handlebars'; import { AllConfigType } from '../config/config.type'; +import { I18nContext, I18nService } from 'nestjs-i18n'; +import { LanguageEnum } from '../i18n/language.enum'; @Injectable() export class MailerService { private readonly transporter: nodemailer.Transporter; - constructor(private readonly configService: ConfigService) { + private readonly fallbackLanguage: LanguageEnum; + + constructor( + private readonly configService: ConfigService, + private readonly i18nService: I18nService, + ) { this.transporter = nodemailer.createTransport({ host: configService.get('mail.host', { infer: true }), port: configService.get('mail.port', { infer: true }), @@ -20,6 +27,23 @@ export class MailerService { pass: configService.get('mail.password', { infer: true }), }, }); + + // Store fallback language for reuse + this.fallbackLanguage = this.configService.get('app.fallbackLanguage', { + infer: true, + }) as LanguageEnum; + + // Register i18n helper for Handlebars + Handlebars.registerHelper('t', (key: string, options) => { + const lang = options.hash.lang || this.fallbackLanguage; + return this.i18nService.translate(key, { lang }); + }); + } + + // Helper method to get current language + private getLanguage(contextLang?: string | LanguageEnum): LanguageEnum { + const i18n = I18nContext.current(); + return (contextLang || i18n?.lang || this.fallbackLanguage) as LanguageEnum; } async sendMail({ @@ -28,14 +52,22 @@ export class MailerService { ...mailOptions }: nodemailer.SendMailOptions & { templatePath: string; - context: Record; + context: Record & { lang?: LanguageEnum }; }): Promise { let html: string | undefined; + if (templatePath) { const template = await fs.readFile(templatePath, 'utf-8'); + + // Use helper method to get language + const lang = this.getLanguage(context.lang as LanguageEnum); + html = Handlebars.compile(template, { strict: true, - })(context); + })({ + ...context, + lang, + }); } await this.transporter.sendMail({