diff --git a/src/components/Routines/screens/Detail/RoutineDetail.tsx b/src/components/Routines/screens/Detail/RoutineDetail.tsx
index 9f97e004c..88c2fc826 100644
--- a/src/components/Routines/screens/Detail/RoutineDetail.tsx
+++ b/src/components/Routines/screens/Detail/RoutineDetail.tsx
@@ -10,11 +10,11 @@ import i18n from "@/i18n";
import React from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
-import { dateToLocale } from "@/core/lib/date";
+import { formatNaiveDate } from "@/core/lib/date";
import { makeLink, WgerLink } from "@/core/lib/url";
export const RoutineDetail = () => {
- const { t } = useTranslation();
+ const { t, i18n } = useTranslation();
const params = useParams<{ routineId: string }>();
const routineId = parseInt(params.routineId ?? '');
if (Number.isNaN(routineId)) {
@@ -25,7 +25,7 @@ export const RoutineDetail = () => {
const routineQuery = useRoutineDetailQuery(routineId);
const routine = routineQuery.data;
- const subtitle = routine !== undefined ? `${dateToLocale(routine!.start)} - ${dateToLocale(routine!.end)} (${routine?.durationText})` : '';
+ const subtitle = routine !== undefined ? `${formatNaiveDate(routine!.start, i18n.language)} - ${formatNaiveDate(routine!.end, i18n.language)} (${routine?.durationText})` : '';
const chip = routine?.isTemplate
?
: null;
diff --git a/src/components/Routines/screens/Detail/TemplateDetail.tsx b/src/components/Routines/screens/Detail/TemplateDetail.tsx
index 43dfee932..d506557a4 100644
--- a/src/components/Routines/screens/Detail/TemplateDetail.tsx
+++ b/src/components/Routines/screens/Detail/TemplateDetail.tsx
@@ -8,7 +8,7 @@ import { DayDetailsCard } from "@/components/Routines/widgets/RoutineDetailsCard
import React from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
-import { dateToLocale } from "@/core/lib/date";
+import { formatNaiveDate } from "@/core/lib/date";
import { makeLink, WgerLink } from "@/core/lib/url";
export const TemplateDetail = () => {
@@ -34,7 +34,7 @@ export const TemplateDetail = () => {
mainContent={
- {dateToLocale(routine!.start)} - {dateToLocale(routine!.end)} ({routine!.durationText})
+ {formatNaiveDate(routine!.start, i18n.language)} - {formatNaiveDate(routine!.end, i18n.language)} ({routine!.durationText})
{routine!.description !== ''
diff --git a/src/components/Routines/screens/Overview/RoutineOverview.tsx b/src/components/Routines/screens/Overview/RoutineOverview.tsx
index e74e6e196..7b06e8a06 100644
--- a/src/components/Routines/screens/Overview/RoutineOverview.tsx
+++ b/src/components/Routines/screens/Overview/RoutineOverview.tsx
@@ -8,7 +8,7 @@ import { AddRoutineFab } from "@/components/Routines/screens/Overview/Fab";
import { useRoutinesShallowQuery } from "@/components/Routines/queries";
import React from "react";
import { useTranslation } from "react-i18next";
-import { dateToLocale } from "@/core/lib/date";
+import { formatNaiveDate } from "@/core/lib/date";
import { makeLink, WgerLink } from "@/core/lib/url";
export const RoutineList = (props: {
@@ -42,7 +42,7 @@ export const RoutineList = (props: {
{primaryText} {chipTemplate} {chipVisibility}>}
- secondary={`${props.routine.durationText} (${dateToLocale(props.routine.start)} - ${dateToLocale(props.routine.end)})`}
+ secondary={`${props.routine.durationText} (${formatNaiveDate(props.routine.start, i18n.language)} - ${formatNaiveDate(props.routine.end, i18n.language)})`}
/>
diff --git a/src/core/lib/date.test.ts b/src/core/lib/date.test.ts
index 9524f1305..37c581fc4 100644
--- a/src/core/lib/date.test.ts
+++ b/src/core/lib/date.test.ts
@@ -1,4 +1,4 @@
-import { calculatePastDate, dateTimeToHHMM, dateToYYYYMMDD } from "@/core/lib/date";
+import { calculatePastDate, dateTimeToHHMM, dateToYYYYMMDD, formatNaiveDate } from "@/core/lib/date";
describe("test date utility", () => {
@@ -53,4 +53,32 @@ describe('calculatePastDate', () => {
const result = calculatePastDate('lastYear', new Date('2023-02-14'));
expect(result).toStrictEqual('2022-02-14');
});
+});
+
+describe('formatNaiveDate - Timezone-Agnostic Validation', () => {
+ it('should format standard JS Date objects timezone-agnostically', () => {
+ // E.g. date parsed in UTC representing "2026-05-12"
+ const dateObject = new Date('2026-05-12T00:00:00Z');
+
+ const formattedUS = formatNaiveDate(dateObject, 'en-US');
+ expect(formattedUS).toBe('05/12/2026');
+
+ const formattedIT = formatNaiveDate(dateObject, 'it-IT');
+ expect(formattedIT).toBe('12/05/2026');
+ });
+
+ it('should correctly format month boundaries without falling back to previous/next month', () => {
+ // First day of month
+ expect(formatNaiveDate(new Date('2026-01-01T00:00:00Z'), 'en-US')).toBe('01/01/2026');
+ // Last day of month
+ expect(formatNaiveDate(new Date('2026-12-31T00:00:00Z'), 'en-US')).toBe('12/31/2026');
+ // Leap year date
+ expect(formatNaiveDate(new Date('2024-02-29T00:00:00Z'), 'en-US')).toBe('02/29/2024');
+ });
+
+ it('should return an empty string for invalid dates, null, or undefined', () => {
+ expect(formatNaiveDate(null)).toBe('');
+ expect(formatNaiveDate(undefined)).toBe('');
+ expect(formatNaiveDate(new Date('invalid-date'))).toBe('');
+ });
});
\ No newline at end of file
diff --git a/src/core/lib/date.ts b/src/core/lib/date.ts
index 21f5611db..4f3864819 100644
--- a/src/core/lib/date.ts
+++ b/src/core/lib/date.ts
@@ -82,6 +82,30 @@ export function dateToLocale(dateTime: Date | null, locale?: string, options?: I
return dateTime.toLocaleString(locale ? [locale] : [], options);
}
+/**
+ * Formats a standard JS Date object into a localized date format without applying
+ * local browser timezone translation (i.e. timezone-agnostic).
+ *
+ * @param date - The JS Date object representing a naive date
+ * @param locale - Optional locale for formatting (defaults to active i18n language or 'en-US')
+ * @returns The formatted date string, or an empty string if the input is invalid
+ */
+export function formatNaiveDate(date: Date | null | undefined, locale?: string): string {
+ if (!date || isNaN(date.getTime())) {
+ return '';
+ }
+
+ const resolvedLocale = locale ?? i18n.language ?? 'en-US';
+
+ // Format explicitly using the UTC timezone and consistent fields
+ return date.toLocaleDateString(resolvedLocale, {
+ timeZone: 'UTC',
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ });
+}
+
/*
* Converts a date object to a non localized string in the format HH:MM
*/