Email translation and snackbar translation (#10395)
This pull request focuses on improving localization by replacing hardcoded strings with translatable strings using the `Trans` component from `@lingui/react/macro`. Additionally, it introduces locale support to several email components. Here are the most important changes: ### Localization Improvements: * Replaced hardcoded strings with `Trans` components in various email templates to support localization. (`packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx`, `packages/twenty-emails/src/emails/password-reset-link.email.tsx`, `packages/twenty-emails/src/emails/password-update-notify.email.tsx`, `packages/twenty-emails/src/emails/send-email-verification-link.email.tsx`, `packages/twenty-emails/src/emails/send-invite-link.email.tsx`, `packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx`) [[1]](diffhunk://#diff-ca227a03c0aa66428daff938c743435e8a4dc3ffa960c0952f2697a23e280fdbR6-R25) [[2]](diffhunk://#diff-ca227a03c0aa66428daff938c743435e8a4dc3ffa960c0952f2697a23e280fdbL42-R45) [[3]](diffhunk://#diff-523cd37f5680ce418450946f62b7804b6586158efb190ced73920ef0fdf96bc8L1) [[4]](diffhunk://#diff-523cd37f5680ce418450946f62b7804b6586158efb190ced73920ef0fdf96bc8L23-R23) [[5]](diffhunk://#diff-cf16aa55d3eeb6be606bbe93de4c83b6f146c49b60d6f512d4b87e49fe14338cL29-R29) [[6]](diffhunk://#diff-cf16aa55d3eeb6be606bbe93de4c83b6f146c49b60d6f512d4b87e49fe14338cL46-R46) [[7]](diffhunk://#diff-16b613160f937563ec108176f595d8f275a1d87a5b8245d84df60d775f3efebeL1) [[8]](diffhunk://#diff-16b613160f937563ec108176f595d8f275a1d87a5b8245d84df60d775f3efebeL22-R22) [[9]](diffhunk://#diff-0da62e7cc5cfcb32cc25f067fa1d50123047c239af210398f065455ab6700886L1) [[10]](diffhunk://#diff-0da62e7cc5cfcb32cc25f067fa1d50123047c239af210398f065455ab6700886L42-R41) [[11]](diffhunk://#diff-0da62e7cc5cfcb32cc25f067fa1d50123047c239af210398f065455ab6700886L57-R56) [[12]](diffhunk://#diff-483346065c074946a43c18492334bd680422a1d4cb994dc8c3cd39d0208e6016L1-R21) [[13]](diffhunk://#diff-483346065c074946a43c18492334bd680422a1d4cb994dc8c3cd39d0208e6016L28-R31) [[14]](diffhunk://#diff-483346065c074946a43c18492334bd680422a1d4cb994dc8c3cd39d0208e6016L53-R55) ### Locale Support: * Added `locale` prop to email components to dynamically set the locale. (`packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx`, `packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx`) [[1]](diffhunk://#diff-ca227a03c0aa66428daff938c743435e8a4dc3ffa960c0952f2697a23e280fdbR6-R25) [[2]](diffhunk://#diff-483346065c074946a43c18492334bd680422a1d4cb994dc8c3cd39d0208e6016L1-R21) ### SnackBar Messages: * Replaced hardcoded SnackBar messages with translatable strings using the `t` function from `@lingui/react/macro`. (`packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx`, `packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts`, `packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts`, `packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts`, `packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx`, `packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx`) [[1]](diffhunk://#diff-551f2f94eacd8856d22bab7e63dd3ad693f87e9fa9b289864802ebc387f72b42R7) [[2]](diffhunk://#diff-551f2f94eacd8856d22bab7e63dd3ad693f87e9fa9b289864802ebc387f72b42L24-R29) [[3]](diffhunk://#diff-551f2f94eacd8856d22bab7e63dd3ad693f87e9fa9b289864802ebc387f72b42L43-R51) [[4]](diffhunk://#diff-428199461992a01325159f5fbf826d845f05f3361279eccd3f1ce416e0114845R7-R15) [[5]](diffhunk://#diff-428199461992a01325159f5fbf826d845f05f3361279eccd3f1ce416e0114845L24-R26) [[6]](diffhunk://#diff-cde42d6abfed63e52c2bda09d537a6577148d0baf957fde75ceaa8657ed58403R5) [[7]](diffhunk://#diff-cde42d6abfed63e52c2bda09d537a6577148d0baf957fde75ceaa8657ed58403L16-R17) [[8]](diffhunk://#diff-cde42d6abfed63e52c2bda09d537a6577148d0baf957fde75ceaa8657ed58403L28-R33) [[9]](diffhunk://#diff-9332c1988864863f12516c2fb77e814af60bedb37c36ffa094f49afc335d5457R5-R17) [[10]](diffhunk://#diff-9332c1988864863f12516c2fb77e814af60bedb37c36ffa094f49afc335d5457L27-R33) [[11]](diffhunk://#diff-9332c1988864863f12516c2fb77e814af60bedb37c36ffa094f49afc335d5457L42-R44) [[12]](diffhunk://#diff-8d64afa825b47ab71d18e3e284408e2097f5fd2365eae84d9d25d3568c48e49cR7) [[13]](diffhunk://#diff-8d64afa825b47ab71d18e3e284408e2097f5fd2365eae84d9d25d3568c48e49cR20-R28) [[14]](diffhunk://#diff-6e4361ded2b5656afaeb1befa8b1d23a45b490a1118550da290e27cdb8ebcdceR6) [[15]](diffhunk://#diff-6e4361ded2b5656afaeb1befa8b1d23a45b490a1118550da290e27cdb8ebcdceR19-R20) [[16]](diffhunk://#diff-6e4361ded2b5656afaeb1befa8b1d23a45b490a1118550da290e27cdb8ebcdceL29-R38)
This commit is contained in:
@ -2,9 +2,9 @@ import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { SettingsPermissions, SOURCE_LOCALE } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
import omit from 'lodash.omit';
|
||||
import { SOURCE_LOCALE, SettingsPermissions } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
|
||||
import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input';
|
||||
@ -25,6 +25,8 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
|
||||
import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output';
|
||||
import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input';
|
||||
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
|
||||
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
||||
@ -36,6 +38,7 @@ 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 { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
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';
|
||||
@ -48,9 +51,6 @@ import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output';
|
||||
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
|
||||
|
||||
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
||||
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
||||
@ -241,6 +241,7 @@ export class AuthResolver {
|
||||
user.id,
|
||||
user.email,
|
||||
workspace,
|
||||
signUpInput.locale ?? SOURCE_LOCALE,
|
||||
);
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
|
||||
@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@ -45,6 +47,7 @@ import {
|
||||
SignInUpBaseParams,
|
||||
SignInUpNewUserPayload,
|
||||
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
||||
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';
|
||||
@ -56,7 +59,6 @@ import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-in
|
||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@ -424,12 +426,14 @@ export class AuthService {
|
||||
const html = render(emailTemplate, { pretty: true });
|
||||
const text = render(emailTemplate, { plainText: true });
|
||||
|
||||
i18n.activate(locale);
|
||||
|
||||
this.emailService.send({
|
||||
from: `${this.environmentService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||
to: user.email,
|
||||
subject: 'Your Password Has Been Successfully Changed',
|
||||
subject: t`Your Password Has Been Successfully Changed`,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@ -143,12 +145,14 @@ export class ResetPasswordService {
|
||||
const html = render(emailTemplate, { pretty: true });
|
||||
const text = render(emailTemplate, { plainText: true });
|
||||
|
||||
i18n.activate(locale);
|
||||
|
||||
this.emailService.send({
|
||||
from: `${this.environmentService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||
to: email,
|
||||
subject: 'Action Needed to Reset Password',
|
||||
subject: t`Action Needed to Reset Password`,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { SOURCE_LOCALE } from 'twenty-shared';
|
||||
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input';
|
||||
import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output';
|
||||
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 { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
|
||||
@ -19,6 +22,7 @@ export class EmailVerificationResolver {
|
||||
@Args()
|
||||
resendEmailVerificationTokenInput: ResendEmailVerificationTokenInput,
|
||||
@OriginHeader() origin: string,
|
||||
@Context() context: I18nContext,
|
||||
): Promise<ResendEmailVerificationTokenOutput> {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
@ -30,6 +34,7 @@ export class EmailVerificationResolver {
|
||||
return await this.emailVerificationService.resendEmailVerificationToken(
|
||||
resendEmailVerificationTokenInput.email,
|
||||
workspace,
|
||||
context.req.headers['x-locale'] ?? SOURCE_LOCALE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@ -13,6 +15,7 @@ import {
|
||||
AppTokenType,
|
||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service';
|
||||
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import {
|
||||
EmailVerificationException,
|
||||
@ -21,7 +24,6 @@ import {
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@ -40,6 +42,7 @@ export class EmailVerificationService {
|
||||
userId: string,
|
||||
email: string,
|
||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
||||
locale: keyof typeof APP_LOCALES,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
||||
return { success: false };
|
||||
@ -57,7 +60,7 @@ export class EmailVerificationService {
|
||||
|
||||
const emailData = {
|
||||
link: verificationLink.toString(),
|
||||
locale: 'en' as keyof typeof APP_LOCALES,
|
||||
locale,
|
||||
};
|
||||
|
||||
const emailTemplate = SendEmailVerificationLinkEmail(emailData);
|
||||
@ -68,12 +71,14 @@ export class EmailVerificationService {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
i18n.activate(locale);
|
||||
|
||||
await this.emailService.send({
|
||||
from: `${this.environmentService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||
to: email,
|
||||
subject: 'Welcome to Twenty: Please Confirm Your Email',
|
||||
subject: t`Welcome to Twenty: Please Confirm Your Email`,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
@ -84,6 +89,7 @@ export class EmailVerificationService {
|
||||
async resendEmailVerificationToken(
|
||||
email: string,
|
||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
||||
locale: keyof typeof APP_LOCALES,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
||||
throw new EmailVerificationException(
|
||||
@ -125,7 +131,7 @@ export class EmailVerificationService {
|
||||
await this.appTokenRepository.delete(existingToken.id);
|
||||
}
|
||||
|
||||
await this.sendVerificationEmail(user.id, email, workspace);
|
||||
await this.sendVerificationEmail(user.id, email, workspace, locale);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@ -288,6 +290,8 @@ export class WorkspaceInvitationService {
|
||||
}
|
||||
: {},
|
||||
});
|
||||
|
||||
// Todo: sender name and locale should come from workspace member not user!
|
||||
const emailData = {
|
||||
link: link.toString(),
|
||||
workspace: { name: workspace.displayName, logo: workspace.logo },
|
||||
@ -297,7 +301,7 @@ export class WorkspaceInvitationService {
|
||||
lastName: sender.lastName,
|
||||
},
|
||||
serverUrl: this.environmentService.get('SERVER_URL'),
|
||||
locale: 'en' as keyof typeof APP_LOCALES,
|
||||
locale: sender.locale as keyof typeof APP_LOCALES,
|
||||
};
|
||||
|
||||
const emailTemplate = SendInviteLinkEmail(emailData);
|
||||
@ -306,10 +310,12 @@ export class WorkspaceInvitationService {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
i18n.activate(sender.locale);
|
||||
|
||||
await this.emailService.send({
|
||||
from: `${sender.firstName} ${sender.lastName} (via Twenty) <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||
to: invitation.value.email,
|
||||
subject: 'Join your team on Twenty',
|
||||
subject: t`Join your team on Twenty`,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user