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,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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user