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

feat: Support ISO 8601 date strings with full precision for all formatting functions where dates can be passed #758

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

martinmunillas
Copy link
Contributor

@martinmunillas martinmunillas commented Jan 5, 2024

This PR adds support to format ISO 8601 directly, this is super useful for date strings coming directly from the backend, it makes sense as it is the official format accepted in js.

As of right now I find myself doing format.dateTime(new Date(str), format) which is pretty annoying.

I'm not sure if my changes are enough but I assume it should work, please provide any feedback.

Example

import {useFormatter} from 'next-intl';

function BlogPost({post}) {
  const format = useFormatter();

  return (
    <article>
      <h1>{post.title}</h1>
      <time datetime={post.publishedAt}>
        {format.dateTime(post.publishedAt)}
      </time>
    </article>
  );
}

Todo

  • Date formatting
  • Date range formatting
  • Relative date formatting
  • Dev-only validation

Copy link

vercel bot commented Jan 5, 2024

@martinmunillas is attempting to deploy a commit to the next-intl Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

vercel bot commented Jan 5, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
next-intl-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 21, 2024 7:15pm
next-intl-example-app-router ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 21, 2024 7:15pm

Copy link
Owner

@amannn amannn left a comment

Choose a reason for hiding this comment

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

Thanks for getting the ball rolling here! I currently have some concern about this and would like the leave this open for now, please see the inline comment.

packages/use-intl/src/core/createFormatter.tsx Outdated Show resolved Hide resolved
@@ -5,7 +5,8 @@ export enum IntlErrorCode {
INSUFFICIENT_PATH = 'INSUFFICIENT_PATH',
INVALID_MESSAGE = 'INVALID_MESSAGE',
INVALID_KEY = 'INVALID_KEY',
FORMATTING_ERROR = 'FORMATTING_ERROR'
INVALID_FORMAT = 'INVALID_FORMAT',
Copy link
Owner

Choose a reason for hiding this comment

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

I think the existing FORMATTING_ERROR would do for this and avoids increasing the bundle size more than necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought so at first but then I thought FORMATTING_ERROR was a formatting error but not an error because of the format while parsing. but if it still makes sense to you I'll revert it

@@ -153,6 +153,22 @@ export default function createFormatter({
}
}

if (typeof value === 'string') {
const str = value;
value = new Date(value);
Copy link
Owner

@amannn amannn Jan 10, 2024

Choose a reason for hiding this comment

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

There are two problems that I know of with date strings:

1) Optional parts of date strings

Various components can be omitted, so the following are all valid:

  • Date-only form: YYYY, YYYY-MM, YYYY-MM-DD
  • Date-time form: one of the above date-only forms, followed by T, followed by HH:mm, HH:mm:ss, or HH:mm:ss.sss. Each combination can be followed by a time zone offset.

(source)

Omitting the time or the time zone part in the input is problematic because by calling the constructor, a time and time zone are implied and the user can format them:

// E.g. "1:00:00 AM Central European Standard Time"
formatter.dateTime('2020-11-20', {timeStyle: 'full'})

The time zone is especially tricky due to:

When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as local time. This is due to a historical spec error that was not consistent with ISO 8601 but could not be changed due to web compatibility. See Broken Parser – A Web Reality Issue.

Here's an interesting case for you that can be run in the test suite of this repo (uses TZ=Europe/Berlin in the Node.js environment when running a test):

const formatter = createFormatter({locale: 'en'});
expect(
  formatter.dateTime('2020-11-20', {
    dateStyle: 'medium',
    timeZone: 'America/New_York'
  })
).toBe('Nov 20, 2020'); // ❌ Result: Nov 19, 2020

Fun, right? The reason is that the UTC time zone is assumed and the explicitly provided time zone moves the created date the the previous date.

2) Using non-standard date strings

The date constructor also accepts non-standard date strings with varying browser support.


Maybe due to this mess Intl.DateTimeFormat.prototype.format() only accepts Date | number. Hopefully Temporal will become the better alternative at some point in the future.

Don't get me wrong, I absolutely see your point and am trying to optimize for convenience with next-intl. By moving the date parsing into userland, we currently at least make it obvious how a string is turned into a date. Some users might prefer to lint against this even.

I think to move forward with this, we'd have to validate that the incoming date string conforms to ISO 8601 and includes all parts (year, month, date, hour, minute, seconds, milliseconds, timezone). That however will increase the bundle size slightly.

I'm honestly not confident about this change currently. Especially Intl.DateTimeFormat.prototype.format() not accepting strings makes me suspicious, maybe there are other problems I don't know about yet.

I think as an immediate todo, the next-intl docs should explain these problems in more detail in the Formatting dates and times docs. Currently, there's an expandable section "How can I parse dates or manipulate them?" there. I think it should be split in two and details about parsing should be included.

I'm not saying this will never make it in, but I'd say we should leave the PR open for a bit, give it more thought and maybe consider opinions from other users in case others chime in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand point 1 but I don't see how that is any different to what exists now, if i create a date with new Date(2020, 11, 20) and format that, the error is the same, is not an issue of string parsing but on how the date object is created, if the date doesn't contain a

About point 2, yes, there is not much to argue there, js Dates are a mess, and there could be possibles bugs as everywhere, but this doesn't change anything for users use other parsing mechanisms, only adds an easier way for users that do use the new Date(str) one. I wouldn't also mind to include other date parser if necessary or change the error message and documentation if necessary.

I still understand your concerns and appreciate your feedback! I'm just think it would be super useful to directly accept strings

Copy link
Owner

Choose a reason for hiding this comment

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

if i create a date with new Date(2020, 11, 20) and format that, the error is the same

That's true. To me, the difference is that this is in userland and hopefully caught in review (or by a linter).

If the user calls format.dateTime('2020-11-20', {dateStyle: 'medium'}) (from my example above) this looks correct. Even the output "Nov 19, 2020" looks correct—but is off by a day. In this simplified example it's easy to spot but maybe less so in a complex app.

The possibility I see here is validating at runtime that a full ISO 8601 string is passed. I'm unsure if dev-only error handling is a good idea here, and I'd like to avoid increasing the bundle size due to this. We currently don't have this issue if the user calls new Date in the app code. Furthermore, this gives the user the chance to include runtime validation code if it's relevant for the app.

I think for the time being I'll wait with this and instead improve the docs on parsing dates. I hope you can understand.

Copy link
Owner

@amannn amannn Jan 29, 2024

Choose a reason for hiding this comment

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

A closely related note from the recently released date-fns@3:

Screenshot 2024-01-29 at 10 18 34

(https://blog.date-fns.org/v3-is-out/)

Example:

Screenshot 2024-01-29 at 10 19 15

Might be worth keeping an eye on this, maybe it works out well for date-fns or there are learnings for next-intl here.

For reference, here's a blog post on a 2.0 prerelease for date-fns with more background on string parsing.

This is the implementation of toDate from date-fns that is used for normalizing dates internally.

Copy link
Owner

Choose a reason for hiding this comment

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

Another related update: Tempo was just released and they support receiving dates as strings. They include runtime validation that will throw in case an invalid date string is encountered. I've asked the library author for some background on this decision.

Maybe in the end we should really support this pattern. I'd still consider doing the runtime validation only during development to avoid increasing the bundle size.

Copy link
Owner

@amannn amannn Feb 19, 2024

Choose a reason for hiding this comment

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

FYI, I just saw that zod has built-in ISO 8601 parsing. They reference a StackOverflow answer which seems like it could serve us well here: https://stackoverflow.com/a/3143231/343045 (the "complete precision" variant).

We should be sure that we have good test coverage on which strings will print the warning.

Copy link
Owner

@amannn amannn Feb 21, 2024

Choose a reason for hiding this comment

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

On a second thought, maybe we don't need to get too elaborative with docs and explain all the problems with dates in JavaScript.

Maybe it'd be sufficient to not add the troubleshooting section and use this for the error message:

Invalid ISO 8601 date string received: ${value}. Note that all parts of ISO 8601 are required: year, month, date, hour, minute, seconds, milliseconds and the timezone (e.g. '2024-02-21T07:11:36.398Z').

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @amannn sorry for leaving this a little abandoned, I haven't really had time to get my hands on this again. I'm not sure when I'll be able to, so anyone feel free to take over if they have time, if not I'll finish this, I just don't know when.

Copy link
Owner

Choose a reason for hiding this comment

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

I'll look into this!

Copy link
Owner

@amannn amannn May 22, 2024

Choose a reason for hiding this comment

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

I think the implementation is good now. One tradeoff I noticed: Dates in messages are required to be actual dates, strings will lead to a formatting error. So there's some slight divergence there.

Potentially with #705 we'd have more control over this in the future and could enable string dates in messages too.

Need to decide between:

  1. Support this silently for the time being
  2. Suggest this in the docs (potentially with people being confusing why they can't use date strings with t)
  3. Wait for feat: AOT compilation with icu-to-json (experiment) #705
  4. Ask for upstream support in intl-messageformat

packages/use-intl/test/core/createFormatter.test.tsx Outdated Show resolved Hide resolved
packages/use-intl/src/core/createFormatter.tsx Outdated Show resolved Hide resolved
…ring-support

# Conflicts:
#	packages/use-intl/src/core/createFormatter.tsx
#	packages/use-intl/test/core/createFormatter.test.tsx
@amannn amannn changed the title feat: support format date strings feat: Support ISO 8601 date strings with full precision for all formatting functions where dates can be passed May 21, 2024
@amannn amannn removed the contributions welcome Good for people looking to contribute label May 22, 2024
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