diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa618d6b..89da2a5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -210,6 +210,12 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa
* Buttons live in a new **`.hero-nav`** flex wrapper (`inset: 0`, `justify-content: space-between`, **`pointer-events: none`**); the buttons themselves opt back into **`pointer-events: auto`** so the rest of the hero stays click-through to the album page. Wrap-around (last → first / first → last) and auto-advance timer restart use the same pattern as the previous dot handler.
* The dot indicators are kept as **decorative spans** — no click handler, no hover state, **`pointer-events: none`** — so a missed click no longer navigates to the album.
+### Settings — Clock Format setting (Auto / 24h / 12h)
+
+**By [@Psychotoxical](https://github.com/Psychotoxical), thanks to zunoz for the report on the Psysonic Discord, PR [#742](https://github.com/Psychotoxical/psysonic/pull/742)**
+
+* The Queue side panel's ETA label and the sleep-timer preview both go through **`formatClockTime`**, which just delegates to **`toLocaleTimeString`** — on en-US that meant AM/PM with no in-app override. **Settings → System → App Behavior** now exposes a tri-state **Clock Format** select: **`Auto`** (default — keeps existing locale-driven behaviour, so first launch after the update is a no-op for everyone), **`24h`**, and **`12h`**, the explicit values forcing **`hour12`** everywhere `formatClockTime` is used. Wired through `authStore` (`clockFormat` / `setClockFormat`) and consumed by both surfaces; all nine bundled locales ship the four new strings.
+
## Changed
### Backend — Cargo workspace with 5 domain crates (Rust refactor)
diff --git a/src/components/PlaybackDelayModal.tsx b/src/components/PlaybackDelayModal.tsx
index aefa9321..98c528ca 100644
--- a/src/components/PlaybackDelayModal.tsx
+++ b/src/components/PlaybackDelayModal.tsx
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
import { X, Moon, Sunrise } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { usePlayerStore } from '../store/playerStore';
+import { useAuthStore } from '../store/authStore';
import { useShallow } from 'zustand/react/shallow';
import type { TFunction } from 'i18next';
@@ -153,9 +154,10 @@ export default function PlaybackDelayModal({ open, onClose, anchorRef }: Playbac
// Live preview: seconds that would be applied right now if the user clicked.
// Priority: hovered chip → typed custom minutes → nothing.
+ const clockFormat = useAuthStore(s => s.clockFormat);
const previewSeconds = hoverSeconds ?? customSeconds;
const previewAtMs = previewSeconds != null ? nowTick + previewSeconds * 1000 : null;
- const previewClock = previewAtMs != null ? formatClockTime(previewAtMs) : null;
+ const previewClock = previewAtMs != null ? formatClockTime(previewAtMs, clockFormat) : null;
if (!open) return null;
diff --git a/src/components/queuePanel/QueueHeader.tsx b/src/components/queuePanel/QueueHeader.tsx
index c5be0c33..6065ca18 100644
--- a/src/components/queuePanel/QueueHeader.tsx
+++ b/src/components/queuePanel/QueueHeader.tsx
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import { ChevronDown, ListMusic } from 'lucide-react';
import type { TFunction } from 'i18next';
import { usePlayerStore } from '../../store/playerStore';
+import { useAuthStore } from '../../store/authStore';
import type { Track } from '../../store/playerStoreTypes';
import type { DurationMode } from '../../utils/componentHelpers/queuePanelHelpers';
import { formatLongDuration } from '../../utils/format/formatDuration';
@@ -24,6 +25,7 @@ export function QueueHeader({
}: Props) {
const currentTime = usePlayerStore((s) => Math.floor(s.currentTime / 30) * 30);
const isPlaying = usePlayerStore((s) => s.isPlaying);
+ const clockFormat = useAuthStore((s) => s.clockFormat);
const totalSecs = useMemo(() =>
queue.reduce((acc: number, track: Track) => acc + (track.duration || 0), 0),
@@ -40,7 +42,7 @@ export function QueueHeader({
if (queue.length > 0) {
if (durationMode === 'total') dur = formatLongDuration(Math.floor(totalSecs));
else if (durationMode === 'remaining') dur = `-${formatLongDuration(Math.floor(remainingSecs))}`;
- else dur = formatClockTime(Date.now() + remainingSecs * 1000);
+ else dur = formatClockTime(Date.now() + remainingSecs * 1000, clockFormat);
}
const nextMode: DurationMode =
diff --git a/src/components/settings/SystemTab.tsx b/src/components/settings/SystemTab.tsx
index fbac5c07..1dd3f6bb 100644
--- a/src/components/settings/SystemTab.tsx
+++ b/src/components/settings/SystemTab.tsx
@@ -7,7 +7,7 @@ import { AppWindow, ChevronDown, Download, ExternalLink, Globe, HardDrive, Info,
import { version as appVersion } from '../../../package.json';
import i18n from '../../i18n';
import { useAuthStore } from '../../store/authStore';
-import type { LoggingMode } from '../../store/authStoreTypes';
+import type { ClockFormat, LoggingMode } from '../../store/authStoreTypes';
import { IS_LINUX } from '../../utils/platform';
import { showToast } from '../../utils/ui/toast';
import { AboutPsysonicBrandHeader } from '../AboutPsysonicLol';
@@ -112,6 +112,24 @@ export function SystemTab() {
>
)}
+
+
+
+
{t('settings.clockFormat')}
+
{t('settings.clockFormatDesc')}
+
+
+ auth.setClockFormat(v as ClockFormat)}
+ options={[
+ { value: 'auto', label: t('settings.clockFormatAuto') },
+ { value: '24h', label: t('settings.clockFormatTwentyFour') },
+ { value: '12h', label: t('settings.clockFormatTwelve') },
+ ]}
+ />
+
+
diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts
index 54ee1bfc..2a8b86d2 100644
--- a/src/config/settingsCredits.ts
+++ b/src/config/settingsCredits.ts
@@ -297,6 +297,7 @@ const CONTRIBUTOR_ENTRIES = [
'Song Info: absolute file path on Navidrome via native /api/song/{id} — Subsonic only ever returned a relative path (or none on Navidrome), the native endpoint surfaces the full server-side location (PR #504)',
'Home: Lossless Albums rail + dedicated /lossless-albums page with infinite scroll and header parity (selection mode, enqueue, offline, download ZIPs), streaming load via per-fetch onProgress, sidebar entry default visible, detection via Navidrome native bit_depth-sorted song cursor with always-lossless suffix allowlist (PR #506)',
'Accessibility: OpenDyslexic font option in the Settings picker — bundled locally via @fontsource/opendyslexic, asymmetric glyph shapes for easier b/d, p/q tracking, Latin-only with translated subtitle in all 9 locales calling out the dyslexia-friendly intent and the Cyrillic/CJK fallback (PR #507)',
+ 'Settings: tri-state Clock Format (Auto / 24h / 12h) overriding the locale default for the queue ETA and the sleep-timer preview (PR #742)',
],
},
] as const;
diff --git a/src/locales/de/settings.ts b/src/locales/de/settings.ts
index d6661531..26813781 100644
--- a/src/locales/de/settings.ts
+++ b/src/locales/de/settings.ts
@@ -170,6 +170,11 @@ export const settings = {
showTrayIconDesc: 'Psysonic-Icon im System-Tray / in der Menüleiste anzeigen.',
minimizeToTray: 'Im Tray minimieren',
minimizeToTrayDesc: 'Beim Schließen des Fensters läuft Psysonic weiter im System-Tray statt zu beenden.',
+ clockFormat: 'Uhrzeitformat',
+ clockFormatDesc: 'Format für die Queue-ETA und die Vorschau des Sleep-Timers.',
+ clockFormatAuto: 'Automatisch (Systemsprache)',
+ clockFormatTwentyFour: '24-Stunden',
+ clockFormatTwelve: '12-Stunden (AM/PM)',
preloadMiniPlayer: 'Mini-Player vorladen',
preloadMiniPlayerDesc: 'Baut das Mini-Player-Fenster beim App-Start im Hintergrund auf, damit es beim ersten Öffnen sofort Inhalt zeigt. Kostet etwas mehr RAM.',
discordRichPresence: 'Discord Rich Presence',
diff --git a/src/locales/en/settings.ts b/src/locales/en/settings.ts
index 22ae18f7..61b3ecb3 100644
--- a/src/locales/en/settings.ts
+++ b/src/locales/en/settings.ts
@@ -173,6 +173,11 @@ export const settings = {
showTrayIconDesc: 'Display the Psysonic icon in the system notification area / menu bar.',
minimizeToTray: 'Minimize to Tray',
minimizeToTrayDesc: 'When closing the window, keep Psysonic running in the system tray instead of quitting.',
+ clockFormat: 'Clock Format',
+ clockFormatDesc: 'Wall-clock display used by the queue ETA and the sleep-timer preview.',
+ clockFormatAuto: 'Auto (system locale)',
+ clockFormatTwentyFour: '24-hour',
+ clockFormatTwelve: '12-hour (AM/PM)',
preloadMiniPlayer: 'Preload mini player',
preloadMiniPlayerDesc: 'Build the mini player window in the background at app start so it shows content instantly on first open. Uses a little extra memory.',
discordRichPresence: 'Discord Rich Presence',
diff --git a/src/locales/es/settings.ts b/src/locales/es/settings.ts
index f40138f1..badf8550 100644
--- a/src/locales/es/settings.ts
+++ b/src/locales/es/settings.ts
@@ -170,6 +170,11 @@ export const settings = {
showTrayIconDesc: 'Muestra el icono de Psysonic en el área de notificación / barra de menú.',
minimizeToTray: 'Minimizar a Bandeja',
minimizeToTrayDesc: 'Al cerrar la ventana, mantener Psysonic ejecutándose en la bandeja del sistema en lugar de salir.',
+ clockFormat: 'Formato de hora',
+ clockFormatDesc: 'Formato del reloj utilizado por la ETA de la cola y la vista previa del temporizador de suspensión.',
+ clockFormatAuto: 'Automático (idioma del sistema)',
+ clockFormatTwentyFour: '24 horas',
+ clockFormatTwelve: '12 horas (AM/PM)',
preloadMiniPlayer: 'Precargar mini reproductor',
preloadMiniPlayerDesc: 'Crea la ventana del mini reproductor en segundo plano al iniciar la aplicación para que muestre contenido al instante la primera vez que se abre. Consume un poco más de memoria.',
discordRichPresence: 'Discord Rich Presence',
diff --git a/src/locales/fr/settings.ts b/src/locales/fr/settings.ts
index 8347f9fc..902cf7a8 100644
--- a/src/locales/fr/settings.ts
+++ b/src/locales/fr/settings.ts
@@ -170,6 +170,11 @@ export const settings = {
showTrayIconDesc: 'Affiche l\'icône Psysonic dans la zone de notification / barre des menus.',
minimizeToTray: 'Réduire dans la barre système',
minimizeToTrayDesc: 'Lors de la fermeture, Psysonic continue de fonctionner dans la barre système au lieu de se fermer.',
+ clockFormat: 'Format de l\'heure',
+ clockFormatDesc: 'Format de l\'horloge utilisé par l\'ETA de la file d\'attente et l\'aperçu du minuteur de mise en veille.',
+ clockFormatAuto: 'Automatique (langue du système)',
+ clockFormatTwentyFour: '24 heures',
+ clockFormatTwelve: '12 heures (AM/PM)',
preloadMiniPlayer: 'Précharger le mini-lecteur',
preloadMiniPlayerDesc: 'Construit la fenêtre du mini-lecteur en arrière-plan au démarrage de l\'application afin qu\'elle affiche son contenu instantanément à la première ouverture. Utilise un peu plus de mémoire.',
linuxWebkitSmoothScroll: 'Molette fluide (Linux)',
diff --git a/src/locales/nb/settings.ts b/src/locales/nb/settings.ts
index 7647fc44..f719bcc9 100644
--- a/src/locales/nb/settings.ts
+++ b/src/locales/nb/settings.ts
@@ -169,6 +169,11 @@ export const settings = {
showOrbitTriggerDesc: 'Knappen i toppen for å starte eller bli med i en delt lytteøkt. Skjul den hvis du ikke bruker Orbit — du kan slå den på igjen her.',
minimizeToTray: 'Minimer til oppgavelinjen',
minimizeToTrayDesc: 'Når vinduet lukkes, vil Psysonic bli kjørende i oppgavelinjen fremfor å bli avsluttet.',
+ clockFormat: 'Klokkeformat',
+ clockFormatDesc: 'Klokkevisning brukt av kø-ETA og forhåndsvisningen av hviletimeren.',
+ clockFormatAuto: 'Automatisk (systemspråk)',
+ clockFormatTwentyFour: '24-timers',
+ clockFormatTwelve: '12-timers (AM/PM)',
preloadMiniPlayer: 'Forhåndslast miniavspiller',
preloadMiniPlayerDesc: 'Bygger miniavspiller-vinduet i bakgrunnen ved appstart slik at det viser innhold umiddelbart ved første åpning. Bruker litt mer minne.',
linuxWebkitSmoothScroll: 'Mykt musehjul (Linux)',
diff --git a/src/locales/nl/settings.ts b/src/locales/nl/settings.ts
index 67b7b915..de96e094 100644
--- a/src/locales/nl/settings.ts
+++ b/src/locales/nl/settings.ts
@@ -170,6 +170,11 @@ export const settings = {
showTrayIconDesc: 'Toont het Psysonic-pictogram in het systeemvak / de menubalk.',
minimizeToTray: 'Minimaliseren naar systeemvak',
minimizeToTrayDesc: 'Bij het sluiten van het venster blijft Psysonic actief in het systeemvak in plaats van af te sluiten.',
+ clockFormat: 'Tijdformaat',
+ clockFormatDesc: 'Tijdsweergave voor de wachtrij-ETA en het voorbeeld van de slaaptimer.',
+ clockFormatAuto: 'Automatisch (systeemtaal)',
+ clockFormatTwentyFour: '24-uurs',
+ clockFormatTwelve: '12-uurs (AM/PM)',
preloadMiniPlayer: 'Mini-speler vooraf laden',
preloadMiniPlayerDesc: 'Bouwt het venster van de mini-speler op de achtergrond bij het opstarten van de app, zodat het bij de eerste opening direct inhoud toont. Gebruikt iets meer geheugen.',
linuxWebkitSmoothScroll: 'Vloeiend muiswiel (Linux)',
diff --git a/src/locales/ro/settings.ts b/src/locales/ro/settings.ts
index 7e69bdf7..eb0ca4d4 100644
--- a/src/locales/ro/settings.ts
+++ b/src/locales/ro/settings.ts
@@ -173,6 +173,11 @@ export const settings = {
showTrayIconDesc: 'Afișează iconița Psysonic în zona notificărilor de sistem / bara de meniu.',
minimizeToTray: 'Minimizează în Tavă',
minimizeToTrayDesc: 'La închiderea ferestrei, continuă rularea Psysonic în tava de sistem în loc de ieșire',
+ clockFormat: 'Format Oră',
+ clockFormatDesc: 'Format al ceasului folosit pentru ETA-ul cozii și previzualizarea cronometrului de somn.',
+ clockFormatAuto: 'Automat (limba sistemului)',
+ clockFormatTwentyFour: '24 de ore',
+ clockFormatTwelve: '12 ore (AM/PM)',
preloadMiniPlayer: 'Preîncarcă mini player',
preloadMiniPlayerDesc: 'Crează fereastra mini player în fundal la deschiderea aplicației pentru a arăta conținut instantaneu la prima deschidere. Folosește puțină extra memorie.',
discordRichPresence: 'Prezență Discord Rich',
diff --git a/src/locales/ru/settings.ts b/src/locales/ru/settings.ts
index 24af8c3f..87de6afd 100644
--- a/src/locales/ru/settings.ts
+++ b/src/locales/ru/settings.ts
@@ -176,6 +176,11 @@ export const settings = {
showTrayIconDesc: 'Показывать Psysonic в области уведомлений / строке меню.',
minimizeToTray: 'Сворачивать в трей',
minimizeToTrayDesc: 'При закрытии окна не выходить из приложения, а оставаться в трее.',
+ clockFormat: 'Формат времени',
+ clockFormatDesc: 'Формат времени для ETA очереди и предпросмотра таймера сна.',
+ clockFormatAuto: 'Авто (системный)',
+ clockFormatTwentyFour: '24-часовой',
+ clockFormatTwelve: '12-часовой (AM/PM)',
preloadMiniPlayer: 'Предзагрузка мини-плеера',
preloadMiniPlayerDesc: 'Создаёт окно мини-плеера в фоне при запуске приложения, чтобы при первом открытии содержимое отображалось мгновенно. Использует немного больше памяти.',
useCustomTitlebar: 'Своя строка заголовка',
diff --git a/src/locales/zh/settings.ts b/src/locales/zh/settings.ts
index e91c9ed8..ce3d18ae 100644
--- a/src/locales/zh/settings.ts
+++ b/src/locales/zh/settings.ts
@@ -170,6 +170,11 @@ export const settings = {
showTrayIconDesc: '在系统通知区域 / 菜单栏显示 Psysonic 图标。',
minimizeToTray: '最小化到托盘',
minimizeToTrayDesc: '关闭窗口时,Psysonic 将继续在系统托盘中运行,而不是退出。',
+ clockFormat: '时间格式',
+ clockFormatDesc: '队列预计结束时间和睡眠定时器预览使用的时间格式。',
+ clockFormatAuto: '自动(跟随系统)',
+ clockFormatTwentyFour: '24 小时制',
+ clockFormatTwelve: '12 小时制(AM/PM)',
preloadMiniPlayer: '预加载迷你播放器',
preloadMiniPlayerDesc: '在应用启动时于后台构建迷你播放器窗口,使其首次打开即可立即显示内容。会占用少量额外内存。',
linuxWebkitSmoothScroll: '滚轮平滑(Linux)',
diff --git a/src/store/authStore.settings.test.ts b/src/store/authStore.settings.test.ts
index a542b3c0..b968731d 100644
--- a/src/store/authStore.settings.test.ts
+++ b/src/store/authStore.settings.test.ts
@@ -54,6 +54,7 @@ describe('trivial pass-through setters', () => {
['setShowArtistImages', 'showArtistImages', true],
['setShowTrayIcon', 'showTrayIcon', false],
['setMinimizeToTray', 'minimizeToTray', true],
+ ['setClockFormat', 'clockFormat', '24h'],
['setShowOrbitTrigger', 'showOrbitTrigger', false],
['setDiscordRichPresence', 'discordRichPresence', true],
['setEnableBandsintown', 'enableBandsintown', true],
diff --git a/src/store/authStore.ts b/src/store/authStore.ts
index 67f2977d..bf0dbc50 100644
--- a/src/store/authStore.ts
+++ b/src/store/authStore.ts
@@ -62,6 +62,7 @@ export const useAuthStore = create()(
libraryGridMaxColumns: DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
showTrayIcon: true,
minimizeToTray: false,
+ clockFormat: 'auto',
showOrbitTrigger: true,
discordRichPresence: false,
discordCoverSource: 'server',
diff --git a/src/store/authStoreTypes.ts b/src/store/authStoreTypes.ts
index ecf9aca0..0863b5ae 100644
--- a/src/store/authStoreTypes.ts
+++ b/src/store/authStoreTypes.ts
@@ -16,6 +16,11 @@ export type SeekbarStyle = 'truewave' | 'pseudowave' | 'linedot' | 'bar' | 'thic
/** Queue header duration chip: total duration / time left / ETA finish clock. */
export type DurationMode = 'total' | 'remaining' | 'eta';
export type LoggingMode = 'off' | 'normal' | 'debug';
+/**
+ * Wall-clock format for ETA / sleep-timer labels. `'auto'` follows the user's
+ * system locale (existing behaviour); explicit `'24h'` / `'12h'` overrides it.
+ */
+export type ClockFormat = 'auto' | '24h' | '12h';
export type NormalizationEngine = 'off' | 'replaygain' | 'loudness';
export type DiscordCoverSource = 'none' | 'apple' | 'server';
@@ -89,6 +94,7 @@ export interface AuthState {
libraryGridMaxColumns: number;
showTrayIcon: boolean;
minimizeToTray: boolean;
+ clockFormat: ClockFormat;
/** Whether the "Orbit" topbar trigger is rendered. Users who never
* touch Orbit can hide it so the header stays uncluttered. */
showOrbitTrigger: boolean;
@@ -266,6 +272,7 @@ export interface AuthState {
setLibraryGridMaxColumns: (v: number) => void;
setShowTrayIcon: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
+ setClockFormat: (v: ClockFormat) => void;
setShowOrbitTrigger: (v: boolean) => void;
setDiscordRichPresence: (v: boolean) => void;
setDiscordCoverSource: (v: DiscordCoverSource) => void;
diff --git a/src/store/authUiAppearanceActions.ts b/src/store/authUiAppearanceActions.ts
index dadf0b29..96408a73 100644
--- a/src/store/authUiAppearanceActions.ts
+++ b/src/store/authUiAppearanceActions.ts
@@ -16,6 +16,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
| 'setLibraryGridMaxColumns'
| 'setShowTrayIcon'
| 'setMinimizeToTray'
+ | 'setClockFormat'
| 'setShowOrbitTrigger'
| 'setUseCustomTitlebar'
| 'setPreloadMiniPlayer'
@@ -37,6 +38,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
setLibraryGridMaxColumns: (v) => set({ libraryGridMaxColumns: clampLibraryGridMaxColumns(v) }),
setShowTrayIcon: (v) => set({ showTrayIcon: v }),
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
+ setClockFormat: (v) => set({ clockFormat: v }),
setShowOrbitTrigger: (v) => set({ showOrbitTrigger: v }),
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }),
setPreloadMiniPlayer: (v) => set({ preloadMiniPlayer: v }),
diff --git a/src/utils/format/formatClockTime.test.ts b/src/utils/format/formatClockTime.test.ts
new file mode 100644
index 00000000..baa41167
--- /dev/null
+++ b/src/utils/format/formatClockTime.test.ts
@@ -0,0 +1,32 @@
+/**
+ * `formatClockTime` is a thin wrapper around `Date.toLocaleTimeString` whose
+ * only knob is `hour12`, mapped from the user's `ClockFormat` setting. The
+ * exact `HH:MM` output is locale-dependent and not asserted here — these
+ * tests pin only the `hour12` mapping, which is what the setting controls.
+ */
+import { describe, expect, it } from 'vitest';
+import { formatClockTime } from './formatClockTime';
+
+const SAMPLE_TS = Date.UTC(2026, 0, 1, 19, 17, 0); // 19:17 UTC, deterministic
+
+describe('formatClockTime — clockFormat mapping', () => {
+ it('forces 24-hour output when clockFormat === "24h" (no AM/PM marker)', () => {
+ const out = formatClockTime(SAMPLE_TS, '24h');
+ expect(out).not.toMatch(/AM|PM/i);
+ });
+
+ it('forces 12-hour output when clockFormat === "12h" (renders an AM/PM marker)', () => {
+ const out = formatClockTime(SAMPLE_TS, '12h');
+ expect(out).toMatch(/AM|PM/i);
+ });
+
+ it('falls through to the locale default when clockFormat === "auto"', () => {
+ // We do not assert AM/PM either way here — `'auto'` deliberately defers to
+ // the JS engine's locale. The contract is just "do not force `hour12`".
+ expect(() => formatClockTime(SAMPLE_TS, 'auto')).not.toThrow();
+ });
+
+ it('falls through to the locale default when clockFormat is omitted', () => {
+ expect(() => formatClockTime(SAMPLE_TS)).not.toThrow();
+ });
+});
diff --git a/src/utils/format/formatClockTime.ts b/src/utils/format/formatClockTime.ts
index 2557a0b0..5d668874 100644
--- a/src/utils/format/formatClockTime.ts
+++ b/src/utils/format/formatClockTime.ts
@@ -1,4 +1,15 @@
-/** Localized wall-clock `HH:MM` for a timestamp (sleep-timer / queue-ETA labels). */
-export function formatClockTime(timestampMs: number): string {
- return new Date(timestampMs).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
+import type { ClockFormat } from '../../store/authStoreTypes';
+
+/**
+ * Localized wall-clock `HH:MM` for a timestamp (sleep-timer / queue-ETA labels).
+ * `clockFormat` overrides the system locale's `hour12` default — pass `'auto'`
+ * or omit to keep locale-driven behaviour.
+ */
+export function formatClockTime(timestampMs: number, clockFormat?: ClockFormat): string {
+ const hour12 = clockFormat === '24h' ? false : clockFormat === '12h' ? true : undefined;
+ return new Date(timestampMs).toLocaleTimeString(undefined, {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12,
+ });
}