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 all 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
76 changes: 39 additions & 37 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,15 @@ import {getLocale} from 'next-intl/server';
const locale = await getLocale();
```

### `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>

<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**: The locale is received from [`NextIntlClientProvider`](#nextintlclientprovider).

Note that `NextIntlClientProvider` automatically inherits the locale if it is rendered by a Server Component, therefore you rarely need to pass a locale to `NextIntlClientProvider` yourself.

</Details>

Expand All @@ -277,6 +261,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
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
17 changes: 16 additions & 1 deletion packages/next-intl/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ 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: [
{
// Because:
// - Avoid hardcoding the `locale` param
// - Prepare for a new API in Next.js to read params deeply
// - Avoid issues with `dynamicIO`
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
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import {fireEvent, render, screen} from '@testing-library/react';
import {
usePathname as useNextPathname,
useRouter as useNextRouter,
useParams
useRouter as useNextRouter
} from 'next/navigation.js';
import type {Locale} from 'use-intl';
import {useLocale} from 'use-intl';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {NextIntlClientProvider, useLocale} from '../../index.react-client.tsx';
import {DomainsConfig, Pathnames} from '../../routing.tsx';
import createNavigation from './createNavigation.tsx';

vi.mock('next/navigation.js');
vi.mock('use-intl', async () => ({
...(await vi.importActual('use-intl')),
useLocale: vi.fn(() => 'en')
}));

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

function mockLocation(
Expand Down Expand Up @@ -112,29 +113,6 @@ describe("localePrefix: 'always'", () => {
});

describe('Link', () => {
describe('usage outside of Next.js', () => {
beforeEach(() => {
vi.mocked(useParams<any>).mockImplementation((() => null) as any);
});

it('works with a provider', () => {
render(
<NextIntlClientProvider locale="en">
<Link href="/test">Test</Link>
</NextIntlClientProvider>
);
expect(
screen.getByRole('link', {name: 'Test'}).getAttribute('href')
).toBe('/en/test');
});

it('throws without a provider', () => {
expect(() => render(<Link href="/test">Test</Link>)).toThrow(
'No intl context found. Have you configured the provider?'
);
});
});

it('can receive a ref', () => {
let ref;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import {
useRouter as useNextRouter
} from 'next/navigation.js';
import {useMemo} from 'react';
import type {Locale} from 'use-intl';
import useLocale from '../../react-client/useLocale.tsx';
import {type Locale, useLocale} from 'use-intl';
import {
RoutingConfigLocalizedNavigation,
RoutingConfigSharedNavigation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import {render, screen} from '@testing-library/react';
import {usePathname as useNextPathname, useParams} from 'next/navigation.js';
import {usePathname as useNextPathname} from 'next/navigation.js';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {NextIntlClientProvider} from '../../index.react-client.tsx';
import {NextIntlClientProvider, useLocale} from '../../index.react-client.tsx';
import useBasePathname from './useBasePathname.tsx';

vi.mock('next/navigation.js');
vi.mock('use-intl', async () => ({
...(await vi.importActual('use-intl')),
useLocale: vi.fn(() => 'en')
}));

function mockPathname(pathname: string) {
vi.mocked(useNextPathname).mockImplementation(() => pathname);
vi.mocked(useParams<any>).mockImplementation(() => ({locale: 'en'}));
vi.mocked(useLocale).mockImplementation(() => 'en');
}

function Component() {
Expand Down Expand Up @@ -51,7 +55,6 @@ describe('prefixed routing', () => {
describe('usage outside of Next.js', () => {
beforeEach(() => {
vi.mocked(useNextPathname).mockImplementation((() => null) as any);
vi.mocked(useParams<any>).mockImplementation((() => null) as any);
});

it('returns `null` when used within a provider', () => {
Expand All @@ -62,10 +65,4 @@ describe('usage outside of Next.js', () => {
);
expect(container.innerHTML).toBe('');
});

it('throws without a provider', () => {
expect(() => render(<Component />)).toThrow(
'No intl context found. Have you configured the provider?'
);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {usePathname as useNextPathname} from 'next/navigation.js';
import {useMemo} from 'react';
import useLocale from '../../react-client/useLocale.tsx';
import {useLocale} from 'use-intl';
import {
LocalePrefixConfigVerbose,
LocalePrefixMode,
Expand Down
3 changes: 1 addition & 2 deletions packages/next-intl/src/navigation/shared/BaseLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {
useEffect,
useState
} from 'react';
import type {Locale} from 'use-intl';
import useLocale from '../../react-client/useLocale.tsx';
import {type Locale, useLocale} from 'use-intl';
import {InitializedLocaleCookieConfig} from '../../routing/config.tsx';
import syncLocaleCookie from './syncLocaleCookie.tsx';

Expand Down
3 changes: 0 additions & 3 deletions packages/next-intl/src/react-client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,4 @@ export const useFormatter = callHook(
base_useFormatter
) as typeof base_useFormatter;

// Replace `useLocale` export from `use-intl`
export {default as useLocale} from './useLocale.tsx';

export {default as NextIntlClientProvider} from '../shared/NextIntlClientProvider.tsx';
33 changes: 0 additions & 33 deletions packages/next-intl/src/react-client/useLocale.test.tsx

This file was deleted.

Loading
Loading