From 9da6236e0306a79f273faf75f5df9c7586f73f7b Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 9 May 2025 15:55:45 +0200 Subject: [PATCH] UI: add 12h/24h setting (#21140) --- .../GlobalSettings/UserInterfaceSettings.vue | 25 ++++++++++++++++++- assets/js/components/Tariff/TariffChart.vue | 6 ++++- assets/js/mixins/formatter.test.js | 25 ++++++++++++++++++- assets/js/mixins/formatter.ts | 6 +++++ assets/js/settings.js | 3 +++ assets/js/units.js | 8 ++++++ i18n/de.json | 5 ++++ i18n/en.json | 5 ++++ 8 files changed, 80 insertions(+), 3 deletions(-) diff --git a/assets/js/components/GlobalSettings/UserInterfaceSettings.vue b/assets/js/components/GlobalSettings/UserInterfaceSettings.vue index df6746b23..2ccaaea23 100644 --- a/assets/js/components/GlobalSettings/UserInterfaceSettings.vue +++ b/assets/js/components/GlobalSettings/UserInterfaceSettings.vue @@ -42,6 +42,21 @@ equal-width /> + + + @@ -82,12 +97,15 @@ import FormRow from "../Helper/FormRow.vue"; import SelectGroup from "../Helper/SelectGroup.vue"; import { getLocalePreference, setLocalePreference, LOCALES, removeLocalePreference } from "@/i18n"; import { getThemePreference, setThemePreference, THEMES } from "@/theme"; -import { getUnits, setUnits, UNITS } from "@/units"; +import { getUnits, setUnits, UNITS, is12hFormat, set12hFormat } from "@/units"; import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags"; import { isApp } from "@/utils/native"; import { defineComponent, type PropType } from "vue"; import type { Sponsor } from "@/types/evcc"; +const TIME_12H = "12"; +const TIME_24H = "24"; + export default defineComponent({ name: "UserInterfaceSettings", components: { TelemetrySettings, FormRow, SelectGroup }, @@ -99,10 +117,12 @@ export default defineComponent({ theme: getThemePreference(), language: getLocalePreference() || "", unit: getUnits(), + timeFormat: is12hFormat() ? TIME_12H : TIME_24H, hiddenFeatures: getHiddenFeatures(), fullscreenActive: false, THEMES, UNITS, + TIME_FORMATS: [TIME_24H, TIME_12H], }; }, computed: { @@ -126,6 +146,9 @@ export default defineComponent({ unit(value) { setUnits(value); }, + timeFormat(value) { + set12hFormat(value === TIME_12H); + }, theme(value) { setThemePreference(value); }, diff --git a/assets/js/components/Tariff/TariffChart.vue b/assets/js/components/Tariff/TariffChart.vue index f272be97f..0e626351c 100644 --- a/assets/js/components/Tariff/TariffChart.vue +++ b/assets/js/components/Tariff/TariffChart.vue @@ -27,7 +27,7 @@
{{ - slot.startHour + formatHour(slot.startHour) }}
{{ slot.day }} @@ -55,6 +55,7 @@ diff --git a/assets/js/mixins/formatter.test.js b/assets/js/mixins/formatter.test.js index d33b6fb89..4c82163df 100644 --- a/assets/js/mixins/formatter.test.js +++ b/assets/js/mixins/formatter.test.js @@ -1,6 +1,9 @@ import { mount, config } from "@vue/test-utils"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import formatter, { POWER_UNIT } from "./formatter"; +import * as units from "../units"; + +const is12hSpy = vi.spyOn(units, "is12hFormat").mockReturnValue(false); config.global.mocks["$i18n"] = { locale: "de-DE" }; config.global.mocks["$t"] = (a) => a; @@ -183,3 +186,23 @@ describe("fmtPercentage", () => { expect(fmt.fmtPercentage(-100, 0, false)).eq("-100 %"); }); }); + +const testDate = new Date(2023, 0, 15, 15, 30); // Jan 15, 2023, 15:30 (3:30 PM) + +describe("12h/24h time format", () => { + test("12h format", () => { + is12hSpy.mockReturnValue(true); + expect(fmt.fmtHourMinute(testDate)).toBe("3:30 PM"); + expect(fmt.fmtFullDateTime(testDate)).toBe("So., 15. Jan., 3:30 PM"); + expect(fmt.fmtWeekdayTime(testDate)).toBe("So. 3:30 PM"); + expect(fmt.fmtAbsoluteDate(testDate)).toBe("So 3:30 PM"); + }); + + test("24h format", () => { + is12hSpy.mockReturnValue(false); + expect(fmt.fmtHourMinute(testDate)).toBe("15:30"); + expect(fmt.fmtFullDateTime(testDate)).toBe("So., 15. Jan., 15:30"); + expect(fmt.fmtWeekdayTime(testDate)).toBe("So., 15:30"); + expect(fmt.fmtAbsoluteDate(testDate)).toBe("So 15:30"); + }); +}); diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts index 250f22738..75d263d5e 100644 --- a/assets/js/mixins/formatter.ts +++ b/assets/js/mixins/formatter.ts @@ -1,4 +1,5 @@ import { defineComponent } from "vue"; +import { is12hFormat } from "@/units"; import { CURRENCY } from "../types/evcc"; // list of currencies where energy price should be displayed in subunits (factor 100) @@ -198,6 +199,7 @@ export default defineComponent({ if (locale === "de") return date.getHours(); return new Intl.DateTimeFormat(locale, { hour: "numeric", + hour12: is12hFormat(), }).format(date); }, weekdayShort(date: Date) { @@ -210,6 +212,7 @@ export default defineComponent({ const hour = new Intl.DateTimeFormat(this.$i18n?.locale, { hour: "numeric", minute: "numeric", + hour12: is12hFormat(), }).format(date); return `${weekday} ${hour}`.trim(); @@ -218,6 +221,7 @@ export default defineComponent({ return new Intl.DateTimeFormat(this.$i18n?.locale, { hour: "numeric", minute: "numeric", + hour12: is12hFormat(), }).format(date); }, fmtFullDateTime(date: Date, short: boolean) { @@ -227,6 +231,7 @@ export default defineComponent({ day: "numeric", hour: "numeric", minute: "numeric", + hour12: is12hFormat(), }).format(date); }, fmtWeekdayTime(date: Date) { @@ -234,6 +239,7 @@ export default defineComponent({ weekday: "short", hour: "numeric", minute: "numeric", + hour12: is12hFormat(), }).format(date); }, fmtMonthYear(date: Date) { diff --git a/assets/js/settings.js b/assets/js/settings.js index a64a86006..d3710d018 100644 --- a/assets/js/settings.js +++ b/assets/js/settings.js @@ -3,6 +3,7 @@ import { reactive, watch } from "vue"; const SETTINGS_LOCALE = "settings_locale"; const SETTINGS_THEME = "settings_theme"; const SETTINGS_UNIT = "settings_unit"; +const SETTINGS_12H_FORMAT = "settings_12h_format"; const SETTINGS_HIDDEN_FEATURES = "settings_hidden_features"; const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details"; const SETTINGS_ENERGYFLOW_PV = "settings_energyflow_pv"; @@ -59,6 +60,7 @@ const settings = reactive({ locale: read(SETTINGS_LOCALE), theme: read(SETTINGS_THEME), unit: read(SETTINGS_UNIT), + is12hFormat: readBool(SETTINGS_12H_FORMAT), hiddenFeatures: readBool(SETTINGS_HIDDEN_FEATURES), energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS), energyflowPv: readBool(SETTINGS_ENERGYFLOW_PV), @@ -76,6 +78,7 @@ const settings = reactive({ 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.hiddenFeatures, saveBool(SETTINGS_HIDDEN_FEATURES)); watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS)); watch(() => settings.energyflowPv, saveBool(SETTINGS_ENERGYFLOW_PV)); diff --git a/assets/js/units.js b/assets/js/units.js index ca11d5605..b763ca1fb 100644 --- a/assets/js/units.js +++ b/assets/js/units.js @@ -29,3 +29,11 @@ export function getUnits() { export function setUnits(value) { settings.unit = value; } + +export function is12hFormat() { + return settings.is12hFormat; +} + +export function set12hFormat(value) { + settings.is12hFormat = value; +} diff --git a/i18n/de.json b/i18n/de.json index 0c11d54a9..336139a89 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -931,6 +931,11 @@ "label": "Design", "light": "Hell" }, + "time": { + "12h": "12h", + "24h": "24h", + "label": "Zeitformat" + }, "title": "Darstellung", "unit": { "km": "Kilometer", diff --git a/i18n/en.json b/i18n/en.json index f158b84d7..3de6e1ef1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -931,6 +931,11 @@ "label": "Design", "light": "light" }, + "time": { + "12h": "12h", + "24h": "24h", + "label": "Time format" + }, "title": "User Interface", "unit": { "km": "km",