feat: introduce lunar calendar support#1075
Conversation
| .font(.title3) | ||
| .fontWeight(.light) | ||
| .foregroundColor(Color(white: 0.65)) | ||
| if Locale.current.identifier.hasPrefix("zh") { |
There was a problem hiding this comment.
Make this a separate setting rather than tying the calendar directly to the locale. These should be decoupled, as users may want to choose their calendar independently of their language, especially since both the Gregorian and Chinese calendars are commonly used in China.
Ideally, we could support a primary calendar along with an optional alternate calendar, similar to Apple Calendar, but idk how to present both clearly within the limited available space.
There was a problem hiding this comment.
You’re absolutely right. To address the space constraint, I have a simple idea: we could add a setting that lets users choose between a fixed display or a clickable switch. Basically, when the user taps the date (e.g., "2026"), it swaps the content to the Lunar date, and vice versa.
I’m not sure if this approach is feasible, what do you think?
There was a problem hiding this comment.
I like it, looking forward to seeing what you come up with! You could have a picker with the options something like: [Always Default] [Always Alternate] [Tap to switch]
There was a problem hiding this comment.
Should I add the localization object myself, or will you be handling that?
There was a problem hiding this comment.
For the settings? you should do it. Any Text views you add will automatically manage the localization. For the picker, you will need to manage it similarly to the existing ones
liWanr
left a comment
There was a problem hiding this comment.
I've implemented the changes based on your suggestions—you can check out the latest commits! Let me know if everything looks good or if you’d like any further adjustments.
| } | ||
|
|
||
| enum AlternativeCalendarType: String, CaseIterable, Identifiable, Defaults.Serializable { | ||
| case lunar = "Lunar" |
There was a problem hiding this comment.
Change 'lunar' to 'Chinese.' The Chinese calendar is actually lunisolar, and 'lunar' is too ambiguous since several other calendars use the lunar or lunisolar system.
| struct LunarDateStyle: FormatStyle { | ||
| func format(_ value: Date) -> String { | ||
| let formatter = DateFormatter() | ||
| formatter.calendar = Foundation.Calendar(identifier: .chinese) | ||
| formatter.locale = Locale(identifier: "zh_CN") | ||
| formatter.dateStyle = .long | ||
| formatter.timeStyle = .none | ||
|
|
||
| let fullDate = formatter.string(from: value) | ||
|
|
||
| if let yearRange = fullDate.range(of: "年") { | ||
| let extracted = String(fullDate[yearRange.upperBound...]) | ||
| return extracted | ||
| } | ||
|
|
||
| return fullDate | ||
| } | ||
| } |
There was a problem hiding this comment.
I think we should avoid recreating this every time. I added a suggestion below that makes the formatter static and also should handle the formatting in a cleaner way.
| struct LunarDateStyle: FormatStyle { | |
| func format(_ value: Date) -> String { | |
| let formatter = DateFormatter() | |
| formatter.calendar = Foundation.Calendar(identifier: .chinese) | |
| formatter.locale = Locale(identifier: "zh_CN") | |
| formatter.dateStyle = .long | |
| formatter.timeStyle = .none | |
| let fullDate = formatter.string(from: value) | |
| if let yearRange = fullDate.range(of: "年") { | |
| let extracted = String(fullDate[yearRange.upperBound...]) | |
| return extracted | |
| } | |
| return fullDate | |
| } | |
| } | |
| struct LunarDateStyle: FormatStyle { | |
| private static let formatter: DateFormatter = { | |
| let f = DateFormatter() | |
| f.calendar = Calendar(identifier: .chinese) | |
| f.locale = Locale(identifier: "zh_CN") | |
| f.dateStyle = .long | |
| f.dateFormat = "MMMMd" | |
| return f | |
| }() | |
| func format(_ value: Date) -> String { | |
| Self.formatter.string(from: value) | |
| } | |
| } |
There was a problem hiding this comment.
This view should be refactored. It shouldn't have the hidden view for sizing in a ZStack and it should probably use the .headline font for lunar to keep its characters a similar size to the year.
🔧Settings UI
Added "Calendar subtitle display" option with three modes:
Added conditional "Alternate Calendar" Picker: This picker appears only when "Always alternate" or "Tap to switch" is selected.
📅Calendar View
ZStack. This ensures the UI remains rock-solid and prevents any layout "jitter" or shifting when toggling between the year and Lunar text.calendarIsShowingAlternate) is now persisted. Even after closing the window or restarting the app, it will remember the last selection in "Tap to switch" mode.📃Localization
Localizable.xcstrings.📷Screenshots