From 5345d05176f76532e99cb6bef20372920c80f06b Mon Sep 17 00:00:00 2001 From: Raghav Bhardwaj <118196545+RaghavBhardwaj18@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:39:12 +0000 Subject: [PATCH] fixed 24 hour locale system bug (#11719) --- ui/desktop/src/components/Hub.tsx | 14 ++-- ui/desktop/src/utils/timeUtils.test.ts | 100 +++++++++++++++++++++++++ ui/desktop/src/utils/timeUtils.ts | 39 ++++++++++ 3 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 ui/desktop/src/utils/timeUtils.test.ts diff --git a/ui/desktop/src/components/Hub.tsx b/ui/desktop/src/components/Hub.tsx index 46e684d4d..91e7da555 100644 --- a/ui/desktop/src/components/Hub.tsx +++ b/ui/desktop/src/components/Hub.tsx @@ -27,6 +27,7 @@ import { } from '../utils/nextChatExtensions'; import { formatAcpError } from '../acp/errors'; import { toastError } from '../toasts'; +import { formatClockDisplay } from '../utils/timeUtils'; const i18n = defineMessages({ goodMorning: { id: 'hub.goodMorning', defaultMessage: 'Good morning' }, @@ -34,19 +35,14 @@ const i18n = defineMessages({ goodEvening: { id: 'hub.goodEvening', defaultMessage: 'Good evening' }, }); -function useClock(): { time: string; meridiem: string; hour: number } { +function useClock() { const [now, setNow] = useState(() => new Date()); useEffect(() => { const interval = setInterval(() => setNow(new Date()), 30_000); return () => clearInterval(interval); }, []); - const hour = now.getHours(); - const minutes = now.getMinutes(); - const meridiem = hour >= 12 ? 'PM' : 'AM'; - const displayHour = ((hour + 11) % 12) + 1; - const time = `${displayHour}:${String(minutes).padStart(2, '0')}`; - return { time, meridiem, hour }; + return formatClockDisplay(now); } export default function Hub({ @@ -151,7 +147,9 @@ export default function Hub({ {time} - {meridiem} + {meridiem ? ( + {meridiem} + ) : null}

{greeting}

diff --git a/ui/desktop/src/utils/timeUtils.test.ts b/ui/desktop/src/utils/timeUtils.test.ts new file mode 100644 index 000000000..22806f85d --- /dev/null +++ b/ui/desktop/src/utils/timeUtils.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from 'vitest'; +import { formatClockDisplay, formatMessageTimestamp } from './timeUtils'; + +describe('timeUtils', () => { + describe('formatClockDisplay', () => { + it('formats 12-hour locale with AM/PM meridiem (en-US)', () => { + // 8:49 PM + const eveningDate = new Date(2026, 8, 1, 20, 49, 0); + const eveningResult = formatClockDisplay(eveningDate, 'en-US'); + expect(eveningResult.time).toBe('8:49'); + expect(eveningResult.meridiem).toBe('PM'); + expect(eveningResult.hour).toBe(20); + + // 8:49 AM + const morningDate = new Date(2026, 8, 1, 8, 49, 0); + const morningResult = formatClockDisplay(morningDate, 'en-US'); + expect(morningResult.time).toBe('8:49'); + expect(morningResult.meridiem).toBe('AM'); + expect(morningResult.hour).toBe(8); + + // Midnight (12:00 AM) + const midnight = new Date(2026, 8, 1, 0, 0, 0); + const midnightResult = formatClockDisplay(midnight, 'en-US'); + expect(midnightResult.time).toBe('12:00'); + expect(midnightResult.meridiem).toBe('AM'); + expect(midnightResult.hour).toBe(0); + + // Noon (12:00 PM) + const noon = new Date(2026, 8, 1, 12, 0, 0); + const noonResult = formatClockDisplay(noon, 'en-US'); + expect(noonResult.time).toBe('12:00'); + expect(noonResult.meridiem).toBe('PM'); + expect(noonResult.hour).toBe(12); + }); + + it('formats 24-hour locale without meridiem (en-GB)', () => { + const eveningDate = new Date(2026, 8, 1, 20, 49, 0); + const result = formatClockDisplay(eveningDate, 'en-GB'); + expect(result.time).toBe('20:49'); + expect(result.meridiem).toBe(''); + expect(result.hour).toBe(20); + + const midnight = new Date(2026, 8, 1, 0, 5, 0); + const midnightResult = formatClockDisplay(midnight, 'en-GB'); + expect(midnightResult.time).toBe('0:05'); + expect(midnightResult.meridiem).toBe(''); + expect(midnightResult.hour).toBe(0); + }); + + it('formats 24-hour European locales without meridiem (de-DE, sv-SE, fr-FR)', () => { + const eveningDate = new Date(2026, 8, 1, 20, 49, 0); + + const deResult = formatClockDisplay(eveningDate, 'de-DE'); + expect(deResult.time).toBe('20:49'); + expect(deResult.meridiem).toBe(''); + + const svResult = formatClockDisplay(eveningDate, 'sv-SE'); + expect(svResult.time).toBe('20:49'); + expect(svResult.meridiem).toBe(''); + + const frResult = formatClockDisplay(eveningDate, 'fr-FR'); + expect(frResult.time).toBe('20:49'); + expect(frResult.meridiem).toBe(''); + }); + + it('defaults to current date when no date is supplied', () => { + const result = formatClockDisplay(); + expect(result).toHaveProperty('time'); + expect(result).toHaveProperty('meridiem'); + expect(result).toHaveProperty('hour'); + expect(typeof result.time).toBe('string'); + expect(typeof result.hour).toBe('number'); + }); + + it('handles unexpected/invalid locale gracefully using fallback', () => { + const eveningDate = new Date(2026, 8, 1, 20, 49, 0); + const result = formatClockDisplay(eveningDate, 'invalid-locale-!!!'); + expect(result.time).toBe('8:49'); + expect(result.meridiem).toBe('PM'); + expect(result.hour).toBe(20); + }); + }); + + describe('formatMessageTimestamp', () => { + it('formats timestamp from today with time only', () => { + const now = new Date(); + const timestamp = Math.floor(now.getTime() / 1000); + const result = formatMessageTimestamp(timestamp); + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + + it('formats timestamp from previous date with date and time', () => { + const pastDate = new Date(2025, 0, 1, 12, 0, 0); + const timestamp = Math.floor(pastDate.getTime() / 1000); + const result = formatMessageTimestamp(timestamp); + expect(result).toContain('2025'); + }); + }); +}); diff --git a/ui/desktop/src/utils/timeUtils.ts b/ui/desktop/src/utils/timeUtils.ts index 6de531d05..e4fc4408b 100644 --- a/ui/desktop/src/utils/timeUtils.ts +++ b/ui/desktop/src/utils/timeUtils.ts @@ -28,3 +28,42 @@ export function formatMessageTimestamp(timestamp?: number): string { return `${dateStr} ${timeStr}`; } + +export interface ClockDisplay { + time: string; + meridiem: string; + hour: number; +} + +export function formatClockDisplay( + date: Date = new Date(), + locale: string = currentLocale +): ClockDisplay { + const hour = date.getHours(); + + try { + const formatter = new Intl.DateTimeFormat(locale, { + hour: 'numeric', + minute: '2-digit', + }); + + const parts = formatter.formatToParts(date); + const dayPeriodPart = parts.find((p) => p.type === 'dayPeriod'); + const meridiem = dayPeriodPart ? dayPeriodPart.value : ''; + + const time = parts + .filter((p) => p.type !== 'dayPeriod') + .map((p) => p.value) + .join('') + .trim(); + + return { time, meridiem, hour }; + } catch { + const minutes = date.getMinutes(); + const meridiem = hour >= 12 ? 'PM' : 'AM'; + const displayHour = ((hour + 11) % 12) + 1; + const time = `${displayHour}:${String(minutes).padStart(2, '0')}`; + return { time, meridiem, hour }; + } +} +