Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intl support for Temporal proposal #129

Closed
sffc opened this issue Apr 4, 2019 · 11 comments
Closed

Intl support for Temporal proposal #129

sffc opened this issue Apr 4, 2019 · 11 comments
Labels
behavior Relating to behavior defined in the proposal integration
Milestone

Comments

@sffc
Copy link
Collaborator

sffc commented Apr 4, 2019

We would like to add support for Temporal toLocaleString as well as formatting of relevant Temporal objects in Intl.DateTimeFormat before Temporal reaches Stage 3. Use this thread to track these discussions.

I will shortly post notes from a meeting we had last week where we came up with some initial ideas on how to design this API.

@sffc
Copy link
Collaborator Author

sffc commented Apr 5, 2019

Here are partial notes from a ~2hr brainstorming session with @pipobscure, myself (@sffc), @gibson042, @fabalbon, and @littledan in New York after TC39. The rest of this message contains a digest of our conclusions.


First, the following Temporal types are the ones that make sense to be formatted.

  • CivilDate
  • CivilTime
  • CivilDateTime
  • ZonedDateTime
  • (possibly others like YearMonth and MonthDate, but I can't find those in the README)

We considered adding separate Intl objects for each type: Intl.CivilDateFormat, Intl.CivilTimeFormat, etc. We preferred not to go down this route because:

  1. A lot of different objects to add to the namespace
  2. In principle, the above Temporal objects are representations of dates and times, so it makes sense that they should be able to use Intl.DateTimeFormat
  3. Unified NumberFormat as well as formatRange set precedent of adding related features to the existing Intl constructor instead of adding a new constructor

The next question was whether to overload Intl.DateTimeFormat.prototype.format or to add new methods like Intl.DateTimeFormat.prototype.formatCivilDate. Given that TC39 opted to prefer overloading of Intl.NumberFormat.prototype.format for BigInt, it makes sense to follow that precedent and perform overloading of Intl.DateTimeFormat.prototype.format for Temporal types.

The behavior in the default case makes sense. For example:

let cd = new CivilDate(2018, 5, 3);
let dtf = new Intl.DateTimeFormat("bn");
console.log(dtf.format(cd));  // ৩/৫/২০১৮

The tricky part is when you pass settings to Intl.DateTimeFormat that don't make sense for the given Temporal type. For example, what should happen when you run the following code?

let cd = new CivilDate(2018, 5, 3);
let dtf = new Intl.DateTimeFormat("fr", {
  hour: "2-digit"
});
console.log(dtf.format(cd));

We considered four options:

  1. Return "03/05/2018", as in the default case
  2. Return "00 h", treating the hour field as if it were zero
  3. Return the empty string
  4. Throw an exception

No one really liked options 1-3 that much, but it took a while to come up with a sensible condition on which to throw the exception. To explain that condition, let me explain how we go about implementing the common case in the spec.

Intl.DateTimeFormat currently has an internal slot named [[Pattern]]. The pattern is a locale-sensitive template used to format Date (window.Date, not Temporal) objects. We proposed adding new internal pattern slots, one for each type of Temporal object.

  • [[CivilDatePattern]]
  • [[CivilTimePattern]]
  • [[CivilDateTimePattern]]
  • [[ZonedDateTimePattern]], which we could omit in favor of using [[Pattern]] for ZonedDateTime.

Here's the key part: the way each of those patterns get populated is by taking the subset of skeleton keys (input keys) that are needed for each Civil type, as shown below.

Input Property (Skeleton) CivilDate CivilTime CivilDateTime ZonedDateTime
"weekday" ? ? ?
"era" ? ? ?
"year"
"month"
"day"
"hour"
"minute"
"second"
"timeZoneName"

If the subset is empty, then set the corresponding internal slot to null. For example, if the user gives the option bag { hour: "2-digit" }, there are no fields available in the CivilDate subset, so[[CivilDatePattern]] gets set to null. Then, when .format gets called with a CivilDate, if [[CivilDatePattern]] is null, you throw an exception.

Additional note: for formatRange, @fabalbon said that it should be possible to merge the changes required to Temporal with that proposal with somewhat minimal effort.

@sffc sffc mentioned this issue Apr 5, 2019
16 tasks
@sffc
Copy link
Collaborator Author

sffc commented Apr 5, 2019

@littledan said:

Temporal should have built-in support for Intl. @sffc, @fabalbon, @gibson042 @pipobscure and I discussed including this of part of Intl.DateTimeFormat at length, but later, @domenic, pointed out that this isn't really compatible with the idea of lazy-loading built-in modules. So, now I'm thinking that this would make more sense as a separate formatter, e.g., the export from std:temporal could be called IntlTemporalFormat, and work generally the same as Intl.DateTimeFormat (even share options processing spec text, except for ignoring the timeZone). As we discussed previously, the idea of having a single IntlTemporalFormat instance supporting several different Temporal data types seems right. Internally, there would be one pattern per Temporal type that could be formatted. The types that are supported would initially be CivilYearMonth, CivilMonthDay, CivilDate, CivilTime, CivilDateTime, and ZonedDateTime.

I think I like IntlTemporalFormat better than separate Intl.CivilDateFormat, Intl.CivilTimeFormat, etc., but I have some questions:

  1. All Intl logic is supposed to be in ECMA 402. Is the idea that IntlTemporalFormat be specified in ECMA 402? How does that work?
  2. I don't really understand "this isn't really compatible with the idea of lazy-loading built-in modules". Why can't the spec specify behavior for an object type that hasn't been imported yet? The lazy initialization of the internal slots can be implementation-defined. Why does that affect the spec?
  3. It would be a major precedent setter to expose the Intl libraries in the std:temporal built-in module namespace. Right now, all Intl logic is nicely encapsulated in the Intl module. Implementations can leave out the whole Intl object and still be ECMA 262 compliant. In the future, we want Intl.MessageFormat to interoperate with the Intl formatters. It seems messy to me to pull Intl support out into each individual built-in module.
  4. Related to 3: Issue Data-Driven API ecma402#210 may provide a way to override locale data on a formatter level or an Intl level. In other words, it makes the under-the-hood logic for Intl objects interconnected. Does it make sense that a call to a hypothetical API like Intl.setDataSource() affect a separate imported module?
  5. This goes back to what I had already posted above, but all formatters so far are designed around a type of thing to be formatted. Intl.NumberFormat formats numbers, currencies, and units. Intl.DateTimeFormat formats dates, times, and datetime ranges. It makes intuitive sense that
    Temporal types should be compatible with vanilla Intl.DateTimeFormat, not some other workaround that we make for it.
  6. If Intl.DateTimeFormat is a first-class citizen, but IntlTemporalFormat is not (having to be imported), it may be less discoverable, leading to less adoption of i18n best practices on the web.

(edited with additional line items)

@domenic
Copy link
Member

domenic commented Apr 5, 2019

To give some more background, Chrome believes that built-in modules should be truly modularized: they should not have cross-cutting dependencies or interactions with built-in globals, or other modules. This allows the code for implementing a built-in module to not interact with any of the code for those other features, and e.g. then be lazy-loaded only into realms that need it.

So, if types in the std:temporal module have cross-cutting concerns with the pre-existing Intl global, then we have two options:

  • Work on an isolation strategy, so that the new behaviors and types are isolated to the std:temporal module and do not affect the behavior or contents of the Intl global, or
  • Abandon the built-in module idea for temporal, and put it onto the big mess-of-interconnected-things global namespace where it can be intertwingled with existing things in that mess.

I prefer the former; I think clean modularization boundaries is a big improvement for ECMAScript standard library features.

On your specific questions:

  1. I think it's pretty easy to specify extensions to built-in modules across two specs. @littledan and @Ms2ger have been working on specific mechanisms for this in Define Web IDL Modules whatwg/webidl#675 (you would use a partial module temporal declaration).

  2. Explained above.

  3. Indeed this would be a departure from existing precedent. This is a classic "consistency vs. goodness" tradeoff. (Goodness being modularization, in this case.) As for your usage of terms like "messy", I think that's subjective, and IMO the existing lumping of everything into overloads on a single large global object is pretty messy.

  4. No, it does not make sense; if temporal is to be modularized, it should not be affected by global state setters.

  5. I think this depends on your definition of "type". Clearly you don't mean ECMAScript type, because e.g. BigInt and number are different types but they both end up in NumberFormat. But, are Date and CivilTime really the same "type"? I would argue no; the whole point of this proposal is that the temporal types have radically different data models, and should not be considered part of the "Date" type. In particular, from what I understand there's already a mismatch between Intl.DateTimeFormat and the temporal types with regard to timezones, which makes creating a new type better suited for temporal a much more attractive option than attempting to patch over that mismatch.

  6. I think this same argument applies for all of temporal. If you believe built-in modules are second-class citizens, then you should perhaps be having a larger discussion, either on this repository (to advocate for temporal itself becoming "first class") or in the JavaScript standard library proposal (to argue against built-in modules existing at all). To my mind, built-in modules are not second-class, and it's imperative that the formatting mechanisms for temporal and temporal itself to use the same importing system (whether that be built-in modules, as I prefer, or built-in globals, as you seem to be advocating).

@pipobscure
Copy link
Collaborator

I agee with @domenic

there is no reason std:temporal cannot deliver a district TemporalFormat that delivers this functionality. So long as it keeps the same API as DateFormat that could work well, be clean & consistent.

I’ll be updating the spec soonish. Definitely in time for it to have been discussed online by June.

@littledan
Copy link
Member

It's really great to have all of this written up, @sffc. Whether we do this within Intl.DateTimeFormat or a new built-in module is more of a question of what we want from built-in modules, rather than what's practical to specify or implement (both browser implementations and polyfills--polyfills can easily monkey-patch Intl.DateTimeFormat).

With respect to the points in #129 (comment), what sorts of patterns should we have in built-in modules going forward in cases like this? Is this a good chance to set a new default, or is the overriding concern ergonomics and consistency with existing functionality? cc @domenic @tschneidereit @annevk @Ms2ger @msaboff @mattijs

@sffc
Copy link
Collaborator Author

sffc commented Apr 5, 2019

Thanks for the explanations.

To give some more background, Chrome believes that built-in modules should be truly modularized: they should not have cross-cutting dependencies or interactions with built-in globals, or other modules. This allows the code for implementing a built-in module to not interact with any of the code for those other features, and e.g. then be lazy-loaded only into realms that need it.

I don't see how this prevents the act of importing std:temporal to enable the required overloads to Intl.DateTimeFormat.prototype.format. The implementation of the function overloads for Temporal types need not be loaded until code tries to call them (e.g., using something like dynamic import).

@domenic
Copy link
Member

domenic commented Apr 5, 2019

Changing the behavior of a global object upon importing a module is the kind of anti-modular side effects we would like to avoid.

Also note that dynamic import is async and format() is not.

@sffc
Copy link
Collaborator Author

sffc commented May 16, 2019

Let's say that in a world where Temporal is a built-in module, then the i18n support for Temporal should also be in its own built-in module. That seems reasonable.

Moving in this direction with Temporal i18n would set strong precedent for what Intl would look like in the future. Intl-as-a-module is a very interesting and important design space. I would be a bit uneasy moving forward in this direction without investing the time to design a proper module system for Intl as a whole, or else we may set precedent that we may regret later.

To be clear, though, as I have stated before, I do not want to see Temporal reach Stage 3 until we have an i18n solution for it. Not only does it send the wrong message if we don't make i18n a first-class citizen, but the ecosystem will induce technical debt in the form of workarounds to add that functionality, potentially in ways that we don't want them to do.

An expedient option would be to introduce Temporal as a global object as originally proposed. I understand that Temporal could be an opportunity to move the needle on built-in modules. However, I am not convinced that this proposal is the right way to move the needle.

I could easily see an effort in the not-so-distant future in which TC39 comes together and makes a runtime-wide effort to modularize the globals in JavaScript: not just Temporal but also Intl, Map/Set, and so forth. We could add syntax along the same vein as ES6 "use strict"; to enable the lightweight JavaScript runtime that loads built-in modules as needed, for example. In this way, we can produce a more cohesive module system, and we don't risk making decisions now about the form of the Temporal and Intl modules that we may regret later.

In the end, assuming we see a future for ourselves in which we convert existing globals to built-in modules, I do not see any major downsides of adding Temporal today as a new global in ECMAScript. It could potentially be the last one we add.

@littledan
Copy link
Member

littledan commented May 16, 2019

The get-originals proposal provides a path towards modularizing globals; however, it looks a bit different from some of the drafts for new built-in modules, like kv-storage in the sense that it uses the global/ prefix. Overall, some of us are thinking about modules being a place to set "new defaults". At a high level, I think we have to make a tradeoff/balance between everything being exactly consistent with the past, and improving things over time.

@ljharb
Copy link
Member

ljharb commented May 16, 2019

The trick there is getting consensus on what constitutes “improvement”, and often that will mean ensuring consistency.

@ryzokuken
Copy link
Member

@littledan @sffc I guess this can be closed now that we've explored all avenues of interaction with 402 (including more specific issues and the ongoing work on Intl.DurationFormat)? Please feel free reopen if I am mistaken.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
behavior Relating to behavior defined in the proposal integration
Projects
None yet
Development

No branches or pull requests

6 participants