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!: Require NextIntlClientProvider for using useLocale on the client side (preparation for dynamicIO) #1541

Merged
merged 24 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e78099d
Lazy-init `now` when `useNow` or `format.relativeTime` is used in com…
amannn Nov 12, 2024
7c528cf
Don't create default value for `now` when inheriting in `NextIntlClie…
amannn Nov 12, 2024
246e828
update test
amannn Nov 13, 2024
76adb07
fix test
amannn Nov 13, 2024
99a3135
fix docs
amannn Nov 13, 2024
7e11418
fix comment
amannn Nov 13, 2024
a892a44
fix test
amannn Nov 13, 2024
204b5df
feat!: Don't read a default for `locale` from `useParams.locale` on t…
amannn Nov 13, 2024
4b678cd
adapt docs
amannn Nov 13, 2024
17a9602
fix playground, minor docs fixes
amannn Nov 13, 2024
1e66d07
Lazy read `now` only when necessary in `createFormatter`
amannn Nov 14, 2024
36230db
Merge branch 'feat/dio-now' into feat/dio-no-params
amannn Nov 14, 2024
b470184
bump size
amannn Nov 14, 2024
3e9a7c7
Merge branch 'feat/dio-now' into feat/dio-no-params
amannn Nov 14, 2024
6063dd2
fix race condition for linting example-app-router-playground
amannn Nov 14, 2024
0de0e48
Merge branch 'feat/dio-now' into feat/dio-no-params
amannn Nov 14, 2024
846fae8
fix syntax error
amannn Nov 14, 2024
2e9d353
Merge branch 'feat/dio-now' into feat/dio-no-params
amannn Nov 14, 2024
fdfe5f3
fix docs, now we've got it right
amannn Nov 14, 2024
058036d
cleanup
amannn Nov 14, 2024
d0c8f16
revert
amannn Nov 14, 2024
d19e7dc
Merge branch 'feat/dio-now' into feat/dio-no-params
amannn Nov 14, 2024
6872c00
cleanup
amannn Nov 14, 2024
f54f529
Merge remote-tracking branch 'origin/v4' into feat/dio-no-params
amannn Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,17 @@ These functions are available:

Components that aren't declared with the `async` keyword and don't use interactive features like `useState`, are referred to as [shared components](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#sharing-code-between-server-and-client). These can render either as a Server or Client Component, depending on where they are imported from.

In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components.
In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components:

```tsx filename="UserDetails.tsx"
import {useTranslations} from 'next-intl';

export default function UserDetails({user}) {
const t = useTranslations('UserProfile');

// This component will execute as a Server Component by default.
// However, if it is imported from a Client Component, it will
// execute as a Client Component.
return (
<section>
<h2>{t('title')}</h2>
Expand Down
117 changes: 69 additions & 48 deletions docs/src/pages/docs/usage/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ export default getRequestConfig(async () => {
});
```

<Details id="server-request-locale">
<summary>Which values can the `requestLocale` parameter hold?</summary>

While the `requestLocale` parameter typically corresponds to the `[locale]` segment that was matched by the middleware, there are three special cases to consider:

1. **Overrides**: When an explicit `locale` is passed to [awaitable functions](/docs/environments/actions-metadata-route-handlers) like `getTranslations({locale: 'en'})`, then this value will be used instead of the segment.
1. **`undefined`**: The value can be `undefined` when a page outside of the `[locale]` segment renders (e.g. a language selection page at `app/page.tsx`).
1. **Invalid values**: Since the `[locale]` segment effectively acts like a catch-all for unknown routes (e.g. `/unknown.txt`), invalid values should be replaced with a valid locale. In addition to this, you might want to call `notFound()` in [the root layout](/docs/getting-started/app-router/with-i18n-routing#layout) to abort the render in this case.

</Details>

</Tabs.Tab>
<Tabs.Tab>

Expand All @@ -191,14 +202,13 @@ export default getRequestConfig(async () => {
</Tabs.Tab>
</Tabs>

<Details id="server-request-locale">
<summary>Which values can the `requestLocale` parameter hold?</summary>
<Details id="locale-change">
<summary>How can I change the locale?</summary>

While the `requestLocale` parameter typically corresponds to the `[locale]` segment that was matched by the middleware, there are three special cases to consider:
Depending on if you're using [i18n routing](/docs/getting-started/app-router), the locale can be changed as follows:

1. **Overrides**: When an explicit `locale` is passed to [awaitable functions](/docs/environments/actions-metadata-route-handlers) like `getTranslations({locale: 'en'})`, then this value will be used instead of the segment.
1. **`undefined`**: The value can be `undefined` when a page outside of the `[locale]` segment renders (e.g. a language selection page at `app/page.tsx`).
1. **Invalid values**: Since the `[locale]` segment effectively acts like a catch-all for unknown routes (e.g. `/unknown.txt`), invalid values should be replaced with a valid locale. In addition to this, you might want to call `notFound()` in [the root layout](/docs/getting-started/app-router/with-i18n-routing#layout) to abort the render in this case.
1. **With i18n routing**: The locale is managed by the router and can be changed by using navigation APIs from `next-intl` like [`Link`](/docs/routing/navigation#link) or [`useRouter`](/docs/routing/navigation#userouter).
2. **Without i18n routing**: You can change the locale by updating the value where the locale is read from (e.g. a cookie, a user setting, etc.). If you're looking for inspiration, you can have a look at the [App Router without i18n routing example](/examples#app-router-without-i18n-routing) that manages the locale via a cookie.

</Details>

Expand All @@ -218,41 +228,13 @@ import {getLocale} from 'next-intl/server';
const locale = await getLocale();
```

### `Locale` [#locale-type]

When passing a `locale` to another function, you can use the `Locale` type for the receiving parameter:

```tsx
import {Locale} from 'next-intl';

async function getPosts(locale: Locale) {
// ...
}
```

<Callout>
By default, `Locale` is typed as `string`. However, you can optionally provide
a strict union based on your supported locales for this type by [augmenting
the `Locale` type](/docs/workflows/typescript#locale).
</Callout>

<Details id="locale-change">
<summary>How can I change the locale?</summary>

Depending on if you're using [i18n routing](/docs/getting-started/app-router), the locale can be changed as follows:

1. **With i18n routing**: The locale is managed by the router and can be changed by using navigation APIs from `next-intl` like [`Link`](/docs/routing/navigation#link) or [`useRouter`](/docs/routing/navigation#userouter).
2. **Without i18n routing**: You can change the locale by updating the value where the locale is read from (e.g. a cookie, a user setting, etc.). If you're looking for inspiration, you can have a look at the [App Router without i18n routing example](/examples#app-router-without-i18n-routing) that manages the locale via a cookie.

</Details>

<Details id="locale-return-value">
<summary>Which value is returned from `useLocale`?</summary>

The returned value is resolved based on these priorities:
Depending on how a component renders, the returned locale corresponds to:

1. **Server Components**: If you're using [i18n routing](/docs/getting-started/app-router), the returned locale is the one that you've either provided via [`setRequestLocale`](/docs/getting-started/app-router/with-i18n-routing#static-rendering) or alternatively the one in the `[locale]` segment that was matched by the middleware. If you're not using i18n routing, the returned locale is the one that you've provided via `getRequestConfig`.
2. **Client Components**: In this case, the locale is received from `NextIntlClientProvider` or alternatively `useParams().locale`. Note that `NextIntlClientProvider` automatically inherits the locale if the component is rendered by a Server Component.
1. **Server Components**: The locale represents the value returned in [`i18n/request.ts`](#i18n-request).
2. **Client Components**: In this case, the locale is received from [`NextIntlClientProvider`](#nextintlclientprovider). Note that `NextIntlClientProvider` automatically inherits the locale if it is rendered by a Server Component.
amannn marked this conversation as resolved.
Show resolved Hide resolved

</Details>

Expand All @@ -277,6 +259,24 @@ return (

</Details>

### `Locale` type [#locale-type]

When passing a `locale` to another function, you can use the `Locale` type for the receiving parameter:

```tsx
import {Locale} from 'next-intl';

async function getPosts(locale: Locale) {
// ...
}
```

<Callout>
By default, `Locale` is typed as `string`. However, you can optionally provide
a strict union based on your supported locales for this type by [augmenting
the `Locale` type](/docs/workflows/typescript#locale).
</Callout>

## Messages

The most crucial aspect of internationalization is providing labels based on the user's language. The recommended workflow is to store your messages in your repository along with the code.
Expand Down Expand Up @@ -451,43 +451,64 @@ const timeZone = await getTimeZone();

## Now value [#now]

When formatting [relative dates and times](/docs/usage/dates-times#relative-times), `next-intl` will format times in relation to a reference point in time that is referred to as "now". By default, this is the time a component renders.

If you prefer to override the default, you can provide an explicit value for `now`:
When formatting [relative dates and times](/docs/usage/dates-times#relative-times), `next-intl` will format times in relation to a reference point in time that is referred to as "now". If you want to ensure that this value is consistent across components, you can configure a global `now` value:

<Tabs items={['i18n/request.ts', 'Provider']}>
<Tabs.Tab>

```tsx filename="i18n/request.ts"
import {getRequestConfig} from 'next-intl/server';

async function now() {
// (if you're using `dynamicIO`)
// 'use cache';

// Use this value consistently
// when formatting relative times
return new Date();
}

export default getRequestConfig(async () => {
return {
// This is the default, a single date instance will be
// used by all Server Components to ensure consistency.
// Tip: This value can be mocked to a constant value
// for consistent results in end-to-end-tests.
now: new Date()
now: await now()

// ...
};
});
```

If a `now` value is provided in `i18n/request.ts`, this will automatically be inherited by Client Components if you wrap them in a `NextIntlClientProvider` that is rendered by a Server Component.

<Details id="now-cache">
<summary>How does the usage of `'use cache'` with `now` relate to cache expiration?</summary>

If you're using [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO), you can cache the value of `now` with the [`'use cache'`](https://nextjs.org/docs/canary/app/api-reference/directives/use-cache) directive:

```tsx
async function now() {
'use cache';
const now = new Date();
}
```

Since the request config from `i18n/request.ts` is shared among all Server Components that use features from `next-intl`, the cache expiration for `now` will apply to all of these, regardless of if they're using relative time formatting or not.

If you want more granular cache control, you can consider passing the `now` value to [`format.relativeTime`](/docs/usage/dates-times#relative-times) explicitly as a second argument where relevant.

</Details>

</Tabs.Tab>
<Tabs.Tab>

```tsx
const now = new Date('2020-11-20T10:36:01.516Z');
const now = new Date();

<NextIntlClientProvider now={now}>...</NextIntlClientProvider>;
```

</Tabs.Tab>
</Tabs>

Similarly to the `timeZone`, the `now` value in Client Components is automatically inherited from the server side if you wrap the relevant components in a `NextIntlClientProvider` that is rendered by a Server Component.

### `useNow` & `getNow` [#use-now]

The configured `now` value can be read in components via `useNow` or `getNow`:
Expand Down
32 changes: 15 additions & 17 deletions docs/src/pages/docs/usage/dates-times.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,32 +67,30 @@ function Component() {
const format = useFormatter();
const dateTime = new Date('2020-11-20T08:30:00.000Z');

// At 2020-11-20T10:36:00.000Z,
// this will render "2 hours ago"
format.relativeTime(dateTime);
// A reference point in time
const now = new Date('2020-11-20T10:36:00.000Z');

// This will render "2 hours ago"
format.relativeTime(dateTime, now);
}
```

Note that values are rounded, so e.g. if 126 minutes have passed, "2 hours ago" will be returned.

### Supplying `now`
### Providing `now` [#relative-times-now]

By default, `relativeTime` will use [the global value for `now`](/docs/usage/configuration#now). If you want to use a different value, you can explicitly pass this as the second parameter.
The `now` value can either be provided on a case-by-case basis or configured [globally](/docs/usage/configuration#now).

```js
import {useFormatter} from 'next-intl';

function Component() {
const format = useFormatter();
const dateTime = new Date('2020-11-20T08:30:00.000Z');
const now = new Date('2020-11-20T10:36:00.000Z');
If you've configured a global `now` value, you can omit the corresponding parameter:

// Renders "2 hours ago"
format.relativeTime(dateTime, now);
}
```js
// Uses the global now value
format.relativeTime(dateTime);
```

If you want the relative time value to update over time, you can do so with [the `useNow` hook](/docs/usage/configuration#now):
### Continuously updating relative times [#relative-times-update]

In case you want a relative time value to update over time, you can do so with [the `useNow` hook](/docs/usage/configuration#now):

```js
import {useNow, useFormatter} from 'next-intl';
Expand All @@ -114,7 +112,7 @@ function Component() {

### Customizing the unit

By default, `relativeTime` will pick a unit based on the difference between the passed date and `now` (e.g. 3 seconds, 40 minutes, 4 days, etc.).
By default, `relativeTime` will pick a unit based on the difference between the passed date and `now` like "3 seconds" or "5 days".

If you want to use a specific unit, you can provide options via the second argument:

Expand Down
3 changes: 3 additions & 0 deletions examples/example-app-router-playground/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const withMdx = mdxPlugin();

export default withMdx(
withNextIntl({
eslint: {
ignoreDuringBuilds: true
},
trailingSlash: process.env.NEXT_PUBLIC_USE_CASE === 'trailing-slash',
basePath:
process.env.NEXT_PUBLIC_USE_CASE === 'base-path'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Metadata} from 'next';
import {notFound} from 'next/navigation';
import {Locale, hasLocale} from 'next-intl';
import {Locale, NextIntlClientProvider, hasLocale} from 'next-intl';
import {
getFormatter,
getNow,
Expand Down Expand Up @@ -50,8 +50,10 @@ export default function LocaleLayout({children, params: {locale}}: Props) {
lineHeight: 1.5
}}
>
<Navigation />
{children}
<NextIntlClientProvider>
<Navigation />
{children}
</NextIntlClientProvider>
</div>
</body>
</html>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default getRequestConfig(async ({requestLocale}) => {

return {
locale,
now: now ? new Date(now) : undefined,
now: now ? new Date(now) : new Date(),
timeZone,
messages,
formats,
Expand Down
16 changes: 15 additions & 1 deletion packages/next-intl/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ export default (await getPresets('typescript', 'react', 'vitest')).concat({
'react-compiler': reactCompilerPlugin
},
rules: {
'react-compiler/react-compiler': 'error'
'react-compiler/react-compiler': 'error',
'no-restricted-imports': [
'error',
{
paths: [
{
// Two reasons:
// - Avoid hardcoding the `locale` param
// - Prepare for a new API in Next.js to read params deeply
name: 'next/navigation.js',
importNames: ['useParams']
}
]
}
]
}
});
28 changes: 12 additions & 16 deletions packages/next-intl/src/navigation/createNavigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,27 @@ import {render, screen} from '@testing-library/react';
import {
RedirectType,
permanentRedirect as nextPermanentRedirect,
redirect as nextRedirect,
useParams as nextUseParams
redirect as nextRedirect
} from 'next/navigation.js';
import {renderToString} from 'react-dom/server';
import {Locale} from 'use-intl';
import {Locale, useLocale} from 'use-intl';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {useLocale} from '../index.react-server.tsx';
import {DomainsConfig, Pathnames, defineRouting} from '../routing.tsx';
import createNavigationClient from './react-client/createNavigation.tsx';
import createNavigationServer from './react-server/createNavigation.tsx';
import getServerLocale from './react-server/getServerLocale.tsx';

vi.mock('react');
vi.mock('next/navigation.js', async () => {
const actual = await vi.importActual('next/navigation.js');
return {
...actual,
useParams: vi.fn(() => ({locale: 'en'})),
redirect: vi.fn(),
permanentRedirect: vi.fn()
};
});
vi.mock('next/navigation.js', async () => ({
...(await vi.importActual('next/navigation.js')),
redirect: vi.fn(),
permanentRedirect: vi.fn()
}));
vi.mock('./react-server/getServerLocale');
vi.mock('use-intl', async () => ({
...(await vi.importActual('use-intl')),
useLocale: vi.fn(() => 'en')
}));

function mockCurrentLocale(locale: Locale) {
// Enable synchronous rendering without having to suspend
Expand All @@ -35,9 +33,7 @@ function mockCurrentLocale(locale: Locale) {

vi.mocked(getServerLocale).mockImplementation(() => promise);

vi.mocked(nextUseParams<{locale: Locale}>).mockImplementation(() => ({
locale
}));
vi.mocked(useLocale).mockImplementation(() => locale);
}

function mockLocation(location: Partial<typeof window.location>) {
Expand Down
Loading
Loading