diff --git a/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts b/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts index 6b59837d303b..fe96eaec2876 100644 --- a/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts +++ b/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts @@ -1,4 +1,10 @@ -import type { CalendarEvent, CalendarEventAttendee, CalendarRepeatRule, EncryptedMailAddress } from "../../../../common/api/entities/tutanota/TypeRefs.js" +import type { + AdvancedRepeatRule, + CalendarEvent, + CalendarEventAttendee, + CalendarRepeatRule, + EncryptedMailAddress, +} from "../../../../common/api/entities/tutanota/TypeRefs.js" import { createCalendarEventAttendee, createEncryptedMailAddress } from "../../../../common/api/entities/tutanota/TypeRefs.js" import m, { Children, Component, Vnode } from "mithril" import { AllIcons, Icon, IconSize } from "../../../../common/gui/base/Icon.js" @@ -21,6 +27,7 @@ import { ExternalLink } from "../../../../common/gui/base/ExternalLink.js" import { createRepeatRuleFrequencyValues, formatEventDuration, getDisplayEventTitle, iconForAttendeeStatus } from "../CalendarGuiUtils.js" import { hasError } from "../../../../common/api/common/utils/ErrorUtils.js" +import { ByRule } from "../../../../common/calendar/import/ImportExportUtils.js" export type EventPreviewViewAttrs = { event: Omit @@ -229,20 +236,95 @@ export function formatRepetitionFrequency(repeatRule: RepeatRule): string | null const frequency = createRepeatRuleFrequencyValues().find((frequency) => frequency.value === repeatRule.frequency) if (frequency) { - return frequency.name + const freq = frequency.name + const readable = buildReadableAdvancedRepetitionRule(repeatRule.advancedRules, downcast(repeatRule.frequency)) + + return `${freq}. ${readable}`.trim() } } else { - return lang.get("repetition_msg", { + const repeatMessage = lang.get("repetition_msg", { "{interval}": repeatRule.interval, "{timeUnit}": getFrequencyTimeUnit(downcast(repeatRule.frequency)), }) + + const advancedRule = buildReadableAdvancedRepetitionRule(repeatRule.advancedRules, downcast(repeatRule.frequency)) + + return `${repeatMessage}. ${advancedRule}`.trim() } return null } +function buildReadableAdvancedRepetitionRule(advancedRule: AdvancedRepeatRule[], frequency: RepeatPeriod): string { + const hasInvalidRules = advancedRule.some( + (rule) => !((frequency === RepeatPeriod.WEEKLY || frequency === RepeatPeriod.MONTHLY) && rule.ruleType === ByRule.BYDAY), + ) + + let translationKey: TranslationKey = "withCustomRules_label" + if (hasInvalidRules) { + return lang.get(translationKey) + } + + const days: string[] = [] + + advancedRule.forEach((item) => { + switch (item.ruleType) { + case ByRule.BYDAY: + days.push(item.interval) + break + default: + return lang.get(translationKey) + } + }) + + if (days.length === 0) return "" + + if (frequency === RepeatPeriod.MONTHLY) { + const ruleRegex = /^([-+]?\d{0,3})([a-zA-Z]{2})?$/g + + const parsedRuleValue = Array.from(days[0].matchAll(ruleRegex)).flat() + + const day = parseShortDay(parsedRuleValue[2] ?? "") + const leadingValue = Number.parseInt(parsedRuleValue[1]) + + if (leadingValue === 1) { + translationKey = "firstOfPeriod_label" + } else if (leadingValue === 2) { + translationKey = "secondOfPeriod_label" + } else if (leadingValue === -1) { + translationKey = "lastOfPeriod_label" + } else if (!Number.isNaN(leadingValue)) { + translationKey = "nthOfPeriod_label" + } + + return lang.get(translationKey, { + "{days}": day, + "{n}": leadingValue, + }) + } + + return lang.get("onDays_label", { + "{days}": joinWithAnd( + days.map((day) => parseShortDay(day)), + ", ", + lang.get("and_label"), + ), + }) +} + +function joinWithAnd(items: any[], separator: string, lastSeparator: string) { + if (items.length > 1) { + const last = items.pop() + const joinedString = items.join(separator) + + return `${joinedString} ${lastSeparator} ${last}` + } + + return items.join(separator) +} + /** - * @returns {string} The returned string includes a leading separator (", " or " "). + * @returns {string} The returned string includes a leading separator (", " or ""). */ export function formatRepetitionEnd(repeatRule: RepeatRule, isAllDay: boolean): string { switch (repeatRule.endType) { @@ -287,6 +369,27 @@ function getFrequencyTimeUnit(frequency: RepeatPeriod): string { } } +function parseShortDay(day: string) { + switch (day) { + case "MO": + return lang.get("monday_label") + case "TU": + return lang.get("tuesday_label") + case "WE": + return lang.get("wednesday_label") + case "TH": + return lang.get("thursday_label") + case "FR": + return lang.get("friday_label") + case "SA": + return lang.get("saturday_label") + case "SU": + return lang.get("sunday_label") + default: + return "" + } +} + function prepareAttendees(attendees: Array, organizer: EncryptedMailAddress | null): Array { // We copy the attendees array so that we can add the organizer, in the case that they are not already in attendees // This is just for display purposes. We need to copy because event.attendees is the source of truth for the event diff --git a/src/common/misc/TranslationKey.ts b/src/common/misc/TranslationKey.ts index 66756a713a61..9de1a41c04e4 100644 --- a/src/common/misc/TranslationKey.ts +++ b/src/common/misc/TranslationKey.ts @@ -1896,27 +1896,14 @@ export type TranslationKeyType = | "friday_label" | "saturday_label" | "sunday_label" - | "january_label" - | "february_label" - | "march_label" - | "april_label" - | "may_label" - | "june_label" - | "july_label" - | "august_label" - | "september_label" - | "october_label" - | "november_label" - | "december_label" | "firstOfPeriod_label" + | "secondOfPeriod_label" + | "thirdOfPeriod_label" + | "nthOfPeriod_label" | "lastOfPeriod_label" - | "onNDayOfPeriod_label" - | "inMonths_label" | "onDays_label" - | "inWeek_label" - | "afterStartOfPeriod_label" | "and_label" - | "beforeEndOfPeriod_label" + | "withCustomRules_label" | "unsupportedAdvancedRules_msg" // Put in temporarily, will be removed soon | "localAdminGroup_label" diff --git a/src/mail-app/translations/de.ts b/src/mail-app/translations/de.ts index e22af8128520..81f44faaa9b6 100644 --- a/src/mail-app/translations/de.ts +++ b/src/mail-app/translations/de.ts @@ -1909,6 +1909,21 @@ export default { "yourFolders_action": "DEINE ORDNER", "yourMessage_label": "Deine Nachricht", "you_label": "Du", + "monday_label": "Montag", + "tuesday_label": "Dienstag", + "wednesday_label": "Mittwoch", + "thursday_label": "Donnerstag", + "friday_label": "Freitag", + "saturday_label": "Samstag", + "sunday_label": "Sonntag", + "and_label": "und", + "firstOfPeriod_label": "Am ersten {day} des Monats", + "secondOfPeriod_label": "Am zweiten {day} des Monats", + "thirdOfPeriod_label": "Am dritten {day} des Monats", + "nthOfPeriod_label": "Am {n}-ten {day} des Monats", + "lastOfPeriod_label": "Am letzten {day} des Monats", + "withCustomRules_label": "Mit benutzerdefinierten Wiederholungsregeln", + "unsupportedAdvancedRules_msg": "Dieses Ereignis enthält eine oder mehrere nicht unterstützte erweiterte Wiederholungsregeln; jede Änderung führt zum Verlust dieser Regeln.", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", diff --git a/src/mail-app/translations/de_sie.ts b/src/mail-app/translations/de_sie.ts index f8fd7fec2f6a..d1617fb3d5fa 100644 --- a/src/mail-app/translations/de_sie.ts +++ b/src/mail-app/translations/de_sie.ts @@ -1909,6 +1909,21 @@ export default { "yourFolders_action": "Ihre ORDNER", "yourMessage_label": "Ihre Nachricht", "you_label": "Sie", + "monday_label": "Montag", + "tuesday_label": "Dienstag", + "wednesday_label": "Mittwoch", + "thursday_label": "Donnerstag", + "friday_label": "Freitag", + "saturday_label": "Samstag", + "sunday_label": "Sonntag", + "and_label": "und", + "firstOfPeriod_label": "Am ersten {day} des Monats", + "secondOfPeriod_label": "Am zweiten {day} des Monats", + "thirdOfPeriod_label": "Am dritten {day} des Monats", + "nthOfPeriod_label": "Am {n}-ten {day} des Monats", + "lastOfPeriod_label": "Am letzten {day} des Monats", + "withCustomRules_label": "Mit benutzerdefinierten Wiederholungsregeln", + "unsupportedAdvancedRules_msg": "Dieses Ereignis enthält eine oder mehrere nicht unterstützte erweiterte Wiederholungsregeln; jede Änderung führt zum Verlust dieser Regeln.", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index dba7c0023c2c..a8547d9ffae4 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1912,30 +1912,16 @@ export default { "friday_label": "Friday", "saturday_label": "Saturday", "sunday_label": "Sunday", - "january_label": "January", - "february_label": "February", - "march_label": "March", - "april_label": "April", - "may_label": "May", - "june_label": "June", - "july_label": "July", - "august_label": "August", - "september_label": "September", - "october_label": "October", - "november_label": "November", - "december_label": "December", - "firstOfPeriod_label": "First {day} of the {period}", - "lastOfPeriod_label": "Last {day} of the {period}", - "onNDayOfPeriod_label": "on {day} of {period}", - "theYear_label": "the Year", - "theMonth_label": "the Month", - "inMonths_label": "in {months}", - "onDays_label": "on {days}", - "inWeek_label": "in weeks {weeks}", - "afterStartOfPeriod_label": "{days} occurrence after start of the {period}", + "onDays_label": "On {days}", "and_label": "and", - "beforeEndOfPeriod_label": "{days} occurrence before end of the {period}", + "firstOfPeriod_label": "On first {day} of the month", + "secondOfPeriod_label": "On second {day} of the month", + "thirdOfPeriod_label": "On third {day} of the month", + "nthOfPeriod_label": "On {n}th {day} of the month", + "lastOfPeriod_label": "On last {day} of the month", + "withCustomRules_label": "With custom repeat rules", "unsupportedAdvancedRules_msg": "This event contains one or more unsupported Advanced Recurrence Rules, any changes will result in the loss of these rules", + "beforeEndOfPeriod_label": "{days} occurrence before end of the {period}", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.",