diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6ae8aeb..788c62c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -192,7 +192,9 @@ Calendar enablement, optional features and week start persist through `AppSettin optional fields in `SettingsDocument`. Week start initially uses `Calendar.current.firstWeekday`, then persists independently of later system changes; all seven weekdays are supported by the settings picker, backups, month grids and widgets. Menu-bar lunar modes remain available in -every app language and use localized lunar dates. Older backups preserve those settings. Additional +every app language and use localized lunar dates when lunar display is enabled. Disabling +lunar display hides its menu-bar presets and text-style controls, renders the saved lunar +preset as date and weekday, and stops applying lunar text styles without overwriting them. Older backups preserve those settings. Additional calendars default off and appear only in selected-day details. There is no event access or network request at runtime. Use `--snapshot --calendar-only` for deterministic light/dark calendar screenshots without starting unrelated samplers or scans. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e432e8c..f1d7857 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -54,7 +54,7 @@ GitHub Actions 只在分支 push 且 `server/**` 发生变化时构建并发布 构建固定使用 `arm64`,仅支持 Apple Silicon Mac。 有 Developer ID Application 证书时,Taskfile 会自动使用钥匙串中第一个证书的唯一指纹;否则由构建脚本逐层补齐 ad-hoc 签名与 entitlement。临时签名没有团队身份,不能加载启用团队验证的 Sparkle 动态库,因此仅此本地/CI 路径关闭 Hardened Runtime;正式 Developer ID 构建仍强制启用,且临时签名构建不能在线安装或操作特权辅助工具。 桌面小组件与主应用通过 `group.work.12306.xstats` App Group 共享只读展示摘要。 -菜单栏日历在所有应用语言下均提供“日期与星期”“日期与农历”“农历”“自定义”显示方式;日期与农历按应用语言呈现,切换语言保留用户选择。自定义可自由组合 y/M/d 字段与分隔符,年份可省略,输入时显示预览并提示无效格式,不影响月历与单日详情的星期显示;支持农历字号与对比度、可选悬停展开,按分钟刷新,休眠或关闭功能后停止。日程和到期提醒默认关闭,通过明确的授权按钮请求 EventKit 权限,只在日历面板可见时查询当前 42 格日期范围;只读展示,不修改日程,也不向 Widget、设置备份或服务端传递内容。列表 ID 仅保存在本机,显示偏好参与设置备份。节假日覆盖年份来自内置 Tyme 数据并随应用更新;“假期倒计时与请假建议”仅在开启“中国大陆节假日与调休”后显示在该开关下方;关闭节假日时隐藏子选项并保留原选择。假期倒计时与最多 3 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。切换到未收录法定安排的年份时,不查询该年的法定节假日,不计算或沿用当前年份的假期概览与请假建议,仅提示缺少该年数据;返回有数据的年份后恢复展示。 +菜单栏日历在所有应用语言下均提供“日期与星期”“日期与农历”“农历”“自定义”显示方式;日期与农历按应用语言呈现,切换语言保留用户选择。关闭农历时,字号与对比度子选项以及菜单栏中的两种农历方式隐藏;已保存的农历方式暂以“日期与星期”呈现,字号和对比度样式不生效,重新开启后恢复原选择。子选项位于农历开关下方。自定义可自由组合 y/M/d 字段与分隔符,年份可省略,输入时显示预览并提示无效格式,不影响月历与单日详情的星期显示;支持农历字号与对比度、可选悬停展开,按分钟刷新,休眠或关闭功能后停止。日程和到期提醒默认关闭,通过明确的授权按钮请求 EventKit 权限,只在日历面板可见时查询当前 42 格日期范围;只读展示,不修改日程,也不向 Widget、设置备份或服务端传递内容。列表 ID 仅保存在本机,显示偏好参与设置备份。节假日覆盖年份来自内置 Tyme 数据并随应用更新;“假期倒计时与请假建议”仅在开启“中国大陆节假日与调休”后显示在该开关下方;关闭节假日时隐藏子选项并保留原选择。假期倒计时与最多 3 天的请假建议只基于已公布的中国调休与双休安排,跨到未知年份时停止推算。切换到未收录法定安排的年份时,不查询该年的法定节假日,不计算或沿用当前年份的假期概览与请假建议,仅提示缺少该年数据;返回有数据的年份后恢复展示。 日历首次初始化按 macOS 地区设置决定默认值,不按设备销售地或应用语言判断:中国大陆(CN)默认启用农历、大陆节假日与调休、传统与公历节日、节气与时令及假期概览;其他地区(含台湾、香港、澳门)默认不启用这些地区相关的可选内容。各地区的月历和单日详情均固定显示星期,不提供独立开关;旧配置与备份中的星期开关被忽略,其他选择保留。藏历与回历各地区均默认关闭。“节气与时令”统一控制二十四节气、三伏、梅雨与数九;月历只显示节气当天、时令阶段起点及出梅,单日详情与单日小组件保留具体天数。旧节气或时令开关任一开启即恢复为开启,全部关闭才恢复为关闭;使用新的设置键使 Widget 旧摘要失效并重新生成。月历不再显示干支,也不提供对应开关;年月日干支只在日期详情中显示,旧偏好与备份中的干支选项被忽略,其他选项保留。用户可自行启用或关闭,首次默认值保存后不随语言、地区变化重置;已有本机偏好及旧备份保持原选择。每周起始日首次采用 macOS 系统日历设置并保存,支持星期日到星期六,手动选择及备份恢复优先;后续系统设置变化不覆盖已保存的值,主应用月历与小组件使用同一周起始日。大陆节假日数据不能用于推断其他地区的法定安排。单日及整月小组件遵循日历选项,未启用大陆节假日时“明天上班吗”提示“仅支持中国大陆节假日”;黄历在单日详情中固定显示,旧偏好与备份中的黄历开关被忽略;宜忌详情仍只为选中日期按需计算,小组件复用主应用预计算的八天摘要缓存。 月历使用固定六行布局,日期 ID 在生成日期时计算一次。面板打开时测量初始高度,后续翻月、详情与日程内容变化在内部滚动,不从内容几何回调反向调整窗口,避免窗口高度与懒布局估算形成反馈循环。 月历计算仅缓存最近一个月份(包含周起始日和时区),供实际面板、高度测量和重复打开共用;翻月替换缓存,避免浏览历史导致内存累积。日程日期标记在工作 actor 内随查询结果生成,界面不再逐格扫描日程列表;连续 EventKit 通知按 300 毫秒窗口合并,关闭面板时取消待执行刷新。 diff --git a/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift b/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift index c2cc42a..7845d1a 100644 --- a/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift +++ b/Packages/XStatsKit/Sources/Localization/CalendarTranslations.swift @@ -36,7 +36,6 @@ extension Translations { {}第{}天 {1}, day {2} 藏历 Tibetan calendar 回历 Hijri calendar -公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Gregorian dates stay visible. Tibetan and Hijri dates appear in day details. Plum-rain days follow traditional calendar rules, not weather forecasts. 该日期暂无藏历数据 Tibetan calendar data is unavailable for this date 该年份暂无中国法定假日与调休数据 Chinese holiday and makeup workday data is unavailable for this year 按传统历法推算,并非天气预报 Traditional calendar calculation, not a weather forecast diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv index c95f6be..9814a69 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv @@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence {}第{}天 اليوم {2} من {1} 藏历 التقويم التبتي 回历 التقويم الهجري -公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 تظل التواريخ الميلادية ظاهرة. تظهر التواريخ التبتية والهجرية في تفاصيل اليوم. تُحسب أمطار البرقوق وفق القواعد التقليدية وليست توقعات جوية. 该日期暂无藏历数据 لا تتوفر بيانات التقويم التبتي لهذا التاريخ 该年份暂无中国法定假日与调休数据 لا تتوفر بيانات العطلات الصينية وأيام العمل التعويضية لهذه السنة 按传统历法推算,并非天气预报 حساب وفق التقويم التقليدي، وليس توقعًا جويًا diff --git a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv index e88d9ae..84ff99e 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv @@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence {}第{}天 {1}, Tag {2} 藏历 Tibetischer Kalender 回历 Hidschri-Kalender -公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Gregorianische Daten bleiben sichtbar. Tibetische und Hidschri-Daten stehen in den Tagesdetails. Die Pflaumenregenzeit folgt traditionellen Kalenderregeln, nicht Wettervorhersagen. 该日期暂无藏历数据 Für dieses Datum sind keine tibetischen Kalenderdaten verfügbar 该年份暂无中国法定假日与调休数据 Für dieses Jahr fehlen chinesische Feiertags- und Ausgleichsarbeitstagsdaten 按传统历法推算,并非天气预报 Traditionelle Kalenderberechnung, keine Wettervorhersage diff --git a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv index 07937d1..ae41930 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv @@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence {}第{}天 {1}, día {2} 藏历 Calendario tibetano 回历 Calendario hiyri -公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Las fechas gregorianas siempre se muestran. Los calendarios tibetano e hiyri aparecen en los detalles del día. Las lluvias de ciruela se calculan según reglas tradicionales, no son pronósticos. 该日期暂无藏历数据 No hay datos del calendario tibetano para esta fecha 该年份暂无中国法定假日与调休数据 No hay datos de festivos chinos ni días laborales compensatorios para este año 按传统历法推算,并非天气预报 Cálculo del calendario tradicional, no un pronóstico diff --git a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv index b34296e..6baa1be 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv @@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence {}第{}天 {1}, jour {2} 藏历 Calendrier tibétain 回历 Calendrier hégirien -公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 Les dates grégoriennes restent visibles. Les dates tibétaines et hégiriennes figurent dans les détails du jour. Les pluies des prunes suivent les règles traditionnelles, sans prévision météo. 该日期暂无藏历数据 Aucune donnée du calendrier tibétain pour cette date 该年份暂无中国法定假日与调休数据 Aucune donnée sur les jours fériés chinois et les jours de travail compensatoires pour cette année 按传统历法推算,并非天气预报 Calcul du calendrier traditionnel, pas une prévision météo diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv index ec44d56..be48580 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv @@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence {}第{}天 {1}の{2}日目 藏历 チベット暦 回历 ヒジュラ暦 -公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 西暦は常に表示します。チベット暦とヒジュラ暦は日付の詳細に表示します。入梅・出梅は伝統的な暦の計算で、天気予報ではありません。 该日期暂无藏历数据 この日付のチベット暦データはありません 该年份暂无中国法定假日与调休数据 この年の中国の祝日・振替出勤日データはありません 按传统历法推算,并非天气预报 伝統的な暦の計算であり、天気予報ではありません diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv index 91a2c80..ec00c26 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv @@ -1335,7 +1335,6 @@ Apple 智能 Apple Intelligence {}第{}天 {1} {2}일째 藏历 티베트력 回历 이슬람력 -公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 양력은 항상 표시됩니다. 티베트력과 이슬람력은 날짜 상세 정보에 표시됩니다. 입매·출매는 전통 달력 계산이며 일기 예보가 아닙니다. 该日期暂无藏历数据 이 날짜의 티베트력 데이터가 없습니다 该年份暂无中国法定假日与调休数据 이 연도의 중국 공휴일 및 대체 근무일 데이터가 없습니다 按传统历法推算,并非天气预报 전통 달력 계산이며 일기 예보가 아닙니다 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv index d732e83..8bbf942 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv @@ -1336,7 +1336,6 @@ Apple 智能 Apple Intelligence {}第{}天 {1}第{2}天 藏历 藏曆 回历 回曆 -公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。 公曆始終顯示。藏曆與回曆顯示在日期詳情中;梅雨天按傳統曆法推算,並非天氣預報。 该日期暂无藏历数据 該日期暫無藏曆資料 该年份暂无中国法定假日与调休数据 該年份暫無中國法定假日與調休資料 按传统历法推算,并非天气预报 按傳統曆法推算,並非天氣預報 diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarMenuBarController.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarMenuBarController.swift index 911e3c4..81bfbd2 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarMenuBarController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarMenuBarController.swift @@ -100,7 +100,7 @@ final class CalendarMenuBarController: NSObject { item.button?.sendAction(on: [.leftMouseUp, .rightMouseUp]) item.button?.image = NSImage(systemSymbolName: "calendar", accessibilityDescription: tr("日历")) item.button?.imagePosition = .imageLeading - item.button?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) + item.button?.font = .menuBarFont(ofSize: 0) self.item = item } if timer == nil { @@ -130,7 +130,8 @@ final class CalendarMenuBarController: NSObject { private func refreshTitle() { guard let button = item?.button else { return } let preferences = model.settings.calendarPreferences - let title = preferences.title(at: Date(), locale: L10n.locale) + let title = preferences.title(at: Date(), locale: L10n.locale, + showsLunar: model.settings.calendarFeatures.contains(.lunar)) button.title = title.isEmpty ? "" : " " + title button.image = NSImage(systemSymbolName: "calendar", accessibilityDescription: tr("日历")) let formatter = DateFormatter() diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPopover.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPopover.swift index d8f9f4c..0400d60 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPopover.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPopover.swift @@ -339,19 +339,22 @@ private struct CalendarDayCell: View { private var foreground: Color { isSelected ? .white : isRest ? .red : DS.Palette.textPrimary } var body: some View { + let showsLunar = features.contains(.lunar) + let largeLunarText = showsLunar && preferences.largeLunarText + let strongerLunarText = showsLunar && preferences.strongerLunarText Button(action: action) { VStack(spacing: sizing.cellHeight < 50 ? 2 : 3) { Text(String(day.day)).font(.system(size: sizing.dayFontSize, weight: isToday ? .semibold : .regular, design: .rounded)) Text(tr(day.subtitle(features: features))) - .font(.system(size: preferences.largeLunarText ? sizing.subtitleFontSize + 2 : sizing.subtitleFontSize, - weight: preferences.strongerLunarText ? .semibold : .regular)) + .font(.system(size: largeLunarText ? sizing.subtitleFontSize + 2 : sizing.subtitleFontSize, + weight: strongerLunarText ? .semibold : .regular)) .lineLimit(1) .minimumScaleFactor(0.75) } .foregroundStyle(foreground) .frame(maxWidth: .infinity) - .frame(height: sizing.cellHeight + (preferences.largeLunarText ? 8 : 0)) + .frame(height: sizing.cellHeight + (largeLunarText ? 8 : 0)) .background(isSelected ? Color.accentColor : holiday != nil ? foreground.opacity(0.05) : .clear, in: RoundedRectangle(cornerRadius: 10)) .overlay { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPreferences.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPreferences.swift index 26be6a5..4ba8468 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPreferences.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarPreferences.swift @@ -10,6 +10,11 @@ public struct CalendarPreferences: Codable, Equatable, Sendable { case standard, dateLunar, lunar, custom public var id: String { rawValue } + /// 总开关只改变实际呈现,保留原模式,重新开启农历时即可恢复。 + func effective(showsLunar: Bool) -> Self { + !showsLunar && (self == .lunar || self == .dateLunar) ? .standard : self + } + var title: String { switch self { case .standard: tr("日期与星期") @@ -70,14 +75,15 @@ public struct CalendarPreferences: Codable, Equatable, Sendable { } @MainActor - func title(at date: Date, locale: Locale, timeZone: TimeZone = .autoupdatingCurrent) -> String { + func title(at date: Date, locale: Locale, timeZone: TimeZone = .autoupdatingCurrent, + showsLunar: Bool = true) -> String { let formatter = DateFormatter() formatter.locale = locale var calendar = Calendar(identifier: .gregorian) calendar.timeZone = timeZone formatter.calendar = calendar formatter.timeZone = timeZone - switch display { + switch display.effective(showsLunar: showsLunar) { case .standard: formatter.setLocalizedDateFormatFromTemplate("MdEEE") case .custom: // 用户明确选择顺序,不能再用本地化模板把日月重新排回月日。 diff --git a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift index f794ccc..5633831 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarSettings.swift @@ -63,6 +63,18 @@ struct CalendarOptionsPopover: View { }), label: feature.title) } } + if feature == .lunar, settings.calendarFeatures.contains(.lunar) { + GroupRow { + SettingRow(title: tr("放大农历文字")) { + DSToggle(isOn: $settings.calendarPreferences.largeLunarText, label: tr("放大农历文字")) + } + } + GroupRow { + SettingRow(title: tr("增强农历对比度")) { + DSToggle(isOn: $settings.calendarPreferences.strongerLunarText, label: tr("增强农历对比度")) + } + } + } if feature == .holidays, settings.calendarFeatures.contains(.holidays) { GroupRow { SettingRow(title: tr("假期倒计时与请假建议")) { @@ -71,22 +83,6 @@ struct CalendarOptionsPopover: View { } } } - GroupRow { - Text(tr("公历始终显示。藏历与回历显示在日期详情中;梅雨天按传统历法推算,并非天气预报。")) - .dsFont(.xs) - .foregroundStyle(DS.Palette.textSecondary) - .fixedSize(horizontal: false, vertical: true) - } - GroupRow { - SettingRow(title: tr("放大农历文字")) { - DSToggle(isOn: $settings.calendarPreferences.largeLunarText, label: tr("放大农历文字")) - } - } - GroupRow { - SettingRow(title: tr("增强农历对比度")) { - DSToggle(isOn: $settings.calendarPreferences.strongerLunarText, label: tr("增强农历对比度")) - } - } } agendaOptions } @@ -111,11 +107,16 @@ struct CalendarOptionsPopover: View { private var presentationOptions: some View { @Bindable var settings = model.settings + let showsLunar = settings.calendarFeatures.contains(.lunar) + let displays: [CalendarPreferences.Display] = showsLunar ? CalendarPreferences.Display.allCases : [.standard, .custom] return SettingsGroup(caption: tr("菜单栏")) { GroupRow(showsDivider: false) { SettingRow(title: tr("显示方式")) { - Picker(tr("显示方式"), selection: $settings.calendarPreferences.display) { - ForEach(CalendarPreferences.Display.allCases) { Text($0.title).tag($0) } + Picker(tr("显示方式"), selection: Binding( + get: { settings.calendarPreferences.display.effective(showsLunar: showsLunar) }, + set: { settings.calendarPreferences.display = $0 } + )) { + ForEach(displays) { Text($0.title).tag($0) } }.labelsHidden().fixedSize() } } @@ -138,7 +139,7 @@ struct CalendarOptionsPopover: View { } } GroupRow { - let preview = settings.calendarPreferences.title(at: Date(), locale: L10n.locale) + let preview = settings.calendarPreferences.title(at: Date(), locale: L10n.locale, showsLunar: showsLunar) HStack { Text(tr("预览")).foregroundStyle(.secondary) Spacer() diff --git a/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift index 9a096e2..ea09826 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/CalendarEnhancementTests.swift @@ -80,6 +80,23 @@ struct CalendarEnhancementTests { #expect(restored.title(at: date, locale: locale, timeZone: zone) == preferences.title(at: date, locale: locale, timeZone: zone)) } + @Test(arguments: [CalendarPreferences.Display.lunar, .dateLunar]) + func disablingLunarDisplayPreservesSavedPresentation(display: CalendarPreferences.Display) throws { + let date = try #require(ISO8601DateFormatter().date(from: "2026-09-28T04:00:00Z")) + let locale = Locale(identifier: "en_US") + let standard = CalendarPreferences().title(at: date, locale: locale, timeZone: zone) + var preferences = CalendarPreferences() + preferences.display = display + preferences.largeLunarText = true + preferences.strongerLunarText = true + let enabled = preferences.title(at: date, locale: locale, timeZone: zone) + #expect(preferences.title(at: date, locale: locale, timeZone: zone, showsLunar: false) == standard) + let restored = try JSONDecoder().decode(CalendarPreferences.self, from: JSONEncoder().encode(preferences)) + #expect(restored.display == display) + #expect(restored.largeLunarText && restored.strongerLunarText) + #expect(restored.title(at: date, locale: locale, timeZone: zone, showsLunar: true) == enabled) + } + @Test(arguments: Array(1...7)) func monthGridAndWidgetHonorEveryWeekStart(firstWeekday: Int) throws { let days = CalendarEngine.month(year: 2026, month: 9, firstWeekday: firstWeekday, timeZone: zone)