Proposal-temporal: Calendar systems for toLocaleString() on Temporal types

Created on 15 Nov 2019  路  27Comments  路  Source: tc39/proposal-temporal

I know there are other threads about calendar systems, like #3 and #31. This thread is specifically about calendar systems when it comes to localized output (toLocaleString).

On regular JavaScript Date, when you call .toLocaleString(), you get the date represented in the local calendar system. I think the intent in the Intl Temporal spec (#129, #169) is for Temporal.Absolute to also use the local calendar system. However, for the other Temporal types, like Temporal.Date and Temporal.MonthDay, we had tentatively recommended that the Gregorian calendar be used to display them.

However, two problems with this approach have been brought to my attention:

  1. An application could use a mix of Temporal types. It would be jarring to the user if different datetime strings were displayed in different calendar systems on the same page.
  2. Around the world, we can't assume that all users understand the Gregorian system, so it would be bad from an i18n perspective if any Gregorian date bubbled up to such a user.

I have different suggestions on how to solve this problem for the different Temporal types.

Temporal.DateTime: Given the time zone, we have enough information to safely convert between calendar systems. We should do that on toLocaleString().

Temporal.Date: The problem here is that calendars might start the day at different instants (e.g. at sunrise or sunset). A potentially reasonable approximation is that we could use a "reference time", like 12:00 noon, for the purposes of formatting a Temporal.Date in the local calendar system. If a user doesn't like our reference time, they can convert to a Temporal.DateTime.

Temporal.YearMonth and Temporal.MonthDay: These are trickier, because they are intrinsically rooted in the Gregorian calendar system. We could (1) do a trick like with Temporal.Date where we choose a reference point, (2) display these in Gregorian, or (3) just not allow localized formatting for these types.

I haven't thought through what to do in the Temporal.Time case yet.

Comments?

@rxaviers @littledan @younies

calendar documentation feedback polyfill spec-text

Most helpful comment

For YearMonth and MonthDay, could we throw an exception if a non-Gregorian calendar is selected?

Do you mean if a non-Gregorian calendar is selected exlicitly or implicitly? For example, ar-SA implies the Islamic calendar, even though it is not explicitly stated. Throwing in this case would not be good behavior because it would make web sites that "work" in some languages but break in others that the developer might not have tested.

All 27 comments

Note regarding Temporal.Date: the ICU4J docs for HebrewCalendar say,

Note: In the traditional Hebrew calendar, days start at sunset. However, in order to keep the time fields in this class synchronized with those of the other calendars and with local clock time, we treat days and months as beginning at midnight, roughly 6 hours after the corresponding sunset.

If helpful, we could cite this as precedent on how to handle Temporal.Date conversion.

This is currently the behavior in regular JavaScript Date (making Hebrew days start at midnight):

new Date(2019, 11, 15, 1, 0, 0).toLocaleString("en-u-ca-hebrew")
// "3/17/5780, 1:00:00 AM"
new Date(2019, 11, 15, 23, 0, 0).toLocaleString("en-u-ca-hebrew")
// "3/17/5780, 11:00:00 PM"

I like the proposed approach for Temporal.DateTime (use an input time zone to make absolute, then localize), and would hope for that to serve as a foundation for all the incomplete types鈥攅.g., fill in missing components to get a date and time of day, then use a time zone to get an absolute time, then localize it using a template ignoring filled-in components. This is a generalization of the "reference time" approach that _should_ work for the cases you identified.

The DateTime and Date plans SGTM. For YearMonth and MonthDay, could we throw an exception if a non-Gregorian calendar is selected?

For YearMonth and MonthDay, could we throw an exception if a non-Gregorian calendar is selected?

Do you mean if a non-Gregorian calendar is selected exlicitly or implicitly? For example, ar-SA implies the Islamic calendar, even though it is not explicitly stated. Throwing in this case would not be good behavior because it would make web sites that "work" in some languages but break in others that the developer might not have tested.

I meant implicitly. Yes, I agree that would be unfortunate; this probably makes that not a very good idea.

Just for clarity: purely for output via toLocaleString

I think that all Temporal types have sufficient information to translate into any other calendar system.

  • a Temporal.Absolute is obvious, because it's a specific moment in absolute time.
  • a Temporal.DateTime for the purposes of output does not need a timezone, since it's considered to be "what the calendar and clock says" which is independent of timezone (remember clocks can stop or be off and calendar leaves can be not flipped over).
  • a Temporal.Date refers to the entirety of the day in question. We won't need a reference point as such, because we can simply go with proportion of coverage. The dates in different calendar systems will overlap to a differing degree, and I'd suggest we simply take the match with the biggest overlap.
  • Temporal.YearMonth and Temporal.MonthDay could follow the same principle as Temporal.Date and simply use the calendar entity with the biggest overlap and therefore highest rate of correctness.

(I'm closing this in 24h unless there is overwhelming resistance, because it's definitely out of scope of an initial Temporal proposal.)

I don't really understand the reason for closing this issue. toLocaleString takes an options bag, and that options bag includes a calendar with https://github.com/tc39/ecma402/pull/175 . We have to determine the semantics one way or another.

Temporal.YearMonth and Temporal.MonthDay could follow the same principle as Temporal.Date and simply use the calendar entity with the biggest overlap and therefore highest rate of correctness.

Can you elaborate on how you would compute "biggest overlap", especially for Temporal.MonthDay?

Since it looks like we're moving forward with a calendar internal slot in Temporal objects, that changes the scope a bit for this issue. Some open questions:

  1. Since both Temporal.Date[Time] and Intl.DateTimeFormat carry a calendar field, who wins?

    1. Temporal calendar wins

    2. Intl.DateTimeFormat calendar wins

    3. Intl.DateTimeFormat calendar wins if the Temporal calendar is "iso"; otherwise the Temporal calendar wins

  2. How about for Temporal.MonthDay and Temporal.YearMonth? These are fundamentally different because we can't perform a calendar conversion.

If we always use the Temporal calendar field in Temporal.MonthDay and Temporal.YearMonth, it seems strange to use the Intl calendar field in Temporal.Date[Time]. Or, maybe that's okay.

Could it throw when the fields are different, encouraging explicit withCalendar usage?

@1: I'd say the Intl.DateTimeFormat would win. The Temporal calendar is used for calculations and internal representations. When using a Intl.DateTimeFormat the explicit purpose is output, so it should win.

@2: Says who? I'd say that depends very much on the calendar. Each calendar can decide if it can do the conversion to ISO or not. If it can, then that's fine. I'd not make this a generic statement of impotence.

On topic 2: Solar calendars might generally be able to map MonthDay to the ISO calendar, but lunar calendars generally won't be able to do that. Built-in lunar calendars would need to throw. Then we get exactly one of the problems we're trying to avoid: exceptions are data- and locale-driven. A programmer testing their app in several solar calendars would not know that their app breaks in a lunar calendar system.

The questions in https://github.com/tc39/proposal-temporal/issues/262#issuecomment-572140644 are still open.

The answers here are related to the default calendar decision, #292.

To add more color here:

Let's say that for Date and DateTime, we use the calendar from Intl.DateTimeFormat, and for YearMonth and MonthDay, we use the calendar from Temporal.

Could that lead to broken code?

For example, if someone wants to format a casual month-day, they could do this:

Temporal.now.date().toLocaleString("en-US", { month: "numeric", day: "numeric" })

However, they could also do:

Temporal.now.date().getMonthDay().toLocaleString("en-US")

In the first case, the local calendar is used. In the second case, the Temporal default calendar is used. This is somewhat related to the question of the Temporal default calendar (#292), because if we go with explicit or partial ISO, then we could force the calendar to be provided in the second call:

Temporal.now.date().getMonthDay(Intl.defaultCalendar).toLocaleString("en-US")

I think most recently we've agreed that it doesn't make that much sense for MonthDay to convert itself to another calendar, so maybe the questions can be reduced to:

  • would we be OK with "Intl wins" Date/DateTime on the one hand and "Temporal wins" for YearMonth/MonthDay on the other?

Pending the decision on #292 (and related issues like #569), my preference would be something along the lines of: Intl wins when calendar == ISO, and Temporal wins when calendar != ISO.

Meeting, May 21: We'll initially ship the polyfill with: the Temporal object's calendar takes precedence if it is not ISO, and otherwise the Intl calendar takes precedence. In addition, we'll revisit this before Stage 3.

While implementing this in the polyfill I ran into the question, what about formatRange and formatRangeToParts? Carrying forward the reasoning above seems fine, but that leaves the case where the two Temporal objects have different, non-ISO calendars.

I think the best choice for the polyfill at this point might be to throw in that case, then we will more easily get feedback on whether people want to do that.

I would throw on formatRange if the types differ. I don't think that would be controversial.

If the types are the same but the calendars differ among the formatRange arguments... probably throw in that case, too.

Agreed on throwing being a good idea for dissimilar calendars.

This is implemented in the polyfill, and what's left to do is to revisit it based on feedback. So I'll unassign myself and mark it "feedback"

We still haven't resolved how to solve the bug in https://github.com/tc39/proposal-temporal/issues/262#issuecomment-617359373:

// Bad, but concise:
Temporal.now.date().getMonthDay().toLocaleString("fa-IR")

// Good, but verbose:
Temporal.now.date().toLocaleString("fa-IR", { month: "numeric", day: "numeric" })

In the absence of an explicit calendar, I think the only way to solve the issue above is to make Temporal.MonthDay.prototype.toLocaleString and Temporal.YearMonth.prototype.toLocaleString throw if the calendar is ISO.

I believe that issue should already be solved in the status quo:

> Temporal.now.date().toYearMonth().toLocaleString('en-CA-u-ca-japanese')
'2-09'

(I'm using a locale with the calendar supplied via an extension key because we don't have any calendars implemented yet that are the default for any locale; we only have the Japanese calendar, and the Gregorian calendar is the default for ja-JP according to new Intl.DateTimeFormat('ja-JP').resolvedOptions(). I don't have any reason to believe it would work differently if the Persian calendar were implemented and the locale was fa-IR though.)

Let's break that apart:

const ym = Temporal.now.date().toYearMonth();

Barring #292, ym represents September 2020 in the ISO calendar. Agreed?

> ym.toLocaleString('en-CA-u-ca-japanese')
'2-09'

Presumably your polyfill code sees the ISO calendar and swaps it for the Japanese calendar. It happens to be that since the Japanese months and ISO months are aligned, you get a valid "September Reiwa 2" representation out on the other side, which you then render.

However, months in lunar and lunisolar calendars are not aligned with ISO. Therefore, you can't just blindly convert a YearMonth between calendar systems like you can with Japanese and ISO.

Conclusion based on the 2020-09-04 champions meeting:

  • There are two places to get a calendar: from the Temporal slot, and from the locale. By ECMA-402 convention, there is also a calendar option for .toLocaleString that overrides the locale's calendar.
  • Algorithm for .toLocaleString calendar resolution:

    1. If the Temporal calendar == the locale calendar, return that calendar.

    2. If the Temporal calendar == iso8601 AND the Temporal type has a .withCalendar() method, return the locale's calendar.

    3. Else, throw an exception. The exception message should be educational.

This means that .toLocaleString should (almost) always work on the fully-specified Temporal.Date, Temporal.DateTime, and Temporal.LocalDateTime types. However, it (almost) always throws on Temporal.MonthDay and Temporal.YearMonth, until a calendar has been specified in one of the following two places:

  1. When constructing your Temporal object, or
  2. In the .toLocaleString method.

Code examples:

// Use locale's calendar (with hypothetical Intl.defaultCalendar)
Temporal.now.yearMonth().withCalendar(Intl.defaultCalendar).toLocaleString();

// Use yearMonth's calendar
ym.toLocaleString(undefined, { calendar: ym.calendar });
Was this page helpful?
0 / 5 - 0 ratings