diff --git a/assets/js/components/GlobalSettings/UserInterfaceSettings.vue b/assets/js/components/GlobalSettings/UserInterfaceSettings.vue
index b7997c583a2..fc7e58051f3 100644
--- a/assets/js/components/GlobalSettings/UserInterfaceSettings.vue
+++ b/assets/js/components/GlobalSettings/UserInterfaceSettings.vue
@@ -59,6 +59,18 @@
equal-width
/>
+
+
+
@@ -91,7 +103,14 @@ import {
removeLocalePreference,
} from "@/i18n.ts";
import { getThemePreference, setThemePreference } from "@/theme.ts";
-import { getUnits, setUnits, is12hFormat, set12hFormat } from "@/units";
+import {
+ getUnits,
+ setUnits,
+ is12hFormat,
+ set12hFormat,
+ getDateFormat,
+ setDateFormat,
+} from "@/units";
import { isApp } from "@/utils/native";
import { defineComponent, type PropType } from "vue";
import { LENGTH_UNIT, THEME, type UiLoadpoint } from "@/types/evcc";
@@ -111,6 +130,7 @@ export default defineComponent({
language: getLocalePreference() || "",
unit: getUnits(),
timeFormat: is12hFormat() ? TIME_12H : TIME_24H,
+ dateFormat: getDateFormat(),
fullscreenActive: false,
THEMES: Object.values(THEME),
UNITS: Object.values(LENGTH_UNIT),
@@ -141,6 +161,9 @@ export default defineComponent({
timeFormat(value) {
set12hFormat(value === TIME_12H);
},
+ dateFormat(value) {
+ setDateFormat(value);
+ },
theme(value) {
setThemePreference(value);
},
diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts
index 9489dd4929a..fef8ad1d242 100644
--- a/assets/js/mixins/formatter.ts
+++ b/assets/js/mixins/formatter.ts
@@ -1,6 +1,44 @@
import { defineComponent } from "vue";
import { is12hFormat } from "@/units";
import { CURRENCY } from "../types/evcc";
+import settings from "@/settings";
+import type { DateFormat } from "@/settings";
+
+// Format the day+month portion of a date according to the user's date-order
+// preference. Month names are always rendered in the given UI locale so they
+// stay translated regardless of the ordering choice.
+// dmy → "17 May" (or "17 Mai" in German)
+// mdy → "May 17"
+// ymd → "2025-05-17"
+// "" → locale-native (existing behaviour, auto)
+function formatDayMonth(date: Date, locale: string | undefined, fmt: DateFormat): string {
+ const monthName = new Intl.DateTimeFormat(locale, { month: "short" }).format(date);
+ const day = date.getDate();
+ const year = date.getFullYear();
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
+ const dd = String(day).padStart(2, "0");
+ if (fmt === "mdy") return `${monthName} ${day}`;
+ if (fmt === "ymd") return `${year}-${mm}-${dd}`;
+ if (fmt === "dmy") return `${day} ${monthName}`;
+ // auto: use locale-native ordering
+ return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(date);
+}
+
+// Format the numeric date portion (no month names) with the right day/month
+// ordering. Used in the compact "short" date format.
+// dmy → "17/05" (via en-GB locale)
+// mdy → "5/17" (via en-US locale)
+// ymd → "2025-05-17" (explicit construction — Intl does not include year with only month+day)
+// "" → locale-native
+function formatNumericDate(date: Date, locale: string | undefined, fmt: DateFormat): string {
+ if (fmt === "ymd") {
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
+ const dd = String(date.getDate()).padStart(2, "0");
+ return `${date.getFullYear()}-${mm}-${dd}`;
+ }
+ const orderLocale = fmt === "mdy" ? "en-US" : fmt === "dmy" ? "en-GB" : locale;
+ return new Intl.DateTimeFormat(orderLocale, { month: "numeric", day: "numeric" }).format(date);
+}
const CURRENCY_SYMBOLS: Record = {
AUD: "$",
@@ -56,6 +94,11 @@ export default defineComponent({
fmtDigits: 1,
};
},
+ computed: {
+ dateFormat(): DateFormat {
+ return settings.dateFormat || "";
+ },
+ },
methods: {
energyPriceSubunit(currency: CURRENCY): string | undefined {
if (currency === CURRENCY.CHF) {
@@ -244,14 +287,29 @@ export default defineComponent({
}).format(date);
},
fmtFullDateTime(date: Date, short: boolean) {
- return new Intl.DateTimeFormat(this.$i18n?.locale, {
- weekday: short ? undefined : "short",
- month: short ? "numeric" : "short",
- day: "numeric",
+ const locale = this.$i18n?.locale;
+ const fmt = this.dateFormat;
+ if (!fmt) {
+ // auto: single Intl call preserves locale-native separators (e.g. German "So., 15. Jan.,")
+ return new Intl.DateTimeFormat(locale, {
+ weekday: short ? undefined : "short",
+ month: short ? "numeric" : "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ hour12: is12hFormat(),
+ }).format(date);
+ }
+ const time = new Intl.DateTimeFormat(locale, {
hour: "numeric",
minute: "numeric",
hour12: is12hFormat(),
}).format(date);
+ if (short) {
+ return `${formatNumericDate(date, locale, fmt)} ${time}`.trim();
+ }
+ const weekday = new Intl.DateTimeFormat(locale, { weekday: "short" }).format(date);
+ return `${weekday} ${formatDayMonth(date, locale, fmt)} ${time}`.trim();
},
fmtWeekdayTime(date: Date) {
return new Intl.DateTimeFormat(this.$i18n?.locale, {
@@ -273,11 +331,17 @@ export default defineComponent({
}).format(date);
},
fmtDayMonth(date: Date) {
- return new Intl.DateTimeFormat(this.$i18n?.locale, {
- weekday: "short",
- day: "numeric",
- month: "short",
- }).format(date);
+ const locale = this.$i18n?.locale;
+ const fmt = this.dateFormat;
+ if (!fmt) {
+ return new Intl.DateTimeFormat(locale, {
+ weekday: "short",
+ day: "numeric",
+ month: "short",
+ }).format(date);
+ }
+ const weekday = new Intl.DateTimeFormat(locale, { weekday: "short" }).format(date);
+ return `${weekday} ${formatDayMonth(date, locale, fmt)}`.trim();
},
fmtDurationUnit(value: number, unit = "second") {
return new Intl.NumberFormat(this.$i18n?.locale, {
diff --git a/assets/js/settings.ts b/assets/js/settings.ts
index 1657c0bc176..f42689a2559 100644
--- a/assets/js/settings.ts
+++ b/assets/js/settings.ts
@@ -23,6 +23,7 @@ const SETTINGS_SOLAR_ADJUSTED = "settings_solar_adjusted";
const SETTINGS_PRICE_ZOOM = "settings_price_zoom";
const SETTINGS_HIDE_FEEDIN = "settings_hide_feedin";
const LAST_BATTERY_SMART_COST_LIMIT = "last_battery_smart_cost_limit";
+const SETTINGS_DATE_FORMAT = "settings_date_format";
const LAST_TARGET_TIME = "last_target_time";
const LAST_SOC_GOAL = "last_soc_goal";
const LAST_ENERGY_GOAL = "last_energy_goal";
@@ -98,6 +99,8 @@ function saveJSON(key: string) {
};
}
+export type DateFormat = "" | "dmy" | "mdy" | "ymd";
+
export interface LoadpointSettings {
order?: number;
visible?: boolean;
@@ -111,6 +114,7 @@ export interface Settings {
theme: THEME | null;
unit: string;
is12hFormat: boolean;
+ dateFormat: DateFormat; // "" = auto, "dmy" = DD/MM, "mdy" = MM/DD, "ymd" = YYYY-MM-DD
energyflowDetails: boolean;
energyflowCo2: boolean;
energyflowPv: boolean;
@@ -139,6 +143,7 @@ const settings: Settings = reactive({
theme: read(SETTINGS_THEME),
unit: read(SETTINGS_UNIT),
is12hFormat: readBool(SETTINGS_12H_FORMAT),
+ dateFormat: read(SETTINGS_DATE_FORMAT) || "",
energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS),
energyflowCo2: readBool(SETTINGS_ENERGYFLOW_CO2),
energyflowPv: readBool(SETTINGS_ENERGYFLOW_PV),
@@ -166,6 +171,7 @@ watch(() => settings.locale, save(SETTINGS_LOCALE));
watch(() => settings.theme, save(SETTINGS_THEME));
watch(() => settings.unit, save(SETTINGS_UNIT));
watch(() => settings.is12hFormat, saveBool(SETTINGS_12H_FORMAT));
+watch(() => settings.dateFormat, save(SETTINGS_DATE_FORMAT));
watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
watch(() => settings.energyflowCo2, saveBool(SETTINGS_ENERGYFLOW_CO2));
watch(() => settings.energyflowPv, saveBool(SETTINGS_ENERGYFLOW_PV));
diff --git a/assets/js/units.ts b/assets/js/units.ts
index cf50379be06..ccd07c3a2de 100644
--- a/assets/js/units.ts
+++ b/assets/js/units.ts
@@ -1,4 +1,5 @@
import settings from "./settings";
+import type { DateFormat } from "./settings";
import { LENGTH_UNIT } from "./types/evcc";
const MILES_FACTOR = 0.6213711922;
@@ -30,3 +31,11 @@ export function is12hFormat() {
export function set12hFormat(value: boolean) {
settings.is12hFormat = value;
}
+
+export function getDateFormat(): DateFormat {
+ return settings.dateFormat || "";
+}
+
+export function setDateFormat(value: DateFormat) {
+ settings.dateFormat = value;
+}
diff --git a/i18n/en.json b/i18n/en.json
index 8d852e51cc2..933842f9cb4 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1465,6 +1465,13 @@
"vehicle": "Vehicle"
},
"settings": {
+ "dateFormat": {
+ "auto": "Auto",
+ "dmy": "DD/MM/YYYY",
+ "label": "Date format",
+ "mdy": "MM/DD/YYYY",
+ "ymd": "YYYY-MM-DD"
+ },
"deviceInfo": "Settings you make in this dialog only affect this device.",
"fullscreen": {
"enter": "Enter fullscreen",