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:
Marie
2025-05-22 14:04:44 +02:00
committed by GitHub
parent 0ac4cc6899
commit b5544b6853
25 changed files with 352 additions and 68 deletions

View File

@ -1,5 +1,6 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
import { enUS } from 'date-fns/locale';
import { DateTime } from 'luxon';
import { formatDateString } from '~/utils/string/formatDateString';
@ -13,6 +14,7 @@ describe('formatDateString', () => {
const result = formatDateString({
...defaultParams,
value: null,
localeCatalog: enUS,
});
expect(result).toBe('');
@ -22,6 +24,7 @@ describe('formatDateString', () => {
const result = formatDateString({
...defaultParams,
value: undefined,
localeCatalog: enUS,
});
expect(result).toBe('');
@ -37,6 +40,7 @@ describe('formatDateString', () => {
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.RELATIVE,
},
localeCatalog: enUS,
});
expect(result).toBe(mockRelativeDate);
@ -58,6 +62,7 @@ describe('formatDateString', () => {
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
},
localeCatalog: enUS,
});
expect(result).toBe(mockFormattedDate);
@ -83,6 +88,7 @@ describe('formatDateString', () => {
displayFormat: FieldDateDisplayFormat.CUSTOM,
customUnicodeDateFormat: 'yyyy',
},
localeCatalog: enUS,
});
expect(result).toBe(mockFormattedDate);
@ -101,6 +107,7 @@ describe('formatDateString', () => {
const result = formatDateString({
...defaultParams,
value: mockDate,
localeCatalog: enUS,
});
expect(result).toBe(mockFormattedDate);

View File

@ -1,6 +1,7 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
import { enUS } from 'date-fns/locale';
import { DateTime } from 'luxon';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
@ -15,6 +16,7 @@ describe('formatDateTimeString', () => {
const result = formatDateTimeString({
...defaultParams,
value: null,
localeCatalog: enUS,
});
expect(result).toBe('');
@ -24,6 +26,7 @@ describe('formatDateTimeString', () => {
const result = formatDateTimeString({
...defaultParams,
value: undefined,
localeCatalog: enUS,
});
expect(result).toBe('');
@ -39,6 +42,7 @@ describe('formatDateTimeString', () => {
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.RELATIVE,
},
localeCatalog: enUS,
});
expect(result).toBe(mockRelativeDate);
@ -60,6 +64,7 @@ describe('formatDateTimeString', () => {
dateFieldSettings: {
displayFormat: FieldDateDisplayFormat.USER_SETTINGS,
},
localeCatalog: enUS,
});
expect(result).toBe(mockFormattedDate);
@ -85,6 +90,7 @@ describe('formatDateTimeString', () => {
displayFormat: FieldDateDisplayFormat.CUSTOM,
customUnicodeDateFormat: 'yyyy',
},
localeCatalog: enUS,
});
expect(result).toBe(mockFormattedDate);
@ -103,6 +109,7 @@ describe('formatDateTimeString', () => {
const result = formatDateTimeString({
...defaultParams,
value: mockDate,
localeCatalog: enUS,
});
expect(result).toBe(mockFormattedDate);

View File

@ -13,11 +13,13 @@ export const formatDateString = ({
timeZone,
dateFormat,
dateFieldSettings,
localeCatalog,
}: {
timeZone: string;
dateFormat: DateFormat;
value?: string | null;
dateFieldSettings?: FieldDateMetadataSettings;
localeCatalog: Locale;
}): string => {
if (!isDefined(value)) {
return '';
@ -25,16 +27,31 @@ export const formatDateString = ({
switch (dateFieldSettings?.displayFormat) {
case FieldDateDisplayFormat.RELATIVE:
return formatDateISOStringToRelativeDate(value);
return formatDateISOStringToRelativeDate({
isoDate: value,
isDayMaximumPrecision: true,
localeCatalog,
});
case FieldDateDisplayFormat.USER_SETTINGS:
return formatDateISOStringToDate(value, timeZone, dateFormat);
case FieldDateDisplayFormat.CUSTOM:
return formatDateISOStringToCustomUnicodeFormat(
value,
return formatDateISOStringToDate({
date: value,
timeZone,
dateFieldSettings.customUnicodeDateFormat,
);
dateFormat,
localeCatalog,
});
case FieldDateDisplayFormat.CUSTOM:
return formatDateISOStringToCustomUnicodeFormat({
date: value,
timeZone,
dateFormat: dateFieldSettings.customUnicodeDateFormat,
localeCatalog,
});
default:
return formatDateISOStringToDate(value, timeZone, dateFormat);
return formatDateISOStringToDate({
date: value,
timeZone,
dateFormat,
localeCatalog,
});
}
};

View File

@ -7,6 +7,7 @@ import {
FieldDateDisplayFormat,
FieldDateMetadataSettings,
} from '@/object-record/record-field/types/FieldMetadata';
import { Locale } from 'date-fns';
export const formatDateTimeString = ({
value,
@ -14,12 +15,14 @@ export const formatDateTimeString = ({
dateFormat,
timeFormat,
dateFieldSettings,
localeCatalog,
}: {
timeZone: string;
dateFormat: DateFormat;
timeFormat: TimeFormat;
value?: string | null;
dateFieldSettings?: FieldDateMetadataSettings;
localeCatalog: Locale;
}) => {
if (!value) {
return '';
@ -27,26 +30,32 @@ export const formatDateTimeString = ({
switch (dateFieldSettings?.displayFormat) {
case FieldDateDisplayFormat.RELATIVE:
return formatDateISOStringToRelativeDate(value);
return formatDateISOStringToRelativeDate({
isoDate: value,
localeCatalog: localeCatalog,
});
case FieldDateDisplayFormat.USER_SETTINGS:
return formatDateISOStringToDateTime(
value,
return formatDateISOStringToDateTime({
date: value,
timeZone,
dateFormat,
timeFormat,
);
localeCatalog,
});
case FieldDateDisplayFormat.CUSTOM:
return formatDateISOStringToCustomUnicodeFormat(
value,
return formatDateISOStringToCustomUnicodeFormat({
date: value,
timeZone,
dateFieldSettings.customUnicodeDateFormat,
);
dateFormat: dateFieldSettings.customUnicodeDateFormat,
localeCatalog,
});
default:
return formatDateISOStringToDateTime(
value,
return formatDateISOStringToDateTime({
date: value,
timeZone,
dateFormat,
timeFormat,
);
localeCatalog,
});
}
};