setup localization for twenty-emails (#9806)

One of the steps to address #8128 

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


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

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

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

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

---------

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

View File

@ -492,6 +492,7 @@ export enum FeatureFlagKey {
IsLocalizationEnabled = 'IsLocalizationEnabled',
IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled',
IsNewRelationEnabled = 'IsNewRelationEnabled',
IsPermissionsEnabled = 'IsPermissionsEnabled',
IsPostgreSQLIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled',
IsRichTextV2Enabled = 'IsRichTextV2Enabled',
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',

View File

@ -1,5 +1,5 @@
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -217,6 +217,11 @@ export type BooleanFieldComparison = {
isNot?: InputMaybe<Scalars['Boolean']>;
};
export type BuildDraftServerlessFunctionInput = {
/** The id of the function. */
id: Scalars['ID'];
};
export enum CalendarChannelVisibility {
METADATA = 'METADATA',
SHARE_EVERYTHING = 'SHARE_EVERYTHING'
@ -689,6 +694,7 @@ export type Mutation = {
activateWorkflowVersion: Scalars['Boolean'];
activateWorkspace: Workspace;
authorizeApp: AuthorizeApp;
buildDraftServerlessFunction: ServerlessFunction;
checkoutSession: BillingSessionOutput;
computeStepOutputSchema: Scalars['JSON'];
createDraftFromWorkflowVersion: WorkflowVersion;
@ -763,6 +769,11 @@ export type MutationAuthorizeAppArgs = {
};
export type MutationBuildDraftServerlessFunctionArgs = {
input: BuildDraftServerlessFunctionInput;
};
export type MutationCheckoutSessionArgs = {
plan?: BillingPlanKey;
recurringInterval: SubscriptionInterval;
@ -1444,6 +1455,7 @@ export type ServerlessFunctionExecutionResult = {
/** Status of the serverless function execution */
export enum ServerlessFunctionExecutionStatus {
ERROR = 'ERROR',
IDLE = 'IDLE',
SUCCESS = 'SUCCESS'
}
@ -1454,6 +1466,7 @@ export type ServerlessFunctionIdInput = {
/** SyncStatus of the serverlessFunction */
export enum ServerlessFunctionSyncStatus {
BUILDING = 'BUILDING',
NOT_READY = 'NOT_READY',
READY = 'READY'
}

View File

@ -20,7 +20,7 @@ import { clientConfigApiStatusState } from '@/client-config/states/clientConfigA
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { isDefined } from 'twenty-shared';
import { APP_LOCALES, isDefined } from 'twenty-shared';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
useCheckUserExistsLazyQuery,
@ -280,7 +280,9 @@ export const useAuth = () => {
)
: TimeFormat[detectTimeFormat()],
});
dynamicActivate(workspaceMember.locale ?? 'en');
dynamicActivate(
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? 'en',
);
}
const workspace = user.currentWorkspace ?? null;

View File

@ -16,7 +16,7 @@ import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { isDefined } from 'twenty-shared';
import { APP_LOCALES, isDefined } from 'twenty-shared';
import { WorkspaceMember } from '~/generated-metadata/graphql';
import { useGetCurrentUserQuery } from '~/generated/graphql';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
@ -70,7 +70,7 @@ export const UserProviderEffect = () => {
return {
...workspaceMember,
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light',
locale: workspaceMember.locale ?? 'en',
locale: (workspaceMember.locale as keyof typeof APP_LOCALES) ?? 'en',
};
};
@ -93,7 +93,9 @@ export const UserProviderEffect = () => {
: TimeFormat[detectTimeFormat()],
});
dynamicActivate(workspaceMember.locale ?? 'en');
dynamicActivate(
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? 'en',
);
}
if (isDefined(workspaceMembers)) {

View File

@ -51,7 +51,7 @@ export const LocalePicker = () => {
if (!isDefined(currentWorkspaceMember)) return;
const handleLocaleChange = async (value: string) => {
const handleLocaleChange = async (value: keyof typeof APP_LOCALES) => {
setCurrentWorkspaceMember({
...currentWorkspaceMember,
...{ locale: value },
@ -126,7 +126,9 @@ export const LocalePicker = () => {
fullWidth
value={i18n.locale}
options={localeOptions}
onChange={(value) => handleLocaleChange(value)}
onChange={(value) =>
handleLocaleChange(value as keyof typeof APP_LOCALES)
}
/>
</StyledContainer>
);

View File

@ -1,6 +1,7 @@
import { i18n } from '@lingui/core';
import { APP_LOCALES } from 'twenty-shared';
export const dynamicActivate = async (locale: string) => {
export const dynamicActivate = async (locale: keyof typeof APP_LOCALES) => {
const { messages } = await import(`../../locales/generated/${locale}.ts`);
i18n.load(locale, messages);
i18n.activate(locale);