Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion assets/js/components/GlobalSettings/UserInterfaceSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@
equal-width
/>
</FormRow>
<FormRow id="settingsDateFormat" :label="$t('settings.dateFormat.label')">
<select
id="settingsDateFormat"
v-model="dateFormat"
class="form-select form-select-sm w-75"
>
<option value="">{{ $t("settings.dateFormat.auto") }}</option>
<option value="dmy">{{ $t("settings.dateFormat.dmy") }}</option>
<option value="mdy">{{ $t("settings.dateFormat.mdy") }}</option>
<option value="ymd">{{ $t("settings.dateFormat.ymd") }}</option>
</select>
</FormRow>
<FormRow v-if="loadpoints.length" :label="$t('settings.loadpoints.label')">
<LoadpointOrderSettings :loadpoints="loadpoints" />
</FormRow>
Expand Down Expand Up @@ -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";
Expand All @@ -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),
Expand Down Expand Up @@ -141,6 +161,9 @@ export default defineComponent({
timeFormat(value) {
set12hFormat(value === TIME_12H);
},
dateFormat(value) {
setDateFormat(value);
},
theme(value) {
setThemePreference(value);
},
Expand Down
82 changes: 73 additions & 9 deletions assets/js/mixins/formatter.ts
Original file line number Diff line number Diff line change
@@ -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<CURRENCY, string> = {
AUD: "$",
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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, {
Expand All @@ -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, {
Expand Down
6 changes: 6 additions & 0 deletions assets/js/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -98,6 +99,8 @@ function saveJSON(key: string) {
};
}

export type DateFormat = "" | "dmy" | "mdy" | "ymd";

export interface LoadpointSettings {
order?: number;
visible?: boolean;
Expand All @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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));
Expand Down
9 changes: 9 additions & 0 deletions assets/js/units.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import settings from "./settings";
import type { DateFormat } from "./settings";
import { LENGTH_UNIT } from "./types/evcc";

const MILES_FACTOR = 0.6213711922;
Expand Down Expand Up @@ -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;
}
7 changes: 7 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading