diff --git a/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx b/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx index b2e559f16..82bcdb6c3 100644 --- a/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx +++ b/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx @@ -3,25 +3,30 @@ import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; import { MainText } from 'src/components/MainText'; import { Title } from 'src/components/Title'; +import { APP_LOCALES } from 'twenty-shared'; type CleanSuspendedWorkspaceEmailProps = { daysSinceInactive: number; userName: string; workspaceDisplayName: string | undefined; + locale: keyof typeof APP_LOCALES; }; export const CleanSuspendedWorkspaceEmail = ({ daysSinceInactive, userName, workspaceDisplayName, + locale, }: CleanSuspendedWorkspaceEmailProps) => { - const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello'; - return ( - - + <BaseEmail width={333} locale={locale}> + <Title value={<Trans>Deleted Workspace</Trans>} /> <MainText> - {helloString}, + {userName?.length > 1 ? ( + <Trans>Dear {userName},</Trans> + ) : ( + <Trans>Hello,</Trans> + )} <br /> <br /> <Trans> @@ -39,7 +44,7 @@ export const CleanSuspendedWorkspaceEmail = ({ </MainText> <CallToAction href="https://app.twenty.com/" - value="Create a new workspace" + value={<Trans>Create a new workspace</Trans>} /> </BaseEmail> ); diff --git a/packages/twenty-emails/src/emails/password-reset-link.email.tsx b/packages/twenty-emails/src/emails/password-reset-link.email.tsx index 2f4f8f759..3e477bca6 100644 --- a/packages/twenty-emails/src/emails/password-reset-link.email.tsx +++ b/packages/twenty-emails/src/emails/password-reset-link.email.tsx @@ -1,4 +1,3 @@ -import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; @@ -20,8 +19,8 @@ export const PasswordResetLinkEmail = ({ }: PasswordResetLinkEmailProps) => { return ( <BaseEmail locale={locale}> - <Title value={t`Reset your password 🗝`} /> - <CallToAction href={link} value={t`Reset`} /> + <Title value={<Trans>Reset your password 🗝</Trans>} /> + <CallToAction href={link} value={<Trans>Reset</Trans>} /> <MainText> <Trans> This link is only valid for the next {duration}. If the link does not diff --git a/packages/twenty-emails/src/emails/password-update-notify.email.tsx b/packages/twenty-emails/src/emails/password-update-notify.email.tsx index 78d87156a..ac8b750fd 100644 --- a/packages/twenty-emails/src/emails/password-update-notify.email.tsx +++ b/packages/twenty-emails/src/emails/password-update-notify.email.tsx @@ -26,7 +26,7 @@ export const PasswordUpdateNotifyEmail = ({ return ( <BaseEmail locale={locale}> - <Title value={t`Password updated`} /> + <Title value={<Trans>Password updated</Trans>} /> <MainText> {helloString}, <br /> @@ -43,7 +43,7 @@ export const PasswordUpdateNotifyEmail = ({ </Trans> <br /> </MainText> - <CallToAction value={t`Connect to Twenty`} href={link} /> + <CallToAction value={<Trans>Connect to Twenty</Trans>} href={link} /> </BaseEmail> ); }; diff --git a/packages/twenty-emails/src/emails/send-email-verification-link.email.tsx b/packages/twenty-emails/src/emails/send-email-verification-link.email.tsx index 71840c2b5..25c32108a 100644 --- a/packages/twenty-emails/src/emails/send-email-verification-link.email.tsx +++ b/packages/twenty-emails/src/emails/send-email-verification-link.email.tsx @@ -1,4 +1,3 @@ -import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { BaseEmail } from 'src/components/BaseEmail'; @@ -19,8 +18,8 @@ export const SendEmailVerificationLinkEmail = ({ }: SendEmailVerificationLinkEmailProps) => { return ( <BaseEmail width={333} locale={locale}> - <Title value={t`Confirm your email address`} /> - <CallToAction href={link} value={t`Verify Email`} /> + <Title value={<Trans>Confirm your email address</Trans>} /> + <CallToAction href={link} value={<Trans>Verify Email</Trans>} /> <br /> <br /> <MainText> diff --git a/packages/twenty-emails/src/emails/send-invite-link.email.tsx b/packages/twenty-emails/src/emails/send-invite-link.email.tsx index 5f934b03f..933b472bb 100644 --- a/packages/twenty-emails/src/emails/send-invite-link.email.tsx +++ b/packages/twenty-emails/src/emails/send-invite-link.email.tsx @@ -1,4 +1,3 @@ -import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { Img } from '@react-email/components'; import { emailTheme } from 'src/common-style'; @@ -39,7 +38,7 @@ export const SendInviteLinkEmail = ({ return ( <BaseEmail width={333} locale={locale}> - <Title value={t`Join your team on Twenty`} /> + <Title value={<Trans>Join your team on Twenty</Trans>} /> <MainText> {capitalize(sender.firstName)} ( <Link @@ -54,7 +53,7 @@ export const SendInviteLinkEmail = ({ <HighlightedContainer> {workspaceLogo && <Img src={workspaceLogo} width={40} height={40} />} {workspace.name && <HighlightedText value={workspace.name} />} - <CallToAction href={link} value={t`Accept invite`} /> + <CallToAction href={link} value={<Trans>Accept invite</Trans>} /> </HighlightedContainer> <WhatIsTwenty /> </BaseEmail> diff --git a/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx b/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx index e8f68e1c0..3367ff8d0 100644 --- a/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx +++ b/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx @@ -1,15 +1,16 @@ -import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; import { MainText } from 'src/components/MainText'; import { Title } from 'src/components/Title'; +import { APP_LOCALES } from 'twenty-shared'; type WarnSuspendedWorkspaceEmailProps = { daysSinceInactive: number; inactiveDaysBeforeDelete: number; userName: string; workspaceDisplayName: string | undefined; + locale: keyof typeof APP_LOCALES; }; export const WarnSuspendedWorkspaceEmail = ({ @@ -17,6 +18,7 @@ export const WarnSuspendedWorkspaceEmail = ({ inactiveDaysBeforeDelete, userName, workspaceDisplayName, + locale, }: WarnSuspendedWorkspaceEmailProps) => { const daysLeft = inactiveDaysBeforeDelete - daysSinceInactive; const dayOrDays = daysLeft > 1 ? 'days' : 'day'; @@ -25,8 +27,8 @@ export const WarnSuspendedWorkspaceEmail = ({ const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello'; return ( - <BaseEmail width={333} locale="en"> - <Title value="Suspended Workspace 😴" /> + <BaseEmail width={333} locale={locale}> + <Title value={<Trans>Suspended Workspace </Trans>} /> <MainText> {helloString}, <br /> @@ -50,7 +52,7 @@ export const WarnSuspendedWorkspaceEmail = ({ </MainText> <CallToAction href="https://app.twenty.com/settings/billing" - value={t`Update your subscription`} + value={<Trans>Update your subscription</Trans>} /> </BaseEmail> ); diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx index 81c0ed330..aa40c846a 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx @@ -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, }); diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useVerifyLogin.test.ts b/packages/twenty-front/src/modules/auth/hooks/__tests__/useVerifyLogin.test.ts index 57cd68a2d..58b29ac29 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useVerifyLogin.test.ts +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useVerifyLogin.test.ts @@ -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 }; }; diff --git a/packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts b/packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts index 096258678..9eb5e5cb3 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts @@ -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); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useHandleResetPassword.test.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useHandleResetPassword.test.ts index ff7ca2522..b7b774e57 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useHandleResetPassword.test.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useHandleResetPassword.test.ts @@ -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, { diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts index d1c52dfd9..8051552c0 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts @@ -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, }); } diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts index b9003935c..7cbcfe283 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts @@ -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 }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx index 79ee5f222..931bc0f0e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx @@ -4,6 +4,7 @@ import { IconCopy, LightIconButton } from 'twenty-ui'; 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'; const StyledButtonContainer = styled.div` padding: 0 ${({ theme }) => theme.spacing(1)}; @@ -16,6 +17,7 @@ export type LightCopyIconButtonProps = { export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => { const { enqueueSnackBar } = useSnackBar(); const theme = useTheme(); + const { t } = useLingui(); return ( <StyledButtonContainer> @@ -23,7 +25,7 @@ export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => { className="copy-button" Icon={IconCopy} onClick={() => { - enqueueSnackBar('Text copied to clipboard', { + enqueueSnackBar(t`Text copied to clipboard`, { variant: SnackBarVariant.Success, icon: <IconCopy size={theme.icon.size.md} />, duration: 2000, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx index babdf7df7..e9f9bc6d7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx @@ -3,6 +3,7 @@ import { usePhonesFieldDisplay } from '@/object-record/record-field/meta-types/h import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { PhonesDisplay } from '@/ui/field/display/components/PhonesDisplay'; +import { useLingui } from '@lingui/react/macro'; import React from 'react'; import { useIcons } from 'twenty-ui'; @@ -15,6 +16,8 @@ export const PhonesFieldDisplay = () => { const { getIcon } = useIcons(); + const { t } = useLingui(); + const IconCircleCheck = getIcon('IconCircleCheck'); const IconExclamationCircle = getIcon('IconExclamationCircle'); @@ -26,13 +29,13 @@ export const PhonesFieldDisplay = () => { try { await navigator.clipboard.writeText(phoneNumber); - enqueueSnackBar('Phone number copied to clipboard', { + enqueueSnackBar(t`Phone number copied to clipboard`, { variant: SnackBarVariant.Success, icon: <IconCircleCheck size={16} color="green" />, duration: 2000, }); } catch (err) { - enqueueSnackBar('Error copying to clipboard', { + enqueueSnackBar(t`Error copying to clipboard`, { variant: SnackBarVariant.Error, icon: <IconExclamationCircle size={16} color="red" />, duration: 2000, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhonesFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhonesFieldDisplay.perf.stories.tsx index fce6cfacc..4e43a8dcd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhonesFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhonesFieldDisplay.perf.stories.tsx @@ -3,6 +3,7 @@ import { ComponentDecorator } from 'twenty-ui'; import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhonesFieldDisplay'; import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; @@ -13,6 +14,7 @@ const meta: Meta = { MemoryRouterDecorator, getFieldDecorator('person', 'phones'), ComponentDecorator, + I18nFrontDecorator, SnackBarDecorator, ], component: PhonesFieldDisplay, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx index 7775b63d9..cd4b56e51 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -8,6 +8,7 @@ import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { useNumberField } from '../../../hooks/useNumberField'; import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput'; @@ -108,7 +109,7 @@ const meta: Meta = { onTab: { control: false }, onShiftTab: { control: false }, }, - decorators: [clearMocksDecorator, SnackBarDecorator], + decorators: [clearMocksDecorator, SnackBarDecorator, I18nFrontDecorator], parameters: { clearMocks: true, }, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx index c6ec55743..c8c09f337 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx @@ -7,6 +7,7 @@ import { FieldMetadataType } from '~/generated/graphql'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { FieldContextProvider } from '../../../components/FieldContextProvider'; import { useTextField } from '../../../hooks/useTextField'; import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput'; @@ -107,7 +108,7 @@ const meta: Meta = { onTab: { control: false }, onShiftTab: { control: false }, }, - decorators: [clearMocksDecorator, SnackBarDecorator], + decorators: [clearMocksDecorator, SnackBarDecorator, I18nFrontDecorator], parameters: { clearMocks: true, }, diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx index e4f258d78..0dcae8f27 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx @@ -15,6 +15,7 @@ import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared'; @@ -49,9 +50,11 @@ export const SettingsAdminWorkspaceContent = ({ const { updateFeatureFlagState } = useFeatureFlagState(); const userLookupResult = useRecoilValue(userLookupResultState); + const { t } = useLingui(); + const handleImpersonate = async (workspaceId: string) => { if (!userLookupResult?.user.id) { - enqueueSnackBar('Please search for a user first', { + enqueueSnackBar(t`Please search for a user first`, { variant: SnackBarVariant.Error, }); return; diff --git a/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx b/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx index d7523ab94..94a4c74dd 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx @@ -5,7 +5,7 @@ import { Button, IconCopy } from 'twenty-ui'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; - +import { useLingui } from '@lingui/react/macro'; const StyledContainer = styled.div` display: flex; flex-direction: row; @@ -20,6 +20,7 @@ type ApiKeyInputProps = { apiKey: string }; export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => { const theme = useTheme(); + const { t } = useLingui(); const { enqueueSnackBar } = useSnackBar(); return ( @@ -31,7 +32,7 @@ export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => { Icon={IconCopy} title="Copy" onClick={() => { - enqueueSnackBar('API Key copied to clipboard', { + enqueueSnackBar(t`API Key copied to clipboard`, { variant: SnackBarVariant.Success, icon: <IconCopy size={theme.icon.size.md} />, duration: 2000, diff --git a/packages/twenty-front/src/modules/settings/developers/components/__stories__/ApiKeyInput.stories.tsx b/packages/twenty-front/src/modules/settings/developers/components/__stories__/ApiKeyInput.stories.tsx index 5ecffb5a7..a2644c06e 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/__stories__/ApiKeyInput.stories.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/__stories__/ApiKeyInput.stories.tsx @@ -2,12 +2,13 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui'; import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; const meta: Meta<typeof ApiKeyInput> = { title: 'Modules/Settings/Developers/ApiKeys/ApiKeyInput', component: ApiKeyInput, - decorators: [ComponentDecorator, SnackBarDecorator], + decorators: [ComponentDecorator, SnackBarDecorator, I18nFrontDecorator], args: { apiKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2VudHktN2VkOWQyMTItMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNjk4MTQyODgyLCJleHAiOjE2OTk0MDE1OTksImp0aSI6ImMyMmFiNjQxLTVhOGYtNGQwMC1iMDkzLTk3MzUwYTM2YzZkOSJ9.JIe2TX5IXrdNl3n-kRFp3jyfNUE7unzXZLAzm2Gxl98', diff --git a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOOIDCForm.tsx b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOOIDCForm.tsx index dc49aaa87..c388b6085 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOOIDCForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOOIDCForm.tsx @@ -5,6 +5,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; import { Controller, useFormContext } from 'react-hook-form'; import { Button, H2Title, IconCopy, Section } from 'twenty-ui'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; @@ -35,6 +36,7 @@ export const SettingsSSOOIDCForm = () => { const { control } = useFormContext(); const { enqueueSnackBar } = useSnackBar(); const theme = useTheme(); + const { t } = useLingui(); const authorizedUrl = window.location.origin; const redirectionUrl = `${REACT_APP_SERVER_BASE_URL}/auth/oidc/callback`; @@ -43,8 +45,8 @@ export const SettingsSSOOIDCForm = () => { <> <Section> <H2Title - title="Client Settings" - description="Provide your OIDC provider details" + title={t`Client Settings`} + description={t`Provide your OIDC provider details`} /> <StyledInputsContainer> <StyledContainer> @@ -59,7 +61,7 @@ export const SettingsSSOOIDCForm = () => { <StyledButtonCopy> <Button Icon={IconCopy} - title="Copy" + title={t`Copy`} onClick={() => { enqueueSnackBar('Authorized Url copied to clipboard', { variant: SnackBarVariant.Success, @@ -75,7 +77,7 @@ export const SettingsSSOOIDCForm = () => { <StyledLinkContainer> <TextInput readOnly={true} - label="Redirection URI" + label={t`Redirection URI`} value={redirectionUrl} fullWidth /> @@ -83,9 +85,9 @@ export const SettingsSSOOIDCForm = () => { <StyledButtonCopy> <Button Icon={IconCopy} - title="Copy" + title={t`Copy`} onClick={() => { - enqueueSnackBar('Redirect Url copied to clipboard', { + enqueueSnackBar(t`Redirect Url copied to clipboard`, { variant: SnackBarVariant.Success, icon: <IconCopy size={theme.icon.size.md} />, duration: 2000, @@ -99,8 +101,8 @@ export const SettingsSSOOIDCForm = () => { </Section> <Section> <H2Title - title="Identity Provider" - description="Enter the credentials to set the connection" + title={t`Identity Provider`} + description={t`Enter the credentials to set the connection`} /> <StyledInputsContainer> <Controller diff --git a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx index 1a39b4a2d..9d0bbfcfb 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx @@ -6,6 +6,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; import { ChangeEvent, useRef } from 'react'; import { useFormContext } from 'react-hook-form'; import { isDefined } from 'twenty-shared'; @@ -57,6 +58,7 @@ export const SettingsSSOSAMLForm = () => { const { enqueueSnackBar } = useSnackBar(); const theme = useTheme(); const { setValue, getValues, watch, trigger } = useFormContext(); + const { t } = useLingui(); const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => { if (isDefined(e.target.files)) { @@ -64,7 +66,7 @@ export const SettingsSSOSAMLForm = () => { const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text); e.target.value = ''; if (!samlMetadataParsed.success) { - return enqueueSnackBar('Invalid File', { + return enqueueSnackBar(t`Invalid File`, { variant: SnackBarVariant.Error, duration: 2000, }); @@ -100,7 +102,7 @@ export const SettingsSSOSAMLForm = () => { `${REACT_APP_SERVER_BASE_URL}/auth/saml/metadata/${getValues('id')}`, ); if (!response.ok) { - return enqueueSnackBar('Metadata file generation failed', { + return enqueueSnackBar(t`Metadata file generation failed`, { variant: SnackBarVariant.Error, duration: 2000, }); @@ -120,8 +122,8 @@ export const SettingsSSOSAMLForm = () => { <> <Section> <H2Title - title="Identity Provider Metadata XML" - description="Upload the XML file with your connection infos" + title={t`Identity Provider Metadata XML`} + description={t`Upload the XML file with your connection infos`} /> <StyledUploadFileContainer> <StyledFileInput @@ -133,7 +135,7 @@ export const SettingsSSOSAMLForm = () => { <Button Icon={IconUpload} onClick={handleUploadFileClick} - title="Upload file" + title={t`Upload file`} ></Button> {isXMLMetadataValid() && ( <IconCheck @@ -146,15 +148,15 @@ export const SettingsSSOSAMLForm = () => { </Section> <Section> <H2Title - title="Service Provider Details" - description="Enter the infos to set the connection" + title={t`Service Provider Details`} + description={t`Enter the infos to set the connection`} /> <StyledInputsContainer> <StyledContainer> <Button Icon={IconDownload} onClick={downloadMetadata} - title="Download file" + title={t`Download file`} ></Button> </StyledContainer> <HorizontalSeparator text={'Or'} /> @@ -194,9 +196,9 @@ export const SettingsSSOSAMLForm = () => { <StyledButtonCopy> <Button Icon={IconCopy} - title="Copy" + title={t`Copy`} onClick={() => { - enqueueSnackBar('Entity ID copied to clipboard', { + enqueueSnackBar(t`Entity ID copied to clipboard`, { variant: SnackBarVariant.Success, icon: <IconCopy size={theme.icon.size.md} />, duration: 2000, diff --git a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu.tsx index d14bfc0ba..507720a6e 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu.tsx @@ -16,6 +16,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useLingui } from '@lingui/react/macro'; import { UnwrapRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared'; import { SsoIdentityProviderStatus } from '~/generated/graphql'; @@ -36,6 +37,8 @@ export const SettingsSecuritySSORowDropdownMenu = ({ const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider(); const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider(); + const { t } = useLingui(); + const handleDeleteSSOIdentityProvider = async ( identityProviderId: string, ) => { @@ -43,7 +46,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({ identityProviderId, }); if (isDefined(result.errors)) { - enqueueSnackBar('Error deleting SSO Identity Provider', { + enqueueSnackBar(t`Error deleting SSO Identity Provider`, { variant: SnackBarVariant.Error, duration: 2000, }); @@ -61,7 +64,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({ : SsoIdentityProviderStatus.Active, }); if (isDefined(result.errors)) { - enqueueSnackBar('Error editing SSO Identity Provider', { + enqueueSnackBar(t`Error editing SSO Identity Provider`, { variant: SnackBarVariant.Error, duration: 2000, }); @@ -82,7 +85,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({ <MenuItem accent="default" LeftIcon={IconArchive} - text={SSOIdp.status === 'Active' ? 'Deactivate' : 'Activate'} + text={SSOIdp.status === 'Active' ? t`Deactivate` : t`Activate`} onClick={() => { toggleSSOIdentityProviderStatus(SSOIdp.id); closeDropdown(); @@ -91,7 +94,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({ <MenuItem accent="danger" LeftIcon={IconTrash} - text="Delete" + text={t`Delete`} onClick={() => { handleDeleteSSOIdentityProvider(SSOIdp.id); closeDropdown(); diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.tsx index 990b9a40d..8716f1660 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.tsx +++ b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowVersion.tsx @@ -3,6 +3,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { RUN_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/runWorkflowVersion'; import { useApolloClient, useMutation } from '@apollo/client'; import { useTheme } from '@emotion/react'; +import { useLingui } from '@lingui/react/macro'; import { IconSettingsAutomation } from 'twenty-ui'; import { RunWorkflowVersionMutation, @@ -21,6 +22,7 @@ export const useRunWorkflowVersion = () => { const { enqueueSnackBar } = useSnackBar(); const theme = useTheme(); + const { t } = useLingui(); const runWorkflowVersion = async ({ workflowVersionId, @@ -36,7 +38,7 @@ export const useRunWorkflowVersion = () => { const workflowRunId = data?.runWorkflowVersion?.workflowRunId; if (!workflowRunId) { - enqueueSnackBar('Workflow run failed', { + enqueueSnackBar(t`Workflow run failed`, { variant: SnackBarVariant.Error, }); return; @@ -44,7 +46,7 @@ export const useRunWorkflowVersion = () => { const link = `/object/workflowRun/${workflowRunId}`; - enqueueSnackBar('Workflow is running...', { + enqueueSnackBar(t`Workflow is running...`, { variant: SnackBarVariant.Success, icon: ( <IconSettingsAutomation @@ -54,7 +56,7 @@ export const useRunWorkflowVersion = () => { ), link: { href: link, - text: 'View execution details', + text: t`View execution details`, }, }); }; diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx index 9726d9aa0..9c9c95ca4 100644 --- a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx @@ -114,7 +114,7 @@ export const SettingsWorkspaceMembers = () => { const handleRemoveWorkspaceInvitation = async (appTokenId: string) => { const result = await deleteWorkspaceInvitation({ appTokenId }); if (isDefined(result.errors)) { - enqueueSnackBar('Error deleting invitation', { + enqueueSnackBar(t`Error deleting invitation`, { variant: SnackBarVariant.Error, duration: 2000, }); @@ -124,7 +124,7 @@ export const SettingsWorkspaceMembers = () => { const handleResendWorkspaceInvitation = async (appTokenId: string) => { const result = await resendInvitation({ appTokenId }); if (isDefined(result.errors)) { - enqueueSnackBar('Error resending invitation', { + enqueueSnackBar(t`Error resending invitation`, { variant: SnackBarVariant.Error, duration: 2000, }); @@ -134,7 +134,7 @@ export const SettingsWorkspaceMembers = () => { const getExpiresAtText = (expiresAt: string) => { const expiresAtDate = new Date(expiresAt); return expiresAtDate < new Date() - ? 'Expired' + ? t`Expired` : formatDistanceToNow(new Date(expiresAt)); }; diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx index f66fcf4fc..f66837806 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx @@ -1,14 +1,15 @@ -import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableCell } from '@/ui/layout/table/components/TableCell'; -import { Button } from 'twenty-ui'; -import { Table } from '@/ui/layout/table/components/Table'; -import { CustomDomainValidRecords } from '~/generated/graphql'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; import styled from '@emotion/styled'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useLingui } from '@lingui/react/macro'; +import { Button } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; +import { CustomDomainValidRecords } from '~/generated/graphql'; const StyledTable = styled(Table)` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; @@ -41,9 +42,11 @@ export const SettingsCustomDomainRecords = ({ }) => { const { enqueueSnackBar } = useSnackBar(); + const { t } = useLingui(); + const copyToClipboard = (value: string) => { navigator.clipboard.writeText(value); - enqueueSnackBar('Copied to clipboard!', { + enqueueSnackBar(t`Copied to clipboard!`, { variant: SnackBarVariant.Success, }); }; diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 1803ebb07..1b38d540f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -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( diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 6268ca0c2..c52429c5e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -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, }); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts index 4d016878e..992462fba 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts @@ -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, }); diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts index 1484581a1..b25b95e32 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts @@ -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, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts index cd34b84f9..58a4e811b 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts @@ -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 }; } diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts index a76fc5943..e4cc31634 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts @@ -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, }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts index 53740ff38..300391eec 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts @@ -1,6 +1,8 @@ import { Injectable, Logger } 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 { differenceInDays } from 'date-fns'; import { @@ -112,18 +114,21 @@ export class CleanerWorkspaceService { inactiveDaysBeforeDelete: this.inactiveDaysBeforeSoftDelete, userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, workspaceDisplayName: `${workspaceDisplayName}`, + locale: workspaceMember.locale, }; const emailTemplate = WarnSuspendedWorkspaceEmail(emailData); const html = render(emailTemplate, { pretty: true }); const text = render(emailTemplate, { plainText: true }); + i18n.activate(workspaceMember.locale); + this.emailService.send({ to: workspaceMember.userEmail, bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'), from: `${this.environmentService.get( 'EMAIL_FROM_NAME', )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`, - subject: 'Action needed to prevent workspace deletion', + subject: t`Action needed to prevent workspace deletion`, html, text, }); @@ -186,6 +191,7 @@ export class CleanerWorkspaceService { daysSinceInactive: daysSinceInactive, userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, workspaceDisplayName, + locale: workspaceMember.locale, }; const emailTemplate = CleanSuspendedWorkspaceEmail(emailData); const html = render(emailTemplate, { pretty: true });