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" }