diff --git a/src/common/datetime/format_date.ts b/src/common/datetime/format_date.ts
index fb6e75feb0..35bc5eaa7a 100644
--- a/src/common/datetime/format_date.ts
+++ b/src/common/datetime/format_date.ts
@@ -7,10 +7,12 @@ if (__BUILD__ === "latest" && polyfillsLoaded) {
}
// Tuesday, August 10
-export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
- formatDateWeekdayMem(locale).format(dateObj);
+export const formatDateWeekdayDay = (
+ dateObj: Date,
+ locale: FrontendLocaleData
+) => formatDateWeekdayDayMem(locale).format(dateObj);
-const formatDateWeekdayMem = memoizeOne(
+const formatDateWeekdayDayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long",
@@ -92,3 +94,14 @@ const formatDateYearMem = memoizeOne(
year: "numeric",
})
);
+
+// Monday
+export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
+ formatDateWeekdayMem(locale).format(dateObj);
+
+const formatDateWeekdayMem = memoizeOne(
+ (locale: FrontendLocaleData) =>
+ new Intl.DateTimeFormat(locale.language, {
+ weekday: "long",
+ })
+);
diff --git a/src/common/translations/day_names.ts b/src/common/translations/day_names.ts
new file mode 100644
index 0000000000..34b18fdb4a
--- /dev/null
+++ b/src/common/translations/day_names.ts
@@ -0,0 +1,10 @@
+import { addDays, startOfWeek } from "date-fns";
+import memoizeOne from "memoize-one";
+import { FrontendLocaleData } from "../../data/translation";
+import { formatDateWeekday } from "../datetime/format_date";
+
+export const dayNames = memoizeOne((locale: FrontendLocaleData): string[] =>
+ Array.from({ length: 7 }, (_, d) =>
+ formatDateWeekday(addDays(startOfWeek(new Date()), d), locale)
+ )
+);
diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts
index eec0503178..0b5fc77030 100644
--- a/src/common/translations/localize.ts
+++ b/src/common/translations/localize.ts
@@ -18,6 +18,7 @@ export type LocalizeKeys =
| `ui.card.alarm_control_panel.${string}`
| `ui.card.weather.attributes.${string}`
| `ui.card.weather.cardinal_direction.${string}`
+ | `ui.components.calendar.event.rrule.${string}`
| `ui.components.logbook.${string}`
| `ui.components.selectors.file.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
diff --git a/src/common/translations/month_names.ts b/src/common/translations/month_names.ts
new file mode 100644
index 0000000000..2e456b1855
--- /dev/null
+++ b/src/common/translations/month_names.ts
@@ -0,0 +1,10 @@
+import { addMonths, startOfYear } from "date-fns";
+import memoizeOne from "memoize-one";
+import { FrontendLocaleData } from "../../data/translation";
+import { formatDateMonth } from "../datetime/format_date";
+
+export const monthNames = memoizeOne((locale: FrontendLocaleData): string[] =>
+ Array.from({ length: 12 }, (_, m) =>
+ formatDateMonth(addMonths(startOfYear(new Date()), m), locale)
+ )
+);
diff --git a/src/components/chart/chart-date-adapter.ts b/src/components/chart/chart-date-adapter.ts
index 3819a22e88..462ee52dc1 100644
--- a/src/components/chart/chart-date-adapter.ts
+++ b/src/components/chart/chart-date-adapter.ts
@@ -40,7 +40,7 @@ import {
formatDateMonth,
formatDateMonthYear,
formatDateShort,
- formatDateWeekday,
+ formatDateWeekdayDay,
formatDateYear,
} from "../../common/datetime/format_date";
import {
@@ -92,7 +92,7 @@ _adapters._date.override({
case "hour":
return formatTime(new Date(time), this.options.locale);
case "weekday":
- return formatDateWeekday(new Date(time), this.options.locale);
+ return formatDateWeekdayDay(new Date(time), this.options.locale);
case "date":
return formatDate(new Date(time), this.options.locale);
case "day":
diff --git a/src/dialogs/more-info/controls/more-info-weather.ts b/src/dialogs/more-info/controls/more-info-weather.ts
index a680dbd754..01a091b146 100644
--- a/src/dialogs/more-info/controls/more-info-weather.ts
+++ b/src/dialogs/more-info/controls/more-info-weather.ts
@@ -14,7 +14,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
-import { formatDateWeekday } from "../../../common/datetime/format_date";
+import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
import { formatTimeWeekday } from "../../../common/datetime/format_time";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-svg-icon";
@@ -170,7 +170,7 @@ class MoreInfoWeather extends LitElement {
`
: html`
- ${formatDateWeekday(
+ ${formatDateWeekdayDay(
new Date(item.datetime),
this.hass.locale
)}
diff --git a/src/panels/calendar/dialog-calendar-event-detail.ts b/src/panels/calendar/dialog-calendar-event-detail.ts
index 33609da394..ebe36d920c 100644
--- a/src/panels/calendar/dialog-calendar-event-detail.ts
+++ b/src/panels/calendar/dialog-calendar-event-detail.ts
@@ -3,13 +3,15 @@ import { mdiCalendarClock, mdiClose } from "@mdi/js";
import { addDays, isSameDay } from "date-fns/esm";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
-import { RRule } from "rrule";
+import { RRule, Weekday } from "rrule";
import { formatDate } from "../../common/datetime/format_date";
import { formatDateTime } from "../../common/datetime/format_date_time";
import { formatTime } from "../../common/datetime/format_time";
import { fireEvent } from "../../common/dom/fire_event";
import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter";
import { isDate } from "../../common/string/is_date";
+import { dayNames } from "../../common/translations/day_names";
+import { monthNames } from "../../common/translations/month_names";
import "../../components/entity/state-info";
import "../../components/ha-date-input";
import "../../components/ha-time-input";
@@ -85,7 +87,7 @@ class DialogCalendarEventDetail extends LitElement {
${this._formatDateRange()}
${this._data!.rrule
- ? this._renderRruleAsText(this._data.rrule)
+ ? this._renderRRuleAsText(this._data.rrule)
: ""}
${this._data.description
? html`
@@ -128,20 +130,59 @@ class DialogCalendarEventDetail extends LitElement {
`;
}
- private _renderRruleAsText(value: string) {
- // TODO: Make sure this handles translations
+ private _renderRRuleAsText(value: string) {
if (!value) {
return "";
}
try {
- return html`
- ${capitalizeFirstLetter(RRule.fromString(`RRULE:${value}`).toText())}
-
`;
+ const rule = RRule.fromString(`RRULE:${value}`);
+ if (rule.isFullyConvertibleToText()) {
+ return html`
+ ${capitalizeFirstLetter(
+ rule.toText(
+ this._translateRRuleElement,
+ {
+ dayNames: dayNames(this.hass.locale),
+ monthNames: monthNames(this.hass.locale),
+ tokens: {},
+ },
+ this._formatDate
+ )
+ )}
+
`;
+ }
+
+ return html`
Cannot convert recurrence rule
`;
} catch (e) {
- return "";
+ return "Error while processing the rule";
}
}
+ private _translateRRuleElement = (id: string | number | Weekday): string => {
+ if (typeof id === "string") {
+ return this.hass.localize(`ui.components.calendar.event.rrule.${id}`);
+ }
+
+ return "";
+ };
+
+ private _formatDate = (year: number, month: string, day: number): string => {
+ if (!year || !month || !day) {
+ return "";
+ }
+
+ // Build date so we can then format it
+ const date = new Date();
+ date.setFullYear(year);
+ // As input we already get the localized month name, so we now unfortunately
+ // need to convert it back to something Date can work with. The already localized
+ // months names are a must in the RRule.Language structure (an empty string[] would
+ // mean we get undefined months input in this method here).
+ date.setMonth(monthNames(this.hass.locale).indexOf(month));
+ date.setDate(day);
+ return formatDate(date, this.hass.locale);
+ };
+
private _formatDateRange() {
const start = new Date(this._data!.dtstart);
// All day events should be displayed as a day earlier
diff --git a/src/panels/calendar/dialog-calendar-event-editor.ts b/src/panels/calendar/dialog-calendar-event-editor.ts
index 7316f27dc3..06dc41c1fe 100644
--- a/src/panels/calendar/dialog-calendar-event-editor.ts
+++ b/src/panels/calendar/dialog-calendar-event-editor.ts
@@ -330,6 +330,13 @@ class DialogCalendarEventEditor extends LitElement {
}
private async _createEvent() {
+ if (!this._summary || !this._calendarId) {
+ this._error = this.hass.localize(
+ "ui.components.calendar.event.not_all_required_fields"
+ );
+ return;
+ }
+
this._submitting = true;
try {
await createCalendarEvent(
@@ -418,6 +425,10 @@ class DialogCalendarEventEditor extends LitElement {
state-info {
line-height: 40px;
}
+ ha-alert {
+ display: block;
+ margin-bottom: 16px;
+ }
ha-textfield,
ha-textarea {
display: block;
diff --git a/src/translations/en.json b/src/translations/en.json
index 2ba4a278af..c7e2d39589 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -642,6 +642,7 @@
"all_day": "All Day",
"start": "Start",
"end": "End",
+ "not_all_required_fields": "Not all required fields are filled in.",
"confirm_delete": {
"delete": "Delete Event",
"delete_this": "Delete Only This Event",
@@ -666,6 +667,30 @@
"daily": "days"
}
},
+ "rrule": {
+ "every": "every",
+ "years": "years",
+ "year": "year",
+ "months": "months",
+ "month": "month",
+ "weeks": "weeks",
+ "week": "week",
+ "weekdays": "weekdays",
+ "weekday": "weekday",
+ "days": "days",
+ "day": "day",
+ "until": "until",
+ "for": "for",
+ "in": "in",
+ "on": "on",
+ "on the": "on the",
+ "and": "and",
+ "or": "or",
+ "at": "at",
+ "last": "last",
+ "time": "time",
+ "times": "times"
+ },
"summary": "Summary",
"description": "Description"
}