diff --git a/packages/ui/src/components/chat/message/MessageBody.tsx b/packages/ui/src/components/chat/message/MessageBody.tsx index eda9f26e..7f93bbf6 100644 --- a/packages/ui/src/components/chat/message/MessageBody.tsx +++ b/packages/ui/src/components/chat/message/MessageBody.tsx @@ -1038,6 +1038,7 @@ const AssistantMessageBody = React.memo(({ const collapsibleThinkingBlocks = useUIStore((state) => state.collapsibleThinkingBlocks); const groupReasoningBlocks = useUIStore((state) => state.groupReasoningBlocks); const showSplitAssistantMessageActions = useUIStore((state) => state.showSplitAssistantMessageActions); + const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const vscodeApi = useRuntimeAPIs().vscode; const isSortedRenderMode = chatRenderMode === 'sorted'; const collapsedPreviewCount = 7; @@ -1757,9 +1758,9 @@ const AssistantMessageBody = React.memo(({ : (typeof messageCreatedAt === 'number' && messageCreatedAt > 0 ? messageCreatedAt : null); if (timestamp === null) return null; - const formatted = formatTimestampForDisplay(timestamp); + const formatted = formatTimestampForDisplay(timestamp, timeFormatPreference); return formatted.length > 0 ? formatted : null; - }, [messageCompletedAt, messageCreatedAt]); + }, [messageCompletedAt, messageCreatedAt, timeFormatPreference]); const footerTimestampClassName = 'text-sm text-muted-foreground/60 tabular-nums flex items-center gap-1'; const canOpenMessagePreview = !isMiniChatSurface && !isMobile && !isVSCode; diff --git a/packages/ui/src/components/chat/message/timeFormat.ts b/packages/ui/src/components/chat/message/timeFormat.ts index bd92f08e..610b4aff 100644 --- a/packages/ui/src/components/chat/message/timeFormat.ts +++ b/packages/ui/src/components/chat/message/timeFormat.ts @@ -1,4 +1,5 @@ -const pad2 = (value: number): string => String(value).padStart(2, '0'); +import { formatTimeForPreference } from '@/lib/timeFormat'; +import type { TimeFormatPreference } from '@/stores/useUIStore'; const isSameDay = (left: Date, right: Date): boolean => { return ( @@ -18,7 +19,7 @@ const isValidTimestamp = (timestamp: number): boolean => { return Number.isFinite(timestamp) && !Number.isNaN(new Date(timestamp).getTime()); }; -export const formatTimestampForDisplay = (timestamp: number): string => { +export const formatTimestampForDisplay = (timestamp: number, timeFormatPreference: TimeFormatPreference): string => { if (!isValidTimestamp(timestamp)) { return ''; } @@ -26,7 +27,7 @@ export const formatTimestampForDisplay = (timestamp: number): string => { const date = new Date(timestamp); const now = new Date(); - const timePart = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`; + const timePart = formatTimeForPreference(date, timeFormatPreference); if (isSameDay(date, now)) { return timePart; diff --git a/packages/ui/src/components/layout/ContextSidebarTab.tsx b/packages/ui/src/components/layout/ContextSidebarTab.tsx index 15879613..a14918d0 100644 --- a/packages/ui/src/components/layout/ContextSidebarTab.tsx +++ b/packages/ui/src/components/layout/ContextSidebarTab.tsx @@ -11,6 +11,8 @@ import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessions, useSessionMessageRecords } from '@/sync/sync-context'; import { copyTextToClipboard } from '@/lib/clipboard'; import { useI18n } from '@/lib/i18n'; +import { formatDateTimeForPreference } from '@/lib/timeFormat'; +import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore'; type SessionMessage = { info: Message; parts: Part[] }; @@ -229,9 +231,9 @@ const formatMoney = (value: number): string => { return `$${value.toFixed(2)}`; }; -const formatDateTime = (timestamp: number | null): string => { +const formatDateTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => { if (!timestamp || !Number.isFinite(timestamp)) return '-'; - return new Date(timestamp).toLocaleString(undefined, { + return formatDateTimeForPreference(timestamp, timeFormatPreference, { month: 'short', day: 'numeric', year: 'numeric', @@ -240,9 +242,9 @@ const formatDateTime = (timestamp: number | null): string => { }); }; -const formatMessageDateMeta = (timestamp: number | null): string => { +const formatMessageDateMeta = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => { if (!timestamp || !Number.isFinite(timestamp)) return '-'; - return new Date(timestamp).toLocaleString(undefined, { + return formatDateTimeForPreference(timestamp, timeFormatPreference, { month: 'short', day: 'numeric', hour: 'numeric', @@ -273,6 +275,7 @@ const resolveProviderAndModel = ( export const ContextPanelContent: React.FC = () => { const { t } = useI18n(); const { currentTheme } = useThemeSystem(); + const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]); const [expandedRawMessages, setExpandedRawMessages] = React.useState>({}); const [copiedRawMessageId, setCopiedRawMessageId] = React.useState(null); @@ -416,7 +419,7 @@ export const ContextPanelContent: React.FC = () => { {viewModel.createdAt && ( <> · - {formatDateTime(viewModel.createdAt)} + {formatDateTime(viewModel.createdAt, timeFormatPreference)} )} @@ -547,7 +550,7 @@ export const ContextPanelContent: React.FC = () => { {capitalizeRole(role)} {message.info.id} - {formatMessageDateMeta(messageCreatedAt)} + {formatMessageDateMeta(messageCreatedAt, timeFormatPreference)} diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index dc02f2cc..2c3b850d 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -41,7 +41,9 @@ import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_ import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar'; import { PaceIndicator } from '@/components/sections/usage/PaceIndicator'; import { updateDesktopSettings } from '@/lib/persistence'; +import { formatTimeForPreference } from '@/lib/timeFormat'; import { eventMatchesShortcut, formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts'; +import type { TimeFormatPreference } from '@/stores/useUIStore'; import { getAllModelFamilies, getDisplayModelName, @@ -356,6 +358,7 @@ type DesktopServicesMenuProps = { remoteUpdateError: string | null; onOpenRemoteUpdate: () => void; showPredValues: boolean; + timeFormatPreference: TimeFormatPreference; }; const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ @@ -391,6 +394,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ remoteUpdateError, onOpenRemoteUpdate, showPredValues, + timeFormatPreference, }: DesktopServicesMenuProps) { const { t } = useI18n(); return ( @@ -511,7 +515,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
{t('header.services.rateLimits')} - {formatTime(quotaLastUpdated)} + {formatTime(quotaLastUpdated, timeFormatPreference)}
@@ -572,7 +576,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({ : calculateExpectedUsagePercent(paceInfo.elapsedRatio)) : null; const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent); - const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted); + const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference); return (
@@ -714,13 +718,10 @@ const formatCompactHeaderLabel = (value: string): string => { return trimmed.length > 12 ? `${trimmed.slice(0, 9).trimEnd()}...` : trimmed; }; -const formatTime = (timestamp: number | null) => { +const formatTime = (timestamp: number | null, timeFormatPreference: 'auto' | '12h' | '24h') => { if (!timestamp) return '-'; try { - return new Date(timestamp).toLocaleTimeString(undefined, { - hour: 'numeric', - minute: '2-digit', - }); + return formatTimeForPreference(timestamp, timeFormatPreference, { fallback: '-' }); } catch { return '-'; } @@ -791,6 +792,7 @@ export const Header: React.FC = ({ const activeMainTab = useUIStore((state) => state.activeMainTab); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); + const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const getCurrentModel = useConfigStore((state) => state.getCurrentModel); const runtimeApis = useRuntimeAPIs(); @@ -2089,6 +2091,7 @@ export const Header: React.FC = ({ remoteUpdateChecking={remoteUpdateChecking} remoteUpdateError={remoteUpdateError} onOpenRemoteUpdate={openRemoteInstanceUpdate} + timeFormatPreference={timeFormatPreference} /> = ({
{t('header.services.rateLimits')} - {formatTime(quotaLastUpdated)} + {formatTime(quotaLastUpdated, timeFormatPreference)}
@@ -2533,7 +2536,7 @@ export const Header: React.FC = ({ : calculateExpectedUsagePercent(paceInfo.elapsedRatio)) : null; const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent); - const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted); + const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference); return (
diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index fdd509e0..d238d2ba 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -30,19 +30,18 @@ import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_ import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore'; import { useUpdateStore } from '@/stores/useUpdateStore'; import { updateDesktopSettings } from '@/lib/persistence'; +import { formatTimeForPreference } from '@/lib/timeFormat'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; import type { UsageWindow } from '@/types'; import type { SessionContextUsage } from '@/stores/types/sessionTypes'; +import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore'; const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView }))); -const formatTime = (timestamp: number | null) => { +const formatTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference) => { if (!timestamp) return '-'; try { - return new Date(timestamp).toLocaleTimeString(undefined, { - hour: 'numeric', - minute: '2-digit', - }); + return formatTimeForPreference(timestamp, timeFormatPreference, { fallback: '-' }); } catch { return '-'; } @@ -583,6 +582,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on const quotaLastUpdated = useQuotaStore((state) => state.lastUpdated); const quotaDisplayMode = useQuotaStore((state) => state.displayMode); const showPredValues = useQuotaStore((state) => state.showPredValues); + const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const dropdownProviderIds = useQuotaStore((state) => state.dropdownProviderIds); const loadQuotaSettings = useQuotaStore((state) => state.loadSettings); const setQuotaDisplayMode = useQuotaStore((state) => state.setDisplayMode); @@ -839,7 +839,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on
- {t('vscodeLayout.quota.lastUpdated', { time: formatTime(quotaLastUpdated) })} + {t('vscodeLayout.quota.lastUpdated', { time: formatTime(quotaLastUpdated, timeFormatPreference) })}
{!hasRateLimits && ( @@ -899,7 +899,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on
)} - {formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted)} + {formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference)} diff --git a/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx b/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx index 55315631..ae4cda65 100644 --- a/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx +++ b/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx @@ -15,8 +15,9 @@ import { type StoredPasskey, } from '@/lib/passkeys'; import { useI18n } from '@/lib/i18n'; +import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore'; -const formatTimestamp = (timestamp: number | null, neverUsedText: string) => { +const formatTimestamp = (timestamp: number | null, neverUsedText: string, timeFormatPreference: TimeFormatPreference) => { if (!timestamp || !Number.isFinite(timestamp)) { return neverUsedText; } @@ -24,11 +25,13 @@ const formatTimestamp = (timestamp: number | null, neverUsedText: string) => { return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short', + hour12: timeFormatPreference === 'auto' ? undefined : timeFormatPreference === '12h', }).format(timestamp); }; export const PasskeySettings: React.FC = () => { const { t } = useI18n(); + const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const [supportsPasskeys, setSupportsPasskeys] = React.useState(false); const [isLoading, setIsLoading] = React.useState(true); const [isRegistering, setIsRegistering] = React.useState(false); @@ -230,10 +233,10 @@ export const PasskeySettings: React.FC = () => { {passkey.lastUsedAt ? t('settings.openchamber.passkeys.item.lastUsed', { - time: formatTimestamp(passkey.lastUsedAt, t('settings.openchamber.passkeys.time.neverUsed')), + time: formatTimestamp(passkey.lastUsedAt, t('settings.openchamber.passkeys.time.neverUsed'), timeFormatPreference), }) : t('settings.openchamber.passkeys.item.added', { - time: formatTimestamp(passkey.createdAt, t('settings.openchamber.passkeys.time.neverUsed')), + time: formatTimestamp(passkey.createdAt, t('settings.openchamber.passkeys.time.neverUsed'), timeFormatPreference), })}
diff --git a/packages/ui/src/components/session/ScheduledTasksDialog.tsx b/packages/ui/src/components/session/ScheduledTasksDialog.tsx index e1d72bdc..8f6e6e29 100644 --- a/packages/ui/src/components/session/ScheduledTasksDialog.tsx +++ b/packages/ui/src/components/session/ScheduledTasksDialog.tsx @@ -14,6 +14,8 @@ import { toast } from '@/components/ui'; import { Icon } from "@/components/icon/Icon"; import type { IconName } from "@/components/icon/icons"; import { useUIStore } from '@/stores/useUIStore'; +import { formatTimeForPreference } from '@/lib/timeFormat'; +import type { TimeFormatPreference } from '@/stores/useUIStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { refreshGlobalSessions } from '@/stores/useGlobalSessionsStore'; @@ -100,11 +102,11 @@ const formatSchedule = (task: ScheduledTask, t: ReturnType['t']) return t('sessions.scheduledTasks.dialog.schedule.cron', { cron: task.schedule.cron || '' }); }; -const formatClockTime = (value?: number): string => { +const formatClockTime = (value: number | undefined, timeFormatPreference: TimeFormatPreference): string => { if (!value || !Number.isFinite(value)) { return ''; } - return new Date(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); + return formatTimeForPreference(value, timeFormatPreference); }; const formatRelativeTime = (value: number | undefined, t: ReturnType['t']): string => { @@ -174,6 +176,7 @@ export function ScheduledTasksDialog() { const open = useUIStore((state) => state.isScheduledTasksDialogOpen); const setOpen = useUIStore((state) => state.setScheduledTasksDialogOpen); const isMobile = useUIStore((state) => state.isMobile); + const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const projects = useProjectsStore((state) => state.projects); const activeProject = useProjectsStore((state) => state.getActiveProject()); const homeDirectory = useDirectoryStore((state) => state.homeDirectory); @@ -471,7 +474,7 @@ export function ScheduledTasksDialog() { <> {formatRelativeTime(nextAt, t)} · - {formatClockTime(nextAt)} + {formatClockTime(nextAt, timeFormatPreference)} ) : ( diff --git a/packages/ui/src/components/views/git/HistoryCommitRow.tsx b/packages/ui/src/components/views/git/HistoryCommitRow.tsx index e4a28efc..7f938860 100644 --- a/packages/ui/src/components/views/git/HistoryCommitRow.tsx +++ b/packages/ui/src/components/views/git/HistoryCommitRow.tsx @@ -18,6 +18,8 @@ import type { LanedCommit } from './gitGraph'; import { GitGraphSegment } from './GitGraphSegment'; import * as git from '@/lib/gitApi'; import { toast } from '@/components/ui/toast'; +import { formatDateTimeForPreference } from '@/lib/timeFormat'; +import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore'; const HISTORY_DIFF_REQUEST_TIMEOUT_MS = 15000; const HISTORY_DIFF_LARGE_CHANGED_LINES = 500; @@ -77,14 +79,13 @@ interface HistoryCommitRowProps { onActionSuccess?: () => void; } -function formatCommitDate(date: string) { +function formatCommitDate(date: string, timeFormatPreference: TimeFormatPreference) { const value = new Date(date); if (Number.isNaN(value.getTime())) { return date; } - return value.toLocaleString(undefined, { - hour12: false, + return formatDateTimeForPreference(value, timeFormatPreference, { year: 'numeric', month: 'short', day: 'numeric', @@ -146,6 +147,7 @@ export const HistoryCommitRow = React.memo(({ onActionSuccess, }: HistoryCommitRowProps) => { const { t } = useI18n(); + const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const isGraphMode = mode === 'graph'; type PendingAction = | 'checkout' | 'cherryPick' | 'revert' @@ -397,8 +399,8 @@ export const HistoryCommitRow = React.memo(({ {entry.author_name} · - - {formatCommitDate(entry.date)} + + {formatCommitDate(entry.date, timeFormatPreference)}
· diff --git a/packages/ui/src/components/views/git/PullRequestSection.tsx b/packages/ui/src/components/views/git/PullRequestSection.tsx index 09183068..4ac6d5e6 100644 --- a/packages/ui/src/components/views/git/PullRequestSection.tsx +++ b/packages/ui/src/components/views/git/PullRequestSection.tsx @@ -28,6 +28,7 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer'; import { Icon } from "@/components/icon/Icon"; import { useUIStore } from '@/stores/useUIStore'; +import { formatDateTimeForPreference } from '@/lib/timeFormat'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSelectionStore } from '@/sync/selection-store'; import { useConfigStore } from '@/stores/useConfigStore'; @@ -309,6 +310,7 @@ export const PullRequestSection: React.FC<{ onGeneratedDescription?: () => void; }> = ({ directory, branch, baseBranch, trackingBranch, remotes = [], remoteBranches = [], onGeneratedDescription }) => { const { t } = useI18n(); + const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const { github } = useRuntimeAPIs(); const githubAuthStatus = useGitHubAuthStore((state) => state.status); const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked); @@ -627,8 +629,14 @@ export const PullRequestSection: React.FC<{ if (!Number.isFinite(ts)) { return value; } - return new Date(ts).toLocaleString(); - }, []); + return formatDateTimeForPreference(ts, timeFormatPreference, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }); + }, [timeFormatPreference]); const connectedGitHubLogin = React.useMemo(() => { const login = githubAuthStatus?.user?.login; diff --git a/packages/ui/src/lib/quota/utils.ts b/packages/ui/src/lib/quota/utils.ts index c6f980d9..fbac3d0d 100644 --- a/packages/ui/src/lib/quota/utils.ts +++ b/packages/ui/src/lib/quota/utils.ts @@ -1,3 +1,6 @@ +import { formatDateTimeForPreference, formatTimeForPreference } from '@/lib/timeFormat'; +import type { TimeFormatPreference } from '@/stores/useUIStore'; + export const clampPercent = (value: number | null): number | null => { if (typeof value !== 'number' || !Number.isFinite(value)) { return null; @@ -22,6 +25,7 @@ export const formatQuotaValueLabel = ( export const formatQuotaResetLabel = ( resetAt: number | null, fallback?: string | null, + timeFormatPreference: TimeFormatPreference = 'auto', ): string => { if (!resetAt) { return fallback ?? ''; @@ -37,13 +41,10 @@ export const formatQuotaResetLabel = ( const isToday = resetDate.toDateString() === now.toDateString(); if (isToday) { - return resetDate.toLocaleTimeString(undefined, { - hour: 'numeric', - minute: '2-digit', - }); + return formatTimeForPreference(resetDate, timeFormatPreference, { fallback: fallback ?? '' }); } - return resetDate.toLocaleString(undefined, { + return formatDateTimeForPreference(resetDate, timeFormatPreference, { month: 'short', day: 'numeric', weekday: 'short', diff --git a/packages/ui/src/lib/timeFormat.ts b/packages/ui/src/lib/timeFormat.ts new file mode 100644 index 00000000..de2fedc9 --- /dev/null +++ b/packages/ui/src/lib/timeFormat.ts @@ -0,0 +1,58 @@ +import type { TimeFormatPreference } from '@/stores/useUIStore'; + +type TimePrecision = 'minute' | 'second'; + +const getHour12Option = (preference: TimeFormatPreference): boolean | undefined => { + if (preference === '12h') return true; + if (preference === '24h') return false; + return undefined; +}; + +export const getUses24HourForPreference = (preference: TimeFormatPreference, locale: string): boolean => { + if (preference === '24h') return true; + if (preference === '12h') return false; + + try { + const options = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions(); + if (typeof options.hour12 === 'boolean') { + return !options.hour12; + } + return options.hourCycle === 'h23' || options.hourCycle === 'h24'; + } catch { + return true; + } +}; + +export const formatTimeForPreference = ( + timestamp: number | Date, + preference: TimeFormatPreference, + options: { precision?: TimePrecision; hour?: 'numeric' | '2-digit'; fallback?: string } = {}, +): string => { + const date = timestamp instanceof Date ? timestamp : new Date(timestamp); + if (!Number.isFinite(date.getTime())) { + return options.fallback ?? ''; + } + + return date.toLocaleTimeString(undefined, { + hour: options.hour ?? 'numeric', + minute: '2-digit', + second: options.precision === 'second' ? '2-digit' : undefined, + hour12: getHour12Option(preference), + }); +}; + +export const formatDateTimeForPreference = ( + timestamp: number | Date, + preference: TimeFormatPreference, + options: Intl.DateTimeFormatOptions, +): string => { + const date = timestamp instanceof Date ? timestamp : new Date(timestamp); + if (!Number.isFinite(date.getTime())) { + return ''; + } + + return date.toLocaleString(undefined, { + ...options, + hour12: options.hour ? getHour12Option(preference) : options.hour12, + }); +};