Set up localization with feature flag control (#9649)

Refers #8128 

Changes Introduced:
- Added i18n configuration.
- Added a feature flag for localization.
- Enabled language switching based on the flag.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Anne Deepa Prasanna
2025-01-17 01:30:56 +05:30
committed by GitHub
parent b81ffcc77c
commit f44b31573a
38 changed files with 912 additions and 79 deletions

View File

@ -2,7 +2,7 @@
"jsc": {
"target": "es2017",
"parser": {
"syntax": "typescript",
"syntax": "typescript",
"tsx": true,
"decorators": false,
"dynamicImport": false
@ -19,6 +19,19 @@
"hidden": {
"jest": true
}
},
"experimental": {
"plugins": [
[
"@lingui/swc-plugin",
{
"runtimeModules": {
"i18n": ["@lingui/core", "i18n"],
"trans": ["@lingui/react", "Trans"]
}
}
]
]
}
},
"module": {

View File

@ -12,7 +12,16 @@ const jestConfig: JestConfigWithTsJest = {
testEnvironment: 'jsdom',
transformIgnorePatterns: ['../../node_modules/'],
transform: {
'^.+\\.(ts|js|tsx|jsx)$': '@swc/jest',
'^.+\\.(ts|js|tsx|jsx)$': [
'@swc/jest',
{
jsc: {
experimental: {
plugins: [], // Disable Lingui plugin during tests
},
},
},
],
},
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$':

View File

@ -0,0 +1,12 @@
import { defineConfig } from '@lingui/cli';
export default defineConfig({
sourceLocale: 'en',
locales: ['fr', 'en', 'pt', 'de', 'it', 'es', 'zh'],
catalogs: [
{
path: '<rootDir>/src/locales/{locale}/messages',
include: ['src'],
},
],
});

View File

@ -153,6 +153,20 @@
"configurations": {
"ci": {}
}
},
"lingui:extract": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "lingui extract"
}
},
"lingui:compile": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "lingui compile --typescript"
}
}
}
}

View File

@ -330,7 +330,8 @@ export enum FeatureFlagKey {
IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled',
IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled',
IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled',
IsWorkflowEnabled = 'IsWorkflowEnabled'
IsWorkflowEnabled = 'IsWorkflowEnabled',
IsLocalizationEnabled = 'IsLocalizationEnabled'
}
export type FieldConnection = {

View File

@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"vXIe7J\":[\"Language\"],\"AXTJAW\":[\"Select your preferred language\"]}")as Messages;

View File

@ -0,0 +1,16 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-01-16 16:50+0100\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en\n"
#: src/pages/settings/profile/appearance/components/SettingsExperience.tsx:51
msgid "Language"
msgstr "Language"
#: src/pages/settings/profile/appearance/components/SettingsExperience.tsx:52
msgid "Select your preferred language"
msgstr "Select your preferred language"

View File

@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"vXIe7J\":[\"Language\"],\"AXTJAW\":[\"Select your preferred language\"]}")as Messages;

View File

@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"vXIe7J\":[\"Language\"],\"AXTJAW\":[\"Select your preferred language\"]}")as Messages;

View File

@ -0,0 +1,16 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-01-16 16:50+0100\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: fr\n"
#: src/pages/settings/profile/appearance/components/SettingsExperience.tsx:51
msgid "Language"
msgstr "Langue"
#: src/pages/settings/profile/appearance/components/SettingsExperience.tsx:52
msgid "Select your preferred language"
msgstr "Sélectionnez votre langue préférée"

View File

@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"vXIe7J\":[\"Langue\"],\"AXTJAW\":[\"Sélectionnez votre langue préférée\"]}")as Messages;

View File

@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"vXIe7J\":[\"Language\"],\"AXTJAW\":[\"Select your preferred language\"]}")as Messages;

View File

@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"vXIe7J\":[\"Language\"],\"AXTJAW\":[\"Select your preferred language\"]}")as Messages;

View File

@ -0,0 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"vXIe7J\":[\"Language\"],\"AXTJAW\":[\"Select your preferred language\"]}")as Messages;

View File

@ -5,29 +5,41 @@ import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserve
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { HelmetProvider } from 'react-helmet-async';
import { RecoilRoot } from 'recoil';
import { RecoilURLSyncJSON } from 'recoil-sync';
import { IconsProvider } from 'twenty-ui';
import { messages as enMessages } from '../../../locales/en/messages';
import { messages as frMessages } from '../../../locales/fr/messages';
i18n.load({
en: enMessages,
fr: frMessages,
});
i18n.activate('fr');
export const App = () => {
return (
<RecoilRoot>
<RecoilURLSyncJSON location={{ part: 'queryParams' }}>
<AppErrorBoundary>
<CaptchaProvider>
<RecoilDebugObserverEffect />
<ApolloDevLogEffect />
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<ExceptionHandlerProvider>
<HelmetProvider>
<AppRouter />
</HelmetProvider>
</ExceptionHandlerProvider>
</IconsProvider>
</SnackBarProviderScope>
</CaptchaProvider>
<I18nProvider i18n={i18n}>
<CaptchaProvider>
<RecoilDebugObserverEffect />
<ApolloDevLogEffect />
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<ExceptionHandlerProvider>
<HelmetProvider>
<AppRouter />
</HelmetProvider>
</ExceptionHandlerProvider>
</IconsProvider>
</SnackBarProviderScope>
</CaptchaProvider>
</I18nProvider>
</AppErrorBoundary>
</RecoilURLSyncJSON>
</RecoilRoot>

View File

@ -123,11 +123,11 @@ const SettingsProfile = lazy(() =>
})),
);
const SettingsAppearance = lazy(() =>
const SettingsExperience = lazy(() =>
import(
'~/pages/settings/profile/appearance/components/SettingsAppearance'
'~/pages/settings/profile/appearance/components/SettingsExperience'
).then((module) => ({
default: module.SettingsAppearance,
default: module.SettingsExperience,
})),
);
@ -278,7 +278,7 @@ export const SettingsRoutes = ({
<Suspense fallback={<SettingsSkeletonLoader />}>
<Routes>
<Route path={SettingsPath.ProfilePage} element={<SettingsProfile />} />
<Route path={SettingsPath.Appearance} element={<SettingsAppearance />} />
<Route path={SettingsPath.Experience} element={<SettingsExperience />} />
<Route path={SettingsPath.Accounts} element={<SettingsAccounts />} />
<Route path={SettingsPath.NewAccount} element={<SettingsNewAccount />} />
<Route

View File

@ -60,6 +60,7 @@ import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirect
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { i18n } from '@lingui/core';
import { useSearchParams } from 'react-router-dom';
export const useAuth = () => {
@ -279,6 +280,7 @@ export const useAuth = () => {
)
: TimeFormat[detectTimeFormat()],
});
i18n.activate(workspaceMember.locale ?? 'en');
}
const workspace = user.currentWorkspace ?? null;

View File

@ -76,6 +76,6 @@ export const Main: Story = {};
export const Settings: Story = {
args: {
mobileNavigationDrawer: 'settings',
routePath: '/settings/appearance',
routePath: '/settings/experience',
},
};

View File

@ -16,8 +16,8 @@ import {
IconPrinter,
IconSettings,
} from 'twenty-ui';
import { FeatureFlag, FieldMetadataType } from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FeatureFlag, FeatureFlagKey } from '~/generated/graphql';
export const useRecordShowContainerTabs = (
loading: boolean,

View File

@ -106,7 +106,7 @@ export const SettingsNavigationDrawerItems = () => {
/>
<SettingsNavigationDrawerItem
label="Experience"
path={SettingsPath.Appearance}
path={SettingsPath.Experience}
Icon={IconColorSwatch}
/>
<NavigationDrawerItemGroup>

View File

@ -1,6 +1,6 @@
export enum SettingsPath {
ProfilePage = 'profile',
Appearance = 'appearance',
Experience = 'experience',
Accounts = 'accounts',
NewAccount = 'accounts/new',
AccountsCalendars = 'accounts/calendars',

View File

@ -142,7 +142,7 @@ export const Settings: Story = {
/>
<NavigationDrawerItem
label="Appearance"
to={getSettingsPagePath(SettingsPath.Appearance)}
to={getSettingsPagePath(SettingsPath.Experience)}
Icon={IconColorSwatch}
/>
<NavigationDrawerItemGroup>

View File

@ -16,6 +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 { i18n } from '@lingui/core';
import { WorkspaceMember } from '~/generated-metadata/graphql';
import { useGetCurrentUserQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
@ -91,6 +92,8 @@ export const UserProviderEffect = () => {
? getTimeFormatFromWorkspaceTimeFormat(workspaceMember.timeFormat)
: TimeFormat[detectTimeFormat()],
});
i18n.activate(workspaceMember.locale ?? 'en');
}
if (isDefined(workspaceMembers)) {

View File

@ -7,15 +7,15 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
import { userEvent, within } from '@storybook/test';
import { SettingsAppearance } from '../profile/appearance/components/SettingsAppearance';
import { SettingsExperience } from '../profile/appearance/components/SettingsExperience';
Date.now = () => new Date('2022-06-13T12:33:37.000Z').getTime();
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsAppearance',
component: SettingsAppearance,
title: 'Pages/Settings/SettingsExperience',
component: SettingsExperience,
decorators: [PageDecorator],
args: { routePath: '/settings/appearance' },
args: { routePath: '/settings/experience' },
parameters: {
msw: graphqlMocks,
date: new Date(2021, 1, 1),
@ -24,13 +24,13 @@ const meta: Meta<PageDecoratorArgs> = {
export default meta;
export type Story = StoryObj<typeof SettingsAppearance>;
export type Story = StoryObj<typeof SettingsExperience>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Appearance', undefined, {
await canvas.findAllByText('Experience', undefined, {
timeout: 3000,
});

View File

@ -0,0 +1,96 @@
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { Select } from '@/ui/input/components/Select';
import { i18n } from '@lingui/core';
import { isDefined } from '~/utils/isDefined';
import { logError } from '~/utils/logError';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
export const LocalePicker = () => {
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
currentWorkspaceMemberState,
);
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});
const updateWorkspaceMember = async (changedFields: any) => {
if (!currentWorkspaceMember?.id) {
throw new Error('User is not logged in');
}
try {
await updateOneRecord({
idToUpdate: currentWorkspaceMember.id,
updateOneRecordInput: changedFields,
});
} catch (error) {
logError(error);
}
};
if (!isDefined(currentWorkspaceMember)) return;
const handleLocaleChange = (value: string) => {
setCurrentWorkspaceMember({
...currentWorkspaceMember,
...{ locale: value },
});
updateWorkspaceMember({ locale: value });
i18n.activate(value);
};
return (
<StyledContainer>
<Select
dropdownId="preferred-locale"
dropdownWidthAuto
fullWidth
value={i18n.locale}
options={[
{
label: 'Portuguese',
value: 'pt',
},
{
label: 'French',
value: 'fr',
},
{
label: 'German',
value: 'de',
},
{
label: 'Italian',
value: 'it',
},
{
label: 'Spanish',
value: 'es',
},
{
label: 'English',
value: 'en',
},
{
label: 'Chinese',
value: 'zh',
},
]}
onChange={(value) => handleLocaleChange(value)}
/>
</StyledContainer>
);
};

View File

@ -5,11 +5,22 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
import { DateTimeSettings } from '~/pages/settings/profile/appearance/components/DateTimeSettings';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const SettingsAppearance = () => {
import { useLingui } from '@lingui/react/macro';
import { FeatureFlagKey } from '~/generated/graphql';
import { DateTimeSettings } from '~/pages/settings/profile/appearance/components/DateTimeSettings';
import { LocalePicker } from '~/pages/settings/profile/appearance/components/LocalePicker';
export const SettingsExperience = () => {
const { colorScheme, setColorScheme } = useColorScheme();
const isLocalizationEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsLocalizationEnabled,
);
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title="Experience"
@ -33,6 +44,16 @@ export const SettingsAppearance = () => {
/>
<DateTimeSettings />
</Section>
{isLocalizationEnabled && (
<Section>
<H2Title
title={t`Language`}
description={t`Select your preferred language`}
/>
<LocalePicker />
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);

View File

@ -24,7 +24,10 @@ import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserve
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { IconsProvider } from 'twenty-ui';
import { messages as enMessages } from '../../locales/en/messages';
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
export type PageDecoratorArgs = {
@ -63,35 +66,42 @@ const ApolloStorybookDevLogEffect = () => {
return <></>;
};
i18n.load({
en: enMessages,
});
i18n.activate('en');
const Providers = () => {
return (
<RecoilRoot>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<RecoilDebugObserverEffect />
<ApolloProvider client={mockedApolloClient}>
<ApolloStorybookDevLogEffect />
<ClientConfigProviderEffect />
<ClientConfigProvider>
<UserProviderEffect />
<WorkspaceProviderEffect />
<UserProvider>
<ApolloMetadataClientMockedProvider>
<ObjectMetadataItemsProvider>
<FullHeightStorybookLayout>
<HelmetProvider>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<PrefetchDataProvider>
<Outlet />
</PrefetchDataProvider>
</IconsProvider>
</SnackBarProviderScope>
</HelmetProvider>
</FullHeightStorybookLayout>
</ObjectMetadataItemsProvider>
</ApolloMetadataClientMockedProvider>
</UserProvider>
</ClientConfigProvider>
<I18nProvider i18n={i18n}>
<ApolloStorybookDevLogEffect />
<ClientConfigProviderEffect />
<ClientConfigProvider>
<UserProviderEffect />
<WorkspaceProviderEffect />
<UserProvider>
<ApolloMetadataClientMockedProvider>
<ObjectMetadataItemsProvider>
<FullHeightStorybookLayout>
<HelmetProvider>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<PrefetchDataProvider>
<Outlet />
</PrefetchDataProvider>
</IconsProvider>
</SnackBarProviderScope>
</HelmetProvider>
</FullHeightStorybookLayout>
</ObjectMetadataItemsProvider>
</ApolloMetadataClientMockedProvider>
</UserProvider>
</ClientConfigProvider>
</I18nProvider>
</ApolloProvider>
</SnackBarProviderScope>
</RecoilRoot>

View File

@ -15,8 +15,8 @@ describe('title-utils', () => {
expect(getPageTitleFromPath('/settings/profile')).toBe(
SettingsPageTitles.Profile,
);
expect(getPageTitleFromPath('/settings/appearance')).toBe(
SettingsPageTitles.Appearance,
expect(getPageTitleFromPath('/settings/experience')).toBe(
SettingsPageTitles.Experience,
);
expect(getPageTitleFromPath('/settings/accounts')).toBe(
SettingsPageTitles.Accounts,

View File

@ -4,7 +4,7 @@ import { SettingsPath } from '@/types/SettingsPath';
export enum SettingsPageTitles {
Accounts = 'Account - Settings',
Appearance = 'Appearance - Settings',
Experience = 'Experience - Settings',
Profile = 'Profile - Settings',
Objects = 'Data model - Settings',
Members = 'Members - Settings',
@ -17,7 +17,7 @@ export enum SettingsPageTitles {
enum SettingsPathPrefixes {
Accounts = `${AppBasePath.Settings}/${SettingsPath.Accounts}`,
Appearance = `${AppBasePath.Settings}/${SettingsPath.Appearance}`,
Experience = `${AppBasePath.Settings}/${SettingsPath.Experience}`,
Profile = `${AppBasePath.Settings}/${SettingsPath.ProfilePage}`,
Objects = `${AppBasePath.Settings}/${SettingsPath.Objects}`,
Members = `${AppBasePath.Settings}/${SettingsPath.WorkspaceMembersPage}`,
@ -49,8 +49,8 @@ export const getPageTitleFromPath = (pathname: string): string => {
return 'Create Workspace';
case AppPath.CreateProfile:
return 'Create Profile';
case SettingsPathPrefixes.Appearance:
return SettingsPageTitles.Appearance;
case SettingsPathPrefixes.Experience:
return SettingsPageTitles.Experience;
case SettingsPathPrefixes.Accounts:
return SettingsPageTitles.Accounts;
case SettingsPathPrefixes.Profile:

View File

@ -1,4 +1,5 @@
/* eslint-disable no-console */
import { lingui } from '@lingui/vite-plugin';
import { isNonEmptyString } from '@sniptt/guards';
import react from '@vitejs/plugin-react-swc';
import wyw from '@wyw-in-js/vite';
@ -93,11 +94,17 @@ export default defineConfig(({ command, mode }) => {
},
plugins: [
react({ jsxImportSource: '@emotion/react' }),
react({
jsxImportSource: '@emotion/react',
plugins: [['@lingui/swc-plugin', {}]],
}),
tsconfigPaths({
projects: ['tsconfig.json', '../twenty-ui/tsconfig.json'],
}),
svgr(),
lingui({
configPath: path.resolve(__dirname, './lingui.config.ts'),
}),
checker(checkers),
// TODO: fix this, we have to restrict the include to only the components that are using linaria
// Otherwise the build will fail because wyw tries to include emotion styled components

View File

@ -75,6 +75,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IsLocalizationEnabled,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};

View File

@ -15,4 +15,5 @@ export enum FeatureFlagKey {
IsCommandMenuV2Enabled = 'IS_COMMAND_MENU_V2_ENABLED',
IsCrmMigrationEnabled = 'IS_CRM_MIGRATION_ENABLED',
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
IsLocalizationEnabled = 'IS_LOCALIZATION_ENABLED',
}

View File

@ -8,4 +8,4 @@
"http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","en-US","/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","8fc30143-b648-4fc0-afe7-e55e1c452003","Europe/Paris","2024-10-17 12:38:02.518","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","20202020-9e3b-46d4-a556-88b9ddc2b034","1","20202020-1c25-4d02-bf25-6aeccf7ea419"
"http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","en-US","/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","8fc30143-b648-4fc0-afe7-e55e1c452003","Europe/Paris","2024-10-17 12:41:11.844","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","20202020-9e3b-46d4-a556-88b9ddc2b034","1","20202020-1c25-4d02-bf25-6aeccf7ea419"
"http://localhost:3001/settings/profile","en-US","/settings/profile","http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","8fc30143-b648-4fc0-afe7-e55e1c452003","Europe/Paris","2024-10-17 12:41:23.864","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","20202020-9e3b-46d4-a556-88b9ddc2b034","1","20202020-1c25-4d02-bf25-6aeccf7ea419"
"http://localhost:3001/settings/appearance","en-US","/settings/appearance","http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","8fc30143-b648-4fc0-afe7-e55e1c452003","Europe/Paris","2024-10-17 12:41:25.972","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","20202020-9e3b-46d4-a556-88b9ddc2b034","1","20202020-1c25-4d02-bf25-6aeccf7ea419"
"http://localhost:3001/settings/experience","en-US","/settings/experience","http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d","8fc30143-b648-4fc0-afe7-e55e1c452003","Europe/Paris","2024-10-17 12:41:25.972","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","20202020-9e3b-46d4-a556-88b9ddc2b034","1","20202020-1c25-4d02-bf25-6aeccf7ea419"

1 href locale pathname referrer sessionId timeZone timestamp userAgent userId version workspaceId
8 http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d en-US /settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d 8fc30143-b648-4fc0-afe7-e55e1c452003 Europe/Paris 2024-10-17 12:38:02.518 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 20202020-9e3b-46d4-a556-88b9ddc2b034 1 20202020-1c25-4d02-bf25-6aeccf7ea419
9 http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d en-US /settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d 8fc30143-b648-4fc0-afe7-e55e1c452003 Europe/Paris 2024-10-17 12:41:11.844 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 20202020-9e3b-46d4-a556-88b9ddc2b034 1 20202020-1c25-4d02-bf25-6aeccf7ea419
10 http://localhost:3001/settings/profile en-US /settings/profile http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d 8fc30143-b648-4fc0-afe7-e55e1c452003 Europe/Paris 2024-10-17 12:41:23.864 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 20202020-9e3b-46d4-a556-88b9ddc2b034 1 20202020-1c25-4d02-bf25-6aeccf7ea419
11 http://localhost:3001/settings/appearance http://localhost:3001/settings/experience en-US /settings/appearance /settings/experience http://localhost:3001/settings/developers/webhooks/41a8ad80-265a-425a-93da-35452d0ac83d 8fc30143-b648-4fc0-afe7-e55e1c452003 Europe/Paris 2024-10-17 12:41:25.972 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 20202020-9e3b-46d4-a556-88b9ddc2b034 1 20202020-1c25-4d02-bf25-6aeccf7ea419

View File

@ -28,6 +28,13 @@ npx nx run twenty-front:graphql:generate
npx nx run twenty-front:lint # pass --fix to fix lint errors
```
## Translations
```bash
npx nx run twenty-front:lingui:extract
npx nx run twenty-front:lingui:compile
```
### Test
```bash