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:
Félix Malfait
2025-02-23 21:15:41 +01:00
committed by GitHub
parent 2039986684
commit df0d23a365
33 changed files with 195 additions and 103 deletions

View File

@ -4,6 +4,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -21,11 +22,11 @@ export const VerifyEmailEffect = () => {
const navigate = useNavigateApp();
const { readCaptchaToken } = useReadCaptchaToken();
const { t } = useLingui();
useEffect(() => {
const verifyEmailToken = async () => {
if (!email || !emailVerificationToken) {
enqueueSnackBar(`Invalid email verification link.`, {
enqueueSnackBar(t`Invalid email verification link.`, {
dedupeKey: 'email-verification-link-dedupe-key',
variant: SnackBarVariant.Error,
});
@ -40,14 +41,14 @@ export const VerifyEmailEffect = () => {
captchaToken,
);
enqueueSnackBar('Email verified.', {
enqueueSnackBar(t`Email verified.`, {
dedupeKey: 'email-verification-dedupe-key',
variant: SnackBarVariant.Success,
});
navigate(AppPath.Verify, undefined, { loginToken: loginToken.token });
} catch (error) {
enqueueSnackBar('Email verification failed.', {
enqueueSnackBar(t`Email verification failed.`, {
dedupeKey: 'email-verification-dedupe-key',
variant: SnackBarVariant.Error,
});

View File

@ -1,5 +1,8 @@
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { SOURCE_LOCALE } from 'twenty-shared';
import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
@ -8,6 +11,7 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useAuth } from '../useAuth';
import { useVerifyLogin } from '../useVerifyLogin';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
jest.mock('../useAuth', () => ({
useAuth: jest.fn(),
}));
@ -20,9 +24,12 @@ jest.mock('~/hooks/useNavigateApp', () => ({
useNavigateApp: jest.fn(),
}));
dynamicActivate(SOURCE_LOCALE);
const renderHooks = () => {
const { result } = renderHook(() => useVerifyLogin(), {
wrapper: RecoilRoot,
wrapper: ({ children }) =>
RecoilRoot({ children: I18nProvider({ i18n, children }) }),
});
return { result };
};

View File

@ -4,6 +4,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
import { useSetRecoilState } from 'recoil';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -11,6 +12,7 @@ export const useVerifyLogin = () => {
const { enqueueSnackBar } = useSnackBar();
const navigate = useNavigateApp();
const { getAuthTokensFromLoginToken } = useAuth();
const { t } = useLingui();
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
@ -21,7 +23,7 @@ export const useVerifyLogin = () => {
setIsAppWaitingForFreshObjectMetadata(true);
await getAuthTokensFromLoginToken(loginToken);
} catch (error) {
enqueueSnackBar('Authentication failed', {
enqueueSnackBar(t`Authentication failed`, {
variant: SnackBarVariant.Error,
});
navigate(AppPath.SignInUp);

View File

@ -1,29 +1,46 @@
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { act, renderHook } from '@testing-library/react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
import { RecoilRoot } from 'recoil';
import { SOURCE_LOCALE } from 'twenty-shared';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
// Mocks
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
jest.mock('~/generated/graphql');
dynamicActivate(SOURCE_LOCALE);
const renderHooks = () => {
const { result } = renderHook(() => useHandleResetPassword(), {
wrapper: ({ children }) =>
RecoilRoot({ children: I18nProvider({ i18n, children }) }),
});
return { result };
};
describe('useHandleResetPassword', () => {
const enqueueSnackBarMock = jest.fn();
const emailPasswordResetLinkMock = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useSnackBar as jest.Mock).mockReturnValue({
enqueueSnackBar: enqueueSnackBarMock,
});
(useEmailPasswordResetLinkMutation as jest.Mock).mockReturnValue([
emailPasswordResetLinkMock,
]);
jest.clearAllMocks();
});
it('should show error message if email is invalid', async () => {
const { result } = renderHook(() => useHandleResetPassword());
const { result } = renderHooks();
await act(() => result.current.handleResetPassword('')());
expect(enqueueSnackBarMock).toHaveBeenCalledWith('Invalid email', {
@ -36,7 +53,7 @@ describe('useHandleResetPassword', () => {
data: { emailPasswordResetLink: { success: true } },
});
const { result } = renderHook(() => useHandleResetPassword());
const { result } = renderHooks();
await act(() => result.current.handleResetPassword('test@example.com')());
expect(enqueueSnackBarMock).toHaveBeenCalledWith(
@ -50,10 +67,10 @@ describe('useHandleResetPassword', () => {
data: { emailPasswordResetLink: { success: false } },
});
const { result } = renderHook(() => useHandleResetPassword());
const { result } = renderHooks();
await act(() => result.current.handleResetPassword('test@example.com')());
expect(enqueueSnackBarMock).toHaveBeenCalledWith('There was some issue', {
expect(enqueueSnackBarMock).toHaveBeenCalledWith('There was an issue', {
variant: SnackBarVariant.Error,
});
});
@ -62,7 +79,7 @@ describe('useHandleResetPassword', () => {
const errorMessage = 'Network Error';
emailPasswordResetLinkMock.mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useHandleResetPassword());
const { result } = renderHooks();
await act(() => result.current.handleResetPassword('test@example.com')());
expect(enqueueSnackBarMock).toHaveBeenCalledWith(errorMessage, {

View File

@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { t } from '@lingui/core/macro';
import { useResendEmailVerificationTokenMutation } from '~/generated/graphql';
export const useHandleResendEmailVerificationToken = () => {
@ -13,7 +14,7 @@ export const useHandleResendEmailVerificationToken = () => {
(email: string | null) => {
return async () => {
if (!email) {
enqueueSnackBar('Invalid email', {
enqueueSnackBar(t`Invalid email`, {
variant: SnackBarVariant.Error,
});
return;
@ -25,11 +26,11 @@ export const useHandleResendEmailVerificationToken = () => {
});
if (data?.resendEmailVerificationToken?.success === true) {
enqueueSnackBar('Email verification link resent!', {
enqueueSnackBar(t`Email verification link resent!`, {
variant: SnackBarVariant.Success,
});
} else {
enqueueSnackBar('There was some issue', {
enqueueSnackBar(t`There was an issue`, {
variant: SnackBarVariant.Error,
});
}

View File

@ -2,17 +2,19 @@ import { useCallback } from 'react';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
export const useHandleResetPassword = () => {
const { enqueueSnackBar } = useSnackBar();
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
const { t } = useLingui();
const handleResetPassword = useCallback(
(email: string) => {
return async () => {
if (!email) {
enqueueSnackBar('Invalid email', {
enqueueSnackBar(t`Invalid email`, {
variant: SnackBarVariant.Error,
});
return;
@ -24,11 +26,11 @@ export const useHandleResetPassword = () => {
});
if (data?.emailPasswordResetLink?.success === true) {
enqueueSnackBar('Password reset link has been sent to the email', {
enqueueSnackBar(t`Password reset link has been sent to the email`, {
variant: SnackBarVariant.Success,
});
} else {
enqueueSnackBar('There was some issue', {
enqueueSnackBar(t`There was an issue`, {
variant: SnackBarVariant.Error,
});
}
@ -39,7 +41,7 @@ export const useHandleResetPassword = () => {
}
};
},
[enqueueSnackBar, emailPasswordResetLink],
[enqueueSnackBar, emailPasswordResetLink, t],
);
return { handleResetPassword };