Skip to content

feat: introduce lunar calendar support#1075

Open
liWanr wants to merge 2 commits intoTheBoredTeam:devfrom
liWanr:my-feature
Open

feat: introduce lunar calendar support#1075
liWanr wants to merge 2 commits intoTheBoredTeam:devfrom
liWanr:my-feature

Conversation

@liWanr
Copy link

@liWanr liWanr commented Mar 1, 2026

🔧Settings UI

  • Added "Calendar subtitle display" option with three modes:

    • Always default: Keeps the original year display (e.g., "2026").
    • Always alternate: Permanently displays the alternate calendar.
    • Tap to switch: Allows users to toggle between the year and the alternate calendar by tapping the subtitle.
  • Added conditional "Alternate Calendar" Picker: This picker appears only when "Always alternate" or "Tap to switch" is selected.

    Note on Localization: Currently, this feature only supports the Lunar Calendar. Since there is no standard or intuitive way to express Lunar dates in non-Chinese languages (e.g., it defaults to strings with ambiguous meanings like mo1 14 in English), therefore the Lunar display is locked to Simplified Chinese to ensure clarity and accuracy.

📅Calendar View

  • Layout Optimization: Implemented a fixed-width hidden view strategy using ZStack. This ensures the UI remains rock-solid and prevents any layout "jitter" or shifting when toggling between the year and Lunar text.
  • State Persistence: The toggle state (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

  • Added localization objects for all new settings in Localizable.xcstrings.
  • Note: I have not added translations for non-default languages (including Simplified Chinese, which I am most familiar with) yet, as I believe all translation work should be officially handled via Crowdin to maintain project standards.

📷Screenshots

image image image image image

.font(.title3)
.fontWeight(.light)
.foregroundColor(Color(white: 0.65))
if Locale.current.identifier.hasPrefix("zh") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I add the localization object myself, or will you be handling that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Author

@liWanr liWanr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +45 to +62
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
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants