amannn / next-intl

🌐 Internationalization (i18n) for Next.js
https://next-intl-docs.vercel.app
MIT License
2.37k stars 214 forks source link

feat: Type-safe global `formats` #1346

Closed dBianchii closed 4 days ago

dBianchii commented 1 week ago

This PR adds a new configuration called IntlFormats, where users can link their global formatting to this interface, and allow for global formats typesafety to work across their app.

Closes https://github.com/amannn/next-intl/issues/1112

vercel[bot] commented 1 week ago

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 Sep 24, 2024 10:39am
next-intl-example-app-router ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 24, 2024 10:39am
next-intl-example-app-router-without-i18n-routing ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 24, 2024 10:39am
vercel[bot] commented 1 week ago

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

A member of the Team first needs to authorize it.

dBianchii commented 1 week ago

Hi @ amann/maintainers

You said in here that the package supports full, long, medium and short. I believe that this only applies for dates and times within messages, yes? In that comment, did you suggest to make those the defaults as well for the formatter?

You also said:

For number formatting there are defaults for currency and percent.

I haven't found this logic in the code, and also in my tests when I do format.number(5, "percent") or format.number(5, "currency"), nothing happens

Could you explain a bit more of what you meant?

EDIT: Looks like that the percent and currency are available for formatting within messages as well! Got it. In that case, I just need to understand how you think that these should relate to the formatter api.

dBianchii commented 1 week ago

There is one problem with this right now:

I see that the global format api has been designed to be able to handle global formats for client and server differently/separately. (We must provide formats to both getRequestConfig and NextIntlClientProvider component. Each can take in different global formats. ) I don't really understand why it is this way, because in my opinion it would be better if we could just use the same global formats everywhere (client and server). The way I set this up makes it impossible to separate both types, and the user can only provide one type as IntlFormats.

How can we resolve this problem?

amannn commented 1 week ago

it would be better if we could just use the same global formats everywhere (client and server)

Yep, I agree!

The discussion is related to the one about messages: Messages are defined globally, but it's the user's job to provide the relevant messages where relevant. This is to allow the user to optionally pass only certain messages to the client side to optimize bundle size, which is also the reason why the messages need to be passed explicitly to NextIntlClientProvider.

The current state of the library has the same mechanism for formats: In order to potentially reduce bundle size, formats aren't automatically inherited and need to be passed explicitly to NextIntlClientProvider. I've changed my mind in the meantime about this though: Out-of-the-box formats should be inherited automatically, and therefore the user doesn't have to worry. You could still opt-out with formats={null}. This change is already implemented on the v4 branch (see https://github.com/amannn/next-intl/pull/1191), but the release is still a bit further down the road.

That being said, I think it's fine to model IntlFormats the same as the global IntlMessages, especially with the change in v4 coming up.

Hope that sounds reasonable to you!

dBianchii commented 1 week ago

That's awesome to hear. Makes total sense to me. I'll continue with this decision in mind. Thanks!

dBianchii commented 1 week ago

Ok, so I added a few tests to app-router-playground. Please let me know if I should add more. As I mentioned in the comment, I am only testing an environment that has the global IntlFormat type declared. Maybe if you'd like I could do it as well in another workspace.

Please let me know if you think that this implementation is good enough, or if there could be something missing/edge cases. Whenever you think all is good, I'll go ahead and start working on the docs. I have to say I'm very impressed with the documentation on this project. I'll do my best to follow suit and try to explain things in the best manner possible.

amannn commented 1 week ago

Looks awesome, thanks a lot! I had another look over the changed files and cleaned up a few minor things along the way and added some more tests for the case when no strict formats are supplied: https://github.com/amannn/next-intl/pull/1346/commits/3fa91e89cdafda17303416660329f6ec528238a3. Hope that's ok for you!

I think the implementation is solid now! 💯

You wanna give the docs a shot? 🙂 Means a lot to me if you like them! 😊

dBianchii commented 1 week ago

Ready to go. Please check though: I don't know why but I am getting some problems running pnpm i for this branch and also main branch.

image

I noticed I accidentally pushed a bunch pnpm lockfile changes, so I tried to use the correct pnpm version to revert it but no luck in running the installer.

Feel free to make any further changes

dBianchii commented 5 days ago

I was thinking of moving this section to the bottom of the page, so it talks about both cases. But for now I just left it like that because I think that moving it all the way down would require creating another heading which I don't want to do

amannn commented 4 days ago

I did another final test in example-app-router-playground and found that interestingly, while type checking was strict and errors were reported where expected, auto-complete of TypeScript was broken.

I've experimented with a few options with this test suite:

/* eslint-disable @typescript-eslint/no-unused-vars */

const formats = {
  dateTime: {
    medium: {
      dateStyle: 'medium',
      timeStyle: 'short',
      hour12: false
    }
  }
};

type Formats = typeof formats;
interface IntlFormats extends Formats {}

type T1 = keyof IntlFormats['dateTime'] extends string
  ? keyof IntlFormats['dateTime'] | Intl.DateTimeFormatOptions
  : string | Intl.DateTimeFormatOptions;
type T2 = keyof IntlFormats['dateTime'] | Intl.DateTimeFormatOptions;
type T3 =
  | Extract<keyof IntlFormats['dateTime'], string>
  | Intl.DateTimeFormatOptions;

// Type checking works, but no auto-completion
const t1: T1 = 'medium';

// Auto-completion, no string default
const t2: T2 = 'medium';

// Auto-completion and string default ✅
const t3: T3 = 'medium';

ChatGPT gave me the tip of using Extract, this seems to cover all cases now: Auto-complete, errors where expected and a string default 💯. I've adapted this accordingly in https://github.com/amannn/next-intl/pull/1346/commits/cca51345c8520af2bdc24796500eaa070965159d.