Add first weekday option in profile (#13991)

This commit is contained in:
Paul Bottein 2022-10-10 16:58:27 +02:00 committed by GitHub
parent 4deee46864
commit 907466d060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 253 additions and 14 deletions

View File

@ -137,6 +137,7 @@
"vis-network": "^8.5.4",
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"weekstart": "^1.1.0",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",

View File

@ -0,0 +1,29 @@
import { getWeekStartByLocale } from "weekstart";
import { FrontendLocaleData, FirstWeekday } from "../../data/translation";
export const weekdays = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
] as const;
export const firstWeekdayIndex = (locale: FrontendLocaleData): number => {
if (locale.first_weekday === FirstWeekday.language) {
// @ts-ignore
if ("weekInfo" in Intl.Locale.prototype) {
// @ts-ignore
return new Intl.Locale(locale.language).weekInfo.firstDay % 7;
}
return getWeekStartByLocale(locale.language) % 7;
}
return weekdays.indexOf(locale.first_weekday);
};
export const firstWeekday = (locale: FrontendLocaleData) => {
const index = firstWeekdayIndex(locale);
return weekdays[index];
};

View File

@ -33,6 +33,10 @@ const Component = Vue.extend({
return new Date();
},
},
firstDay: {
type: Number,
default: 1,
},
},
render(createElement) {
// @ts-ignore
@ -48,6 +52,10 @@ const Component = Vue.extend({
disabled: this.disabled,
// @ts-ignore
ranges: this.ranges ? {} : false,
"locale-data": {
// @ts-ignore
firstDay: this.firstDay,
},
},
model: {
value: {

View File

@ -2,6 +2,7 @@ import { mdiCalendar } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateNumeric } from "../common/datetime/format_date";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-svg-icon";
@ -14,6 +15,7 @@ export interface datePickerDialogParams {
min?: string;
max?: string;
locale?: string;
firstWeekday?: number;
onChange: (value: string) => void;
}
@ -67,6 +69,7 @@ export class HaDateInput extends LitElement {
value: this.value,
onChange: (value) => this._valueChanged(value),
locale: this.locale.language,
firstWeekday: firstWeekdayIndex(this.locale),
});
}

View File

@ -14,6 +14,7 @@ import {
import { customElement, property } from "lit/decorators";
import { formatDateTime } from "../common/datetime/format_date_time";
import { useAmPm } from "../common/datetime/use_am_pm";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { computeRTLDirection } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types";
import "./date-range-picker";
@ -58,6 +59,7 @@ export class HaDateRangePicker extends LitElement {
start-date=${this.startDate}
end-date=${this.endDate}
?ranges=${this.ranges !== undefined}
first-day=${firstWeekdayIndex(this.hass.locale)}
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>

View File

@ -40,6 +40,7 @@ export class HaDialogDatePicker extends LitElement {
.max=${this._params.max}
.locale=${this._params.locale}
@datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday}
></app-datepicker>
<mwc-button slot="secondaryAction" @click=${this._setToday}
>today</mwc-button

View File

@ -17,10 +17,22 @@ export enum TimeFormat {
twenty_four = "24",
}
export enum FirstWeekday {
language = "language",
monday = "monday",
tuesday = "tuesday",
wednesday = "wednesday",
thursday = "thursday",
friday = "friday",
saturday = "saturday",
sunday = "sunday",
}
export interface FrontendLocaleData {
language: string;
number_format: NumberFormat;
time_format: TimeFormat;
first_weekday: FirstWeekday;
}
declare global {

View File

@ -6,7 +6,7 @@ import {
import { fireEvent } from "../common/dom/fire_event";
import { computeLocalize } from "../common/translations/localize";
import { DEFAULT_PANEL } from "../data/panel";
import { NumberFormat, TimeFormat } from "../data/translation";
import { FirstWeekday, NumberFormat, TimeFormat } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import { getLocalLanguage, getTranslation } from "../util/common-translation";
@ -224,6 +224,7 @@ export const provideHass = (
language: localLanguage,
number_format: NumberFormat.language,
time_format: TimeFormat.language,
first_weekday: FirstWeekday.language,
},
resources: null as any,
localize: () => "",

View File

@ -37,6 +37,7 @@ import type {
HomeAssistant,
ToggleButton,
} from "../../types";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
declare global {
interface HTMLElementTagNameMap {
@ -214,6 +215,7 @@ export class HAFullCalendar extends LitElement {
const config: CalendarOptions = {
...defaultFullCalendarConfig,
locale: this.hass.language,
firstDay: firstWeekdayIndex(this.hass.locale),
initialView: this.initialView,
eventTimeFormat: {
hour: useAmPm(this.hass.locale) ? "numeric" : "2-digit",

View File

@ -1,15 +1,17 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { firstWeekdayIndex } from "../../../../../common/datetime/first_weekday";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { TimeCondition } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { ConditionElement } from "../ha-automation-condition-row";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { TimeCondition } from "../../../../../data/automation";
import { FrontendLocaleData } from "../../../../../data/translation";
import type { HomeAssistant } from "../../../../../types";
import type { ConditionElement } from "../ha-automation-condition-row";
const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
const DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const;
@customElement("ha-automation-condition-time")
export class HaTimeCondition extends LitElement implements ConditionElement {
@ -30,10 +32,15 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
private _schema = memoizeOne(
(
localize: LocalizeFunc,
locale: FrontendLocaleData,
inputModeAfter?: boolean,
inputModeBefore?: boolean
) =>
[
) => {
const dayIndex = firstWeekdayIndex(locale);
const sortedDays = DAYS.slice(dayIndex, DAYS.length).concat(
DAYS.slice(0, dayIndex)
);
return [
{
name: "mode_after",
type: "select",
@ -87,7 +94,7 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
{
type: "multi_select",
name: "weekday",
options: DAYS.map(
options: sortedDays.map(
(day) =>
[
day,
@ -97,7 +104,8 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
] as const
),
},
] as const
] as const;
}
);
protected render() {
@ -110,6 +118,7 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
const schema = this._schema(
this.hass.localize,
this.hass.locale,
inputModeAfter,
inputModeBefore
);

View File

@ -19,6 +19,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import { formatTime24h } from "../../../../common/datetime/format_time";
import { useAmPm } from "../../../../common/datetime/use_am_pm";
import { firstWeekdayIndex } from "../../../../common/datetime/first_weekday";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-textfield";
@ -169,6 +170,7 @@ class HaScheduleForm extends LitElement {
const config: CalendarOptions = {
...defaultFullCalendarConfig,
locale: this.hass.language,
firstDay: firstWeekdayIndex(this.hass.locale),
slotLabelFormat: {
hour: "numeric",
minute: undefined,

View File

@ -24,6 +24,7 @@ import "./ha-force-narrow-row";
import "./ha-long-lived-access-tokens-card";
import "./ha-mfa-modules-card";
import "./ha-pick-dashboard-row";
import "./ha-pick-first-weekday-row";
import "./ha-pick-language-row";
import "./ha-pick-number-format-row";
import "./ha-pick-theme-row";
@ -100,6 +101,10 @@ class HaPanelProfile extends LitElement {
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-time-format-row>
<ha-pick-first-weekday-row
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-first-weekday-row>
<ha-pick-theme-row
.narrow=${this.narrow}
.hass=${this.hass}

View File

@ -0,0 +1,70 @@
import "@material/mwc-list/mwc-list-item";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { firstWeekday } from "../../common/datetime/first_weekday";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-select";
import "../../components/ha-settings-row";
import { FirstWeekday } from "../../data/translation";
import { HomeAssistant } from "../../types";
@customElement("ha-pick-first-weekday-row")
class FirstWeekdayRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public narrow!: boolean;
protected render(): TemplateResult {
return html`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize("ui.panel.profile.first_weekday.header")}
</span>
<span slot="description">
${this.hass.localize("ui.panel.profile.first_weekday.description")}
</span>
<ha-select
.label=${this.hass.localize(
"ui.panel.profile.first_weekday.dropdown_label"
)}
.disabled=${this.hass.locale === undefined}
.value=${this.hass.locale.first_weekday}
@selected=${this._handleFormatSelection}
>
${Object.values(FirstWeekday).map((day) => {
const value = this.hass.localize(
`ui.panel.profile.first_weekday.values.${day}`
);
const twoLine = day === FirstWeekday.language;
return html`
<mwc-list-item .value=${day} .twoline=${twoLine}>
<span>${value}</span>
${twoLine
? html`
<span slot="secondary"
>${this.hass.localize(
`ui.panel.profile.first_weekday.values.${firstWeekday(
this.hass.locale
)}`
)}</span
>
`
: ""}
</mwc-list-item>
`;
})}
</ha-select>
</ha-settings-row>
`;
}
private async _handleFormatSelection(ev) {
fireEvent(this, "hass-first-weekday-select", ev.target.value);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-pick-first-weekday-row": FirstWeekdayRow;
}
}

View File

@ -15,7 +15,7 @@ import { subscribeFrontendUserData } from "../data/frontend";
import { forwardHaptic } from "../data/haptics";
import { DEFAULT_PANEL } from "../data/panel";
import { serviceCallWillDisconnect } from "../data/service";
import { NumberFormat, TimeFormat } from "../data/translation";
import { FirstWeekday, NumberFormat, TimeFormat } from "../data/translation";
import { subscribePanels } from "../data/ws-panels";
import { translationMetadata } from "../resources/translations-metadata";
import { Constructor, HomeAssistant, ServiceCallResponse } from "../types";
@ -58,6 +58,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
language,
number_format: NumberFormat.language,
time_format: TimeFormat.language,
first_weekday: FirstWeekday.language,
},
resources: null as any,
localize: () => "",

View File

@ -6,6 +6,7 @@ import {
} from "../common/util/compute_rtl";
import { debounce } from "../common/util/debounce";
import {
FirstWeekday,
getHassTranslations,
getHassTranslationsPre109,
NumberFormat,
@ -36,6 +37,9 @@ declare global {
"hass-time-format-select": {
time_format: TimeFormat;
};
"hass-first-weekday-select": {
first_weekday: FirstWeekday;
};
"translations-updated": undefined;
}
}
@ -76,6 +80,9 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
this.addEventListener("hass-time-format-select", (e) => {
this._selectTimeFormat((e as CustomEvent).detail, true);
});
this.addEventListener("hass-first-weekday-select", (e) => {
this._selectFirstWeekday((e as CustomEvent).detail, true);
});
this._loadCoreTranslations(getLocalLanguage());
}
@ -114,6 +121,13 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
// We just got time_format from backend, no need to save back
this._selectTimeFormat(locale.time_format, false);
}
if (
locale?.first_weekday &&
this.hass!.locale.first_weekday !== locale.first_weekday
) {
// We just got first_weekday from backend, no need to save back
this._selectFirstWeekday(locale.first_weekday, false);
}
});
this.hass!.connection.subscribeEvents(
@ -161,6 +175,18 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
}
}
private _selectFirstWeekday(
first_weekday: FirstWeekday,
saveToBackend: boolean
) {
this._updateHass({
locale: { ...this.hass!.locale, first_weekday: first_weekday },
});
if (saveToBackend) {
saveTranslationPreferences(this.hass!, this.hass!.locale);
}
}
private _selectLanguage(language: string, saveToBackend: boolean) {
if (!this.hass) {
// should not happen, do it to avoid use this.hass!

View File

@ -1117,6 +1117,15 @@
"day": "{count} {count, plural,\n one {day}\n other {days}\n}",
"week": "{count} {count, plural,\n one {week}\n other {weeks}\n}"
},
"weekdays": {
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday"
},
"errors": {
"config": {
"no_type_provided": "No type provided.",
@ -4271,6 +4280,21 @@
"24": "24 hours"
}
},
"first_weekday": {
"header": "First day of the week",
"dropdown_label": "First day of the week",
"description": "Choose the starting day for calendars.",
"values": {
"language": "Auto (use language setting)",
"monday": "[%key:ui::weekdays::monday%]",
"tuesday": "[%key:ui::weekdays::tuesday%]",
"wednesday": "[%key:ui::weekdays::wednesday%]",
"thursday": "[%key:ui::weekdays::thursday%]",
"friday": "[%key:ui::weekdays::friday%]",
"saturday": "[%key:ui::weekdays::saturday%]",
"sunday": "[%key:ui::weekdays::sunday%]"
}
},
"themes": {
"header": "Theme",
"error_no_theme": "No themes available.",

View File

@ -76,6 +76,7 @@ export async function getUserLocale(
const language = result?.language;
const number_format = result?.number_format;
const time_format = result?.time_format;
const first_weekday = result?.first_weekday;
if (language) {
const availableLanguage = findAvailableLanguage(language);
if (availableLanguage) {
@ -83,12 +84,14 @@ export async function getUserLocale(
language: availableLanguage,
number_format,
time_format,
first_weekday,
};
}
}
return {
number_format,
time_format,
first_weekday,
};
}

View File

@ -1,7 +1,11 @@
import { assert } from "chai";
import { formatDate } from "../../../src/common/datetime/format_date";
import { NumberFormat, TimeFormat } from "../../../src/data/translation";
import {
NumberFormat,
TimeFormat,
FirstWeekday,
} from "../../../src/data/translation";
describe("formatDate", () => {
const dateObj = new Date(2017, 10, 18, 11, 12, 13, 1400);
@ -12,6 +16,7 @@ describe("formatDate", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.language,
first_weekday: FirstWeekday.language,
}),
"November 18, 2017"
);

View File

@ -4,7 +4,11 @@ import {
formatDateTime,
formatDateTimeWithSeconds,
} from "../../../src/common/datetime/format_date_time";
import { NumberFormat, TimeFormat } from "../../../src/data/translation";
import {
NumberFormat,
TimeFormat,
FirstWeekday,
} from "../../../src/data/translation";
describe("formatDateTime", () => {
const dateObj = new Date(2017, 10, 18, 23, 12, 13, 400);
@ -15,6 +19,7 @@ describe("formatDateTime", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
first_weekday: FirstWeekday.language,
}),
"November 18, 2017 at 11:12 PM"
);
@ -23,6 +28,7 @@ describe("formatDateTime", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
first_weekday: FirstWeekday.language,
}),
"November 18, 2017 at 23:12"
);
@ -38,6 +44,7 @@ describe("formatDateTimeWithSeconds", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
first_weekday: FirstWeekday.language,
}),
"November 18, 2017 at 11:12:13 PM"
);
@ -46,6 +53,7 @@ describe("formatDateTimeWithSeconds", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
first_weekday: FirstWeekday.language,
}),
"November 18, 2017 at 23:12:13"
);

View File

@ -5,7 +5,11 @@ import {
formatTimeWithSeconds,
formatTimeWeekday,
} from "../../../src/common/datetime/format_time";
import { NumberFormat, TimeFormat } from "../../../src/data/translation";
import {
NumberFormat,
TimeFormat,
FirstWeekday,
} from "../../../src/data/translation";
describe("formatTime", () => {
const dateObj = new Date(2017, 10, 18, 23, 12, 13, 1400);
@ -16,6 +20,7 @@ describe("formatTime", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
first_weekday: FirstWeekday.language,
}),
"11:12 PM"
);
@ -24,6 +29,7 @@ describe("formatTime", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
first_weekday: FirstWeekday.language,
}),
"23:12"
);
@ -39,6 +45,7 @@ describe("formatTimeWithSeconds", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
first_weekday: FirstWeekday.language,
}),
"11:12:13 PM"
);
@ -47,6 +54,7 @@ describe("formatTimeWithSeconds", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
first_weekday: FirstWeekday.language,
}),
"23:12:13"
);
@ -62,6 +70,7 @@ describe("formatTimeWeekday", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.am_pm,
first_weekday: FirstWeekday.language,
}),
"Wednesday 11:12 PM"
);
@ -70,6 +79,7 @@ describe("formatTimeWeekday", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
first_weekday: FirstWeekday.language,
}),
"Wednesday 23:12"
);

View File

@ -1,13 +1,18 @@
import { assert } from "chai";
import { relativeTime } from "../../../src/common/datetime/relative_time";
import { NumberFormat, TimeFormat } from "../../../src/data/translation";
import {
NumberFormat,
TimeFormat,
FirstWeekday,
} from "../../../src/data/translation";
describe("relativeTime", () => {
const locale = {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.language,
first_weekday: FirstWeekday.language,
};
it("now", () => {

View File

@ -5,6 +5,7 @@ import {
FrontendLocaleData,
NumberFormat,
TimeFormat,
FirstWeekday,
} from "../../../src/data/translation";
let localeData: FrontendLocaleData;
@ -19,6 +20,7 @@ describe("computeStateDisplay", () => {
language: "en",
number_format: NumberFormat.comma_decimal,
time_format: TimeFormat.am_pm,
first_weekday: FirstWeekday.language,
};
});

View File

@ -5,6 +5,7 @@ import {
FrontendLocaleData,
NumberFormat,
TimeFormat,
FirstWeekday,
} from "../../../src/data/translation";
describe("formatNumber", () => {
@ -13,6 +14,7 @@ describe("formatNumber", () => {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.language,
first_weekday: FirstWeekday.language,
};
// Node only ships with English support for `Intl`, so we can not test for other number formats here.

View File

@ -9145,6 +9145,7 @@ fsevents@^1.2.7:
webpack-dev-server: ^4.3.0
webpack-manifest-plugin: ^4.0.2
webpackbar: ^5.0.0-3
weekstart: ^1.1.0
workbox-build: ^6.4.2
workbox-cacheable-response: ^6.4.2
workbox-core: ^6.4.2
@ -15781,6 +15782,13 @@ typescript@^4.4.3:
languageName: node
linkType: hard
"weekstart@npm:^1.1.0":
version: 1.1.0
resolution: "weekstart@npm:1.1.0"
checksum: afce96e0b95809a30f00fa02b13a0927324d9f76b9c10ce6b3de9bbd5926615156f8a0526c63e2bd1cabdc8ec3da68b8df8d6608b6364ded11b5da300a8cfcb4
languageName: node
linkType: hard
"whatwg-url@npm:^7.0.0":
version: 7.1.0
resolution: "whatwg-url@npm:7.1.0"