Fix timezone display + translate dates (#12147)
Fixes https://github.com/twentyhq/twenty/issues/11566 + translates dates - Date display bug: We had an issue with date (not date time) display depending on the timezone the user had selected. The date is stored in the db as yyyy-mm-dd, eg. 2025-05-01 for **May 1st, 2025**. When returned this date is formatted in a UTC DateTime at midnight, so 2025-05-1 00:00:00. Then when displaying the date we were converting this date using the timeZone, so 2025-04-30 17:00:00, thus displaying **April 30th, 2025**. The fix chosen is that we should not take into account the timezone for date (not date time!) displays as we always want to show the same date. - Date translation: dates were not translated, not in their default display (_May 1st, 2025_) nor in their relative display (_about a month ago_). The lib we use for date formatting, date-fns, offers a translation option with pre-built `Locale`s from their lib. Unfortunately and surprisingly we cannot just use directly string locale codes (like `fr-FR`), cf [open issue on date-fns](https://github.com/date-fns/date-fns/issues/3660). A util was introduced to offset this by dynamically importing the right date-fns Locale based on the locale code.
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
import { FieldDateMetadataSettings } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { formatDateString } from '~/utils/string/formatDateString';
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
@ -8,15 +10,16 @@ type DateDisplayProps = {
|
||||
value: string | null | undefined;
|
||||
dateFieldSettings?: FieldDateMetadataSettings;
|
||||
};
|
||||
|
||||
export const DateDisplay = ({ value, dateFieldSettings }: DateDisplayProps) => {
|
||||
const { dateFormat, timeZone } = useContext(UserContext);
|
||||
const { dateFormat } = useContext(UserContext);
|
||||
const dateLocale = useRecoilValue(dateLocaleState);
|
||||
|
||||
const formattedDate = formatDateString({
|
||||
value,
|
||||
timeZone,
|
||||
timeZone: 'UTC', // Needed because we have db-stored date (yyyy-mm-dd) is converted to UTC dateTime by TypeORM
|
||||
dateFormat,
|
||||
dateFieldSettings,
|
||||
localeCatalog: dateLocale.localeCatalog,
|
||||
});
|
||||
|
||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { FieldDateMetadataSettings } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
@ -14,6 +16,7 @@ export const DateTimeDisplay = ({
|
||||
dateFieldSettings,
|
||||
}: DateTimeDisplayProps) => {
|
||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||
const dateLocale = useRecoilValue(dateLocaleState);
|
||||
|
||||
const formattedDate = formatDateTimeString({
|
||||
value,
|
||||
@ -21,6 +24,7 @@ export const DateTimeDisplay = ({
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dateFieldSettings,
|
||||
localeCatalog: dateLocale.localeCatalog,
|
||||
});
|
||||
|
||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
import { DateDisplay } from '../DateDisplay';
|
||||
|
||||
const meta: Meta<typeof DateDisplay> = {
|
||||
title: 'UI/Field/Display/DateDisplay',
|
||||
component: DateDisplay,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
dateFormat: DateFormat.DAY_FIRST,
|
||||
timeFormat: TimeFormat.HOUR_24,
|
||||
timeZone: 'Pacific/Tahiti', // Needed for our test on time difference
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</UserContext.Provider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DateDisplay>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: '2025-05-01T00:00:00.000Z',
|
||||
dateFieldSettings: {
|
||||
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
// Test that date is rightfully displayed and not converted to timeZone date which would be on April 30th
|
||||
const canvas = within(canvasElement);
|
||||
const dateElement = await canvas.findByText('1 May, 2025');
|
||||
expect(dateElement).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,76 @@
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
|
||||
type AppLocale = keyof typeof APP_LOCALES;
|
||||
export const getDateFnsLocaleImport = (locale: AppLocale) => {
|
||||
switch (locale) {
|
||||
case 'af-ZA':
|
||||
return import('date-fns/locale/af');
|
||||
case 'ar-SA':
|
||||
return import('date-fns/locale/ar');
|
||||
case 'ca-ES':
|
||||
return import('date-fns/locale/ca');
|
||||
case 'cs-CZ':
|
||||
return import('date-fns/locale/cs');
|
||||
case 'da-DK':
|
||||
return import('date-fns/locale/da');
|
||||
case 'de-DE':
|
||||
return import('date-fns/locale/de');
|
||||
case 'el-GR':
|
||||
return import('date-fns/locale/el');
|
||||
case 'en':
|
||||
case 'pseudo-en':
|
||||
return import('date-fns/locale/en-US');
|
||||
case 'es-ES':
|
||||
return import('date-fns/locale/es');
|
||||
case 'fi-FI':
|
||||
return import('date-fns/locale/fi');
|
||||
case 'fr-FR':
|
||||
return import('date-fns/locale/fr');
|
||||
case 'he-IL':
|
||||
return import('date-fns/locale/he');
|
||||
case 'hu-HU':
|
||||
return import('date-fns/locale/hu');
|
||||
case 'it-IT':
|
||||
return import('date-fns/locale/it');
|
||||
case 'ja-JP':
|
||||
return import('date-fns/locale/ja');
|
||||
case 'ko-KR':
|
||||
return import('date-fns/locale/ko');
|
||||
case 'nl-NL':
|
||||
return import('date-fns/locale/nl');
|
||||
case 'no-NO':
|
||||
return import('date-fns/locale/nb');
|
||||
case 'pl-PL':
|
||||
return import('date-fns/locale/pl');
|
||||
case 'pt-BR':
|
||||
case 'pt-PT':
|
||||
return import('date-fns/locale/pt');
|
||||
case 'ro-RO':
|
||||
return import('date-fns/locale/ro');
|
||||
case 'ru-RU':
|
||||
return import('date-fns/locale/ru');
|
||||
case 'sr-Cyrl':
|
||||
return import('date-fns/locale/sr');
|
||||
case 'sv-SE':
|
||||
return import('date-fns/locale/sv');
|
||||
case 'tr-TR':
|
||||
return import('date-fns/locale/tr');
|
||||
case 'uk-UA':
|
||||
return import('date-fns/locale/uk');
|
||||
case 'vi-VN':
|
||||
return import('date-fns/locale/vi');
|
||||
case 'zh-CN':
|
||||
return import('date-fns/locale/zh-CN');
|
||||
case 'zh-TW':
|
||||
return import('date-fns/locale/zh-TW');
|
||||
default: {
|
||||
return import('date-fns/locale/en-US');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getDateFnsLocale = async (localeString?: string | null) => {
|
||||
return getDateFnsLocaleImport(localeString as AppLocale)
|
||||
.then((m) => m.default as unknown as Locale)
|
||||
.catch((_e) => undefined);
|
||||
};
|
||||
Reference in New Issue
Block a user