setup localization for twenty-emails (#9806)

One of the steps to address #8128 

How to test:
Please change the locale in the settings and click on change password
button. A password reset email in the preferred locale will be sent.


![image](https://github.com/user-attachments/assets/2b0c2f81-5c4d-4e49-b021-8ee76e7872f2)

![image](https://github.com/user-attachments/assets/0453e321-e5aa-42ea-beca-86e2e97dbee2)

Todo:
- Remove the hardcoded locales for invitation, warn suspended workspace
email, clean suspended workspace emails
- Need to test invitation, email verification, warn suspended workspace
email, clean suspended workspace emails
- The duration variable `5 minutes` is always in english. Do we need to
do something about that? It does seems odd in case of chinese
translations.

Notes:
- Only tested the password reset , password update notify templates.
- Cant test email verification due to error during sign up `Internal
server error: New workspace setup is disabled`

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Anne Deepa Prasanna
2025-02-03 01:31:34 +05:30
committed by GitHub
parent 4b9414a002
commit 39e7f6cec3
58 changed files with 1752 additions and 344 deletions

View File

@ -1,5 +1,5 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@ -33,6 +33,7 @@ import { TransientTokenService } from 'src/engine/core-modules/auth/token/servic
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
@ -336,6 +337,7 @@ export class AuthResolver {
@Mutation(() => EmailPasswordResetLink)
async emailPasswordResetLink(
@Args() emailPasswordResetInput: EmailPasswordResetLinkInput,
@Context() context: I18nContext,
): Promise<EmailPasswordResetLink> {
const resetToken =
await this.resetPasswordService.generatePasswordResetToken(
@ -345,6 +347,7 @@ export class AuthResolver {
return await this.resetPasswordService.sendEmailPasswordResetLink(
resetToken,
emailPasswordResetInput.email,
context.req.headers['x-locale'] || 'en',
);
}
@ -352,13 +355,18 @@ export class AuthResolver {
async updatePasswordViaResetToken(
@Args()
{ passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput,
@Context() context: I18nContext,
): Promise<InvalidatePassword> {
const { id } =
await this.resetPasswordService.validatePasswordResetToken(
passwordResetToken,
);
await this.authService.updatePassword(id, newPassword);
await this.authService.updatePassword(
id,
newPassword,
context.req.headers['x-locale'] || 'en',
);
return await this.resetPasswordService.invalidatePasswordResetToken(id);
}

View File

@ -3,10 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import crypto from 'node:crypto';
import { render } from '@react-email/components';
import { render } from '@react-email/render';
import { addMilliseconds } from 'date-fns';
import ms from 'ms';
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
import { APP_LOCALES } from 'twenty-shared';
import { Repository } from 'typeorm';
import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface';
@ -379,6 +380,7 @@ export class AuthService {
async updatePassword(
userId: string,
newPassword: string,
locale: keyof typeof APP_LOCALES,
): Promise<UpdatePassword> {
if (!userId) {
throw new AuthException(
@ -415,14 +417,11 @@ export class AuthService {
userName: `${user.firstName} ${user.lastName}`,
email: user.email,
link: this.domainManagerService.getBaseUrl().toString(),
locale,
});
const html = render(emailTemplate, {
pretty: true,
});
const text = render(emailTemplate, {
plainText: true,
});
const html = render(emailTemplate, { pretty: true });
const text = render(emailTemplate, { plainText: true });
this.emailService.send({
from: `${this.environmentService.get(

View File

@ -9,11 +9,11 @@ import {
AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { ResetPasswordService } from './reset-password.service';
@ -149,6 +149,7 @@ describe('ResetPasswordService', () => {
const result = await service.sendEmailPasswordResetLink(
mockToken,
'test@example.com',
'en',
);
expect(result.success).toBe(true);
@ -162,6 +163,7 @@ describe('ResetPasswordService', () => {
service.sendEmailPasswordResetLink(
{} as any,
'nonexistent@example.com',
'en',
),
).rejects.toThrow(AuthException);
});

View File

@ -7,6 +7,7 @@ import { render } from '@react-email/render';
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
import ms from 'ms';
import { PasswordResetLinkEmail } from 'twenty-emails';
import { APP_LOCALES } from 'twenty-shared';
import { IsNull, MoreThan, Repository } from 'typeorm';
import {
@ -106,6 +107,7 @@ export class ResetPasswordService {
async sendEmailPasswordResetLink(
resetToken: PasswordResetToken,
email: string,
locale: keyof typeof APP_LOCALES,
): Promise<EmailPasswordResetLink> {
const user = await this.userRepository.findOneBy({
email,
@ -133,16 +135,13 @@ export class ResetPasswordService {
long: true,
},
),
locale,
};
const emailTemplate = PasswordResetLinkEmail(emailData);
const html = render(emailTemplate, {
pretty: true,
});
const text = render(emailTemplate, {
plainText: true,
});
const html = render(emailTemplate, { pretty: true });
const text = render(emailTemplate, { plainText: true });
this.emailService.send({
from: `${this.environmentService.get(