From 7a999c8476a7c5989307b5a1a3aaa65112b3cf9c Mon Sep 17 00:00:00 2001
From: Balaji Krishnamurthy <107975017+BKM14@users.noreply.github.com>
Date: Thu, 4 Sep 2025 20:57:31 +0530
Subject: [PATCH] Format date displayed in Releases section (#14183)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #14177
I noticed a `formatDisplayDate` file being used in `twenty-website` and
created the same one for `twenty-front` as well. Unit tests for the same
have been added.
---------
Co-authored-by: Félix Malfait
---
.../calendar/components/Calendar.tsx | 8 +-
.../emails/components/EmailThreadHeader.tsx | 11 +-
.../components/EmailThreadMessageSender.tsx | 6 +-
.../emails/components/EmailThreads.tsx | 20 +-
.../files/components/Attachments.tsx | 13 +-
.../components/EventRow.tsx | 7 +-
.../modules/ai/components/AIChatMessage.tsx | 10 +-
.../components/KeyboardShortcutMenuItem.tsx | 3 +-
...SettingsAccountsBlocklistTable.stories.tsx | 5 +-
...tingsAccountsBlocklistTableRow.stories.tsx | 3 +-
.../SettingsApiKeysFieldItemTableRow.tsx | 7 +-
.../utils/__tests__/formatExpiration.test.ts | 31 +++-
.../developers/utils/formatExpiration.ts | 15 +-
.../components/SettingsRolesDefaultRole.tsx | 2 +-
.../SettingsApprovedAccessDomainsListCard.tsx | 9 +-
.../components/AbsoluteDatePickerHeader.tsx | 8 +-
.../date/utils/getMonthSelectOptions.ts | 20 +-
.../components/ShowPageSummaryCard.tsx | 15 +-
.../SettingsReleasesChangelogContent.tsx | 5 +-
.../src/utils/__tests__/date-utils.test.ts | 164 ++++++++++-------
.../src/utils/__tests__/title-utils.test.ts | 29 +--
packages/twenty-front/src/utils/date-utils.ts | 173 +++++++-----------
.../twenty-front/src/utils/title-utils.ts | 44 ++---
23 files changed, 344 insertions(+), 264 deletions(-)
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
index eeb283a42c..f0f7cc46a0 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
@@ -1,5 +1,6 @@
import styled from '@emotion/styled';
import { format, getYear } from 'date-fns';
+import { useRecoilValue } from 'recoil';
import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from '@/activities/calendar/constants/Calendar';
@@ -24,6 +25,7 @@ import {
Section,
} from 'twenty-ui/layout';
import { type TimelineCalendarEventsWithTotal } from '~/generated/graphql';
+import { dateLocaleState } from '~/localization/states/dateLocaleState';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -48,6 +50,8 @@ export const Calendar = ({
}: {
targetableObject: ActivityTargetableObject;
}) => {
+ const { localeCatalog } = useRecoilValue(dateLocaleState);
+
const [query, queryName] =
targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Person
? [
@@ -131,7 +135,9 @@ export const Calendar = ({
const year = getYear(monthTime);
const lastMonthTimeOfYear = monthTimesByYear[year]?.[0];
const isLastMonthOfYear = lastMonthTimeOfYear === monthTime;
- const monthLabel = format(monthTime, 'MMMM');
+ const monthLabel = format(monthTime, 'MMMM', {
+ locale: localeCatalog,
+ });
return (
diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadHeader.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadHeader.tsx
index bd28d7997f..233b63e077 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadHeader.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadHeader.tsx
@@ -1,5 +1,8 @@
import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
+import { useRecoilValue } from 'recoil';
+import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
type EmailThreadHeaderProps = {
@@ -40,13 +43,19 @@ export const EmailThreadHeader = ({
subject,
lastMessageSentAt,
}: EmailThreadHeaderProps) => {
+ const { localeCatalog } = useRecoilValue(dateLocaleState);
+ const lastMessageSentAtFormatted = beautifyPastDateRelativeToNow(
+ lastMessageSentAt,
+ localeCatalog,
+ );
+
return (
{subject}
{lastMessageSentAt && (
- Last message {beautifyPastDateRelativeToNow(lastMessageSentAt)}
+ {t`Last message ${lastMessageSentAtFormatted}`}
)}
diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageSender.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageSender.tsx
index dc6f2a761e..d362f6240c 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageSender.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageSender.tsx
@@ -1,7 +1,9 @@
import styled from '@emotion/styled';
+import { useRecoilValue } from 'recoil';
import { ParticipantChip } from '@/activities/components/ParticipantChip';
import { type EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
+import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
const StyledEmailThreadMessageSender = styled.div`
@@ -25,11 +27,13 @@ export const EmailThreadMessageSender = ({
sender,
sentAt,
}: EmailThreadMessageSenderProps) => {
+ const { localeCatalog } = useRecoilValue(dateLocaleState);
+
return (
- {beautifyPastDateRelativeToNow(sentAt)}
+ {beautifyPastDateRelativeToNow(sentAt, localeCatalog)}
);
diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx
index ecb6880fec..792865f25e 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx
@@ -6,15 +6,13 @@ import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview';
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging';
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId';
-import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId';
import { getTimelineThreadsFromOpportunityId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromOpportunityId';
+import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId';
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
-import {
- type TimelineThread,
- type TimelineThreadsWithTotal,
-} from '~/generated/graphql';
+import { Trans } from '@lingui/react/macro';
+import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@@ -24,7 +22,10 @@ import {
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
Section,
} from 'twenty-ui/layout';
-import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
+import {
+ type TimelineThread,
+ type TimelineThreadsWithTotal,
+} from '~/generated/graphql';
const StyledContainer = styled.div`
display: flex;
@@ -94,10 +95,10 @@ export const EmailThreads = ({
- Empty Inbox
+ Empty Inbox
- No email exchange has occurred with this record yet.
+ No email exchange has occurred with this record yet.
@@ -110,7 +111,8 @@ export const EmailThreads = ({
- Inbox {totalNumberOfThreads}
+ Inbox{' '}
+ {totalNumberOfThreads}
>
}
fontColor={H1TitleFontColor.Primary}
diff --git a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
index 4f8a83e476..12a86447f5 100644
--- a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
+++ b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx
@@ -9,6 +9,7 @@ import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttac
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
+import { Trans, useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
@@ -48,6 +49,8 @@ export const Attachments = ({
const [isDraggingFile, setIsDraggingFile] = useState(false);
+ const { t } = useLingui();
+
const onUploadFile = async (file: File) => {
await uploadAttachmentFile(file, targetableObject);
};
@@ -100,10 +103,10 @@ export const Attachments = ({
- No Files
+ No Files
- There are no associated files with this record.
+ There are no associated files with this record.
@@ -136,7 +139,7 @@ export const Attachments = ({
/>
)
diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx
index f794102758..426d9b57c7 100644
--- a/packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx
@@ -13,6 +13,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@@ -91,12 +92,16 @@ export const EventRow = ({
mainObjectMetadataItem,
}: EventRowProps) => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
+ const { localeCatalog } = useRecoilValue(dateLocaleState);
const { recordId } = useContext(TimelineActivityContext);
const recordFromStore = useRecoilValue(recordStoreFamilyState(recordId));
- const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt);
+ const beautifiedCreatedAt = beautifyPastDateRelativeToNow(
+ event.createdAt,
+ localeCatalog,
+ );
const linkedObjectMetadataItem = useLinkedObjectObjectMetadataItem(
event.linkedObjectMetadataId,
);
diff --git a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
index 0ade4f4423..379a9d2430 100644
--- a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
+++ b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
@@ -1,5 +1,6 @@
import { keyframes, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { useRecoilValue } from 'recoil';
import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
import { LazyMarkdownRenderer } from '@/ai/components/LazyMarkdownRenderer';
@@ -8,6 +9,7 @@ import { AgentChatMessageRole } from '@/ai/constants/AgentChatMessageRole';
import { LightCopyIconButton } from '@/object-record/record-field/ui/components/LightCopyIconButton';
import { type AgentChatMessage } from '~/generated/graphql';
+import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
@@ -174,6 +176,7 @@ export const AIChatMessage = ({
agentStreamingMessage: { streamingText: string; toolCall: string };
}) => {
const theme = useTheme();
+ const { localeCatalog } = useRecoilValue(dateLocaleState);
const markdownRender = (text: string) => {
return ;
@@ -248,7 +251,12 @@ export const AIChatMessage = ({
)}
{message.content && (
- {beautifyPastDateRelativeToNow(message.createdAt)}
+
+ {beautifyPastDateRelativeToNow(
+ message.createdAt,
+ localeCatalog,
+ )}
+
)}
diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuItem.tsx b/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuItem.tsx
index c64f899413..2bc52fc615 100644
--- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuItem.tsx
+++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuItem.tsx
@@ -4,6 +4,7 @@ import {
StyledShortcutKeyContainer,
} from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuStyles';
import { type Shortcut } from '@/keyboard-shortcut-menu/types/Shortcut';
+import { t } from '@lingui/core/macro';
type KeyboardMenuItemProps = {
shortcut: Shortcut;
@@ -22,7 +23,7 @@ export const KeyboardMenuItem = ({ shortcut }: KeyboardMenuItemProps) => {
) : (
{shortcut.firstHotKey}
- then
+ {t`then`}
{shortcut.secondHotKey}
)
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTable.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTable.stories.tsx
index 9ba97fe1ad..d6b4d5b2fe 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTable.stories.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTable.stories.tsx
@@ -3,8 +3,9 @@ import { expect, fn, userEvent, within } from '@storybook/test';
import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist';
import { SettingsAccountsBlocklistTable } from '@/settings/accounts/components/SettingsAccountsBlocklistTable';
-import { formatToHumanReadableDate } from '~/utils/date-utils';
import { ComponentDecorator } from 'twenty-ui/testing';
+import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
+import { formatToHumanReadableDate } from '~/utils/date-utils';
const handleBlockedEmailRemoveJestFn = fn();
@@ -18,7 +19,7 @@ const ClearMocksDecorator: Decorator = (Story, context) => {
const meta: Meta = {
title: 'Modules/Settings/Accounts/Blocklist/SettingsAccountsBlocklistTable',
component: SettingsAccountsBlocklistTable,
- decorators: [ComponentDecorator, ClearMocksDecorator],
+ decorators: [ComponentDecorator, ClearMocksDecorator, I18nFrontDecorator],
args: {
blocklist: mockedBlocklist,
handleBlockedEmailRemove: handleBlockedEmailRemoveJestFn,
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTableRow.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTableRow.stories.tsx
index b3a9a7952b..4a197b5370 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTableRow.stories.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTableRow.stories.tsx
@@ -4,6 +4,7 @@ import { expect, fn, userEvent, within } from '@storybook/test';
import { SettingsAccountsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsBlocklistTableRow';
import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist';
import { ComponentDecorator } from 'twenty-ui/testing';
+import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { formatToHumanReadableDate } from '~/utils/date-utils';
const onRemoveJestFn = fn();
@@ -19,7 +20,7 @@ const meta: Meta = {
title:
'Modules/Settings/Accounts/Blocklist/SettingsAccountsBlocklistTableRow',
component: SettingsAccountsBlocklistTableRow,
- decorators: [ComponentDecorator, ClearMocksDecorator],
+ decorators: [ComponentDecorator, ClearMocksDecorator, I18nFrontDecorator],
args: {
blocklistItem: mockedBlocklist[0],
onRemove: onRemoveJestFn,
diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
index fe1818f847..c6317ca004 100644
--- a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
@@ -1,7 +1,10 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { formatExpiration } from '@/settings/developers/utils/formatExpiration';
+import {
+ formatExpiration,
+ isExpired,
+} from '@/settings/developers/utils/formatExpiration';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@@ -64,7 +67,7 @@ export const SettingsApiKeysFieldItemTableRow = ({
{
expect(resultWithExpiresMention).toEqual('Expires in 8 years and 9 days');
});
});
+
+describe('isExpired', () => {
+ it('should return false for future dates', () => {
+ const expiresAt = '2024-01-10T00:00:00.000Z';
+ expect(isExpired(expiresAt)).toBe(false);
+ });
+
+ it('should return true for past dates', () => {
+ const expiresAt = '2023-12-01T00:00:00.000Z';
+ expect(isExpired(expiresAt)).toBe(true);
+ });
+
+ it('should return false for null dates', () => {
+ expect(isExpired(null)).toBe(false);
+ });
+
+ it('should return false for never expiring dates', () => {
+ const expiresAt = '2044-01-10T00:00:00.000Z';
+ expect(isExpired(expiresAt)).toBe(false);
+ });
+});
diff --git a/packages/twenty-front/src/modules/settings/developers/utils/formatExpiration.ts b/packages/twenty-front/src/modules/settings/developers/utils/formatExpiration.ts
index 666c81aa31..7b8b211de2 100644
--- a/packages/twenty-front/src/modules/settings/developers/utils/formatExpiration.ts
+++ b/packages/twenty-front/src/modules/settings/developers/utils/formatExpiration.ts
@@ -1,3 +1,4 @@
+import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { DateTime } from 'luxon';
@@ -12,17 +13,25 @@ export const doesNeverExpire = (expiresAt: string) => {
return dateDiff.years > NEVER_EXPIRE_DELTA_IN_YEARS / 10;
};
+export const isExpired = (expiresAt: string | null) => {
+ if (!isNonEmptyString(expiresAt) || doesNeverExpire(expiresAt)) {
+ return false;
+ }
+ const dateDiff = beautifyDateDiff(expiresAt, undefined, true);
+ return dateDiff.includes('-');
+};
+
export const formatExpiration = (
expiresAt: string | null,
withExpiresMention = false,
short = true,
) => {
if (!isNonEmptyString(expiresAt) || doesNeverExpire(expiresAt)) {
- return withExpiresMention ? 'Never expires' : 'Never';
+ return withExpiresMention ? t`Never expires` : t`Never`;
}
const dateDiff = beautifyDateDiff(expiresAt, undefined, short);
if (dateDiff.includes('-')) {
- return 'Expired';
+ return t`Expired`;
}
- return withExpiresMention ? `Expires in ${dateDiff}` : `In ${dateDiff}`;
+ return withExpiresMention ? t`Expires in ${dateDiff}` : t`In ${dateDiff}`;
};
diff --git a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesDefaultRole.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesDefaultRole.tsx
index f58da89951..6095cad506 100644
--- a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesDefaultRole.tsx
+++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesDefaultRole.tsx
@@ -76,7 +76,7 @@ export const SettingsRoleDefaultRole = ({