Merge v1.25.3: honor the 24h time-format preference app-wide

This commit is contained in:
Marc-André Bombeck committed 2026-06-29 15:49:52 +02:00
commit d0eced41f9
9 files changed
+131 -26

No files matched your search

+5 -2
View File
@@ -11,7 +11,8 @@ import {
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useTranslations } from "@/lib/i18n/context";
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
import { hourCycleOptions } from "@/lib/format-locale";
import { STAGE_COLORS } from "./sleep-stage-stacked-bar";
/**
@@ -112,6 +113,7 @@ function formatMinutes(total: number, locale: string): string {
export function SleepHypnogram({ session }: SleepHypnogramProps) {
const { t, locale } = useTranslations();
const timeFormat = useTimeFormatPreference();
const stageLabels: Record<string, string> = {
DEEP: t("insights.sleep.stages.deep"),
@@ -192,8 +194,9 @@ export function SleepHypnogram({ session }: SleepHypnogramProps) {
new Intl.DateTimeFormat(locale === "de" ? "de-DE" : "en-US", {
hour: "2-digit",
minute: "2-digit",
...hourCycleOptions(timeFormat),
}),
[locale],
[locale, timeFormat],
);
// Per-stage breakdown with percentage of the night's total tracked time.
+14 -2
View File
@@ -17,7 +17,11 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
import {
hourCycleOptions,
type TimeFormatPreference,
} from "@/lib/format-locale";
import type { WorkoutDetailPayload } from "@/hooks/use-workouts";
/**
@@ -103,6 +107,7 @@ function formatDateRange(
startedAt: string,
endedAt: string,
locale: string,
timeFormat: TimeFormatPreference,
): string {
const start = new Date(startedAt);
const end = new Date(endedAt);
@@ -115,6 +120,7 @@ function formatDateRange(
const timeFmt = new Intl.DateTimeFormat(locale, {
hour: "2-digit",
minute: "2-digit",
...hourCycleOptions(timeFormat),
});
return `${dateFmt.format(start)} · ${timeFmt.format(start)} – ${timeFmt.format(end)}`;
}
@@ -139,6 +145,7 @@ function renderSportIconBadge(sportType: string) {
export function WorkoutDetailHeader({ workout }: WorkoutDetailHeaderProps) {
const { t, locale } = useTranslations();
const timeFormat = useTimeFormatPreference();
const sportLabelKey = `insights.workouts.sport.${workout.sportType}`;
const sportLabel = t(sportLabelKey);
const sportName =
@@ -158,7 +165,12 @@ export function WorkoutDetailHeader({ workout }: WorkoutDetailHeaderProps) {
{sportName}
</h2>
<p className="text-muted-foreground text-sm">
{formatDateRange(workout.startedAt, workout.endedAt, locale)}
{formatDateRange(
workout.startedAt,
workout.endedAt,
locale,
timeFormat,
)}
</p>
</div>
<div className="flex flex-col items-end text-right">
+13 -3
View File
@@ -13,7 +13,11 @@ import {
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
import {
hourCycleOptions,
type TimeFormatPreference,
} from "@/lib/format-locale";
import type { WorkoutListEntry } from "@/hooks/use-workouts";
/**
@@ -76,7 +80,11 @@ function formatEnergy(kcal: number, locale: string): string {
}).format(Math.round(kcal));
}
function formatDate(iso: string, locale: string): string {
function formatDate(
iso: string,
locale: string,
timeFormat: TimeFormatPreference,
): string {
const d = new Date(iso);
return new Intl.DateTimeFormat(locale, {
weekday: "short",
@@ -84,6 +92,7 @@ function formatDate(iso: string, locale: string): string {
month: "short",
hour: "2-digit",
minute: "2-digit",
...hourCycleOptions(timeFormat),
}).format(d);
}
@@ -94,6 +103,7 @@ export interface WorkoutListProps {
export function WorkoutList({ workouts, className }: WorkoutListProps) {
const { t, locale } = useTranslations();
const timeFormat = useTimeFormatPreference();
return (
<ul
@@ -129,7 +139,7 @@ export function WorkoutList({ workouts, className }: WorkoutListProps) {
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate font-medium">{sportName}</span>
<span className="text-muted-foreground truncate text-xs">
{formatDate(workout.startedAt, locale)}
{formatDate(workout.startedAt, locale, timeFormat)}
</span>
</div>
<div className="flex flex-col items-end gap-0.5 text-xs">
@@ -13,7 +13,7 @@ import { useQuery } from "@tanstack/react-query";
import { ChevronDown, Loader2, ShieldCheck } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { useTranslations } from "@/lib/i18n/context";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
import { cn } from "@/lib/utils";
@@ -54,6 +54,7 @@ export function SecurityActivityCard({
isAuthenticated: boolean;
}) {
const { t } = useTranslations();
const fmt = useFormatters();
// Collapsed by default — the feed opens only on demand, keeping the security
// surface skimmable. UI-only state; nothing is persisted across reloads.
const [open, setOpen] = useState(false);
@@ -138,7 +139,7 @@ export function SecurityActivityCard({
</p>
</div>
<p className="text-muted-foreground shrink-0 text-xs">
{new Date(e.createdAt).toLocaleString()}
{fmt.dateTime(e.createdAt)}
</p>
</li>
))}
@@ -15,7 +15,7 @@ import { ChevronDown, Loader2, MonitorSmartphone } from "lucide-react";
import { Button } from "@/components/ui/button";
import { SettingsCard } from "@/components/settings/settings-card";
import { useTranslations } from "@/lib/i18n/context";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { apiGet, apiDelete } from "@/lib/api/api-fetch";
import { cn } from "@/lib/utils";
@@ -36,6 +36,7 @@ export function SecuritySessionsCard({
isAuthenticated: boolean;
}) {
const { t } = useTranslations();
const fmt = useFormatters();
const queryClient = useQueryClient();
const [status, setStatus] = useState<string | null>(null);
// Collapsed by default — the list opens only when the user asks for it, so
@@ -158,7 +159,7 @@ export function SecuritySessionsCard({
{s.lastActiveAt && (
<p className="text-muted-foreground text-xs">
{t("settings.security.lastActive", {
time: new Date(s.lastActiveAt).toLocaleString(),
time: fmt.dateTime(s.lastActiveAt),
})}
</p>
)}
+1 -14
View File
@@ -10,6 +10,7 @@ import {
useTimeFormatPreference,
} from "@/lib/i18n/context";
import { formatDate, resolveDateLocale } from "@/lib/date-format";
import { hourCycleOptions } from "@/lib/format-locale";
/**
* Dependency-free datetime input that DISPLAYS the value in the user's
@@ -221,20 +222,6 @@ function formatLocalDateTime(
return `${datePart} ${timePart}`;
}
/** Mirror of `format-locale.ts` `hourCycleOptions`, kept local to the field. */
function hourCycleOptions(
timeFormat: ReturnType<typeof useTimeFormatPreference>,
): Intl.DateTimeFormatOptions {
switch (timeFormat) {
case "H12":
return { hour12: true };
case "H24":
return { hourCycle: "h23" };
default:
return {};
}
}
/** Split a local `yyyy-MM-ddTHH:mm` string into its calendar parts. */
function parseLocalDateTime(value: string): {
date: string;