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

refactor(RSC): Refactor async APIs to make the locale optional #600

Merged
merged 10 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/pages/blog/next-intl-3-0.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ If you're still happy with the Pages Router, rest assured that `next-intl` is de
## New features

1. **Support for React Server Components**: The APIs `useTranslations`, `useFormatter`, `useLocale`, `useNow` and `useTimeZone` can now be used in Server Components ([proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/environments/server-client-components)).
2. **New async APIs to handle i18n outside of components**: To handle i18n in the Metadata API and Route Handlers, the APIs `getTranslator`, `getFormatter`, `getNow`, and `getTimeZone` have been added ([proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/environments/metadata-route-handlers)).
2. **New async APIs to handle i18n outside of components**: To handle i18n in the Metadata API and Route Handlers, the APIs `getTranslations`, `getFormatter`, `getNow`, and `getTimeZone` have been added ([proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/environments/metadata-route-handlers)).
3. **Middleware for internationalized routing**: While Next.js has built-in support for this with the Pages Router, the App Router doesn't include a built-in solution anymore. `next-intl` now provides a drop-in solution that has you covered ([proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/middleware)).
4. **Internationalized navigation APIs**: Similar to the middleware, this provides a drop-in solution that adds internationalization support for Next.js' navigation APIs: `Link`, `useRouter`, `usePathname` and `redirect`. These APIs allow you to handle locale prefixes behind the scenes and also provide support for localizing pathnames (e.g. `/en/about` vs. `/de/ueber-uns`, see the [proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/navigation)).

Expand Down
4 changes: 2 additions & 2 deletions docs/pages/blog/translations-outside-of-react-components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,13 @@ If you’re working with Next.js, you might want to translate i18n messages in [
`next-intl/server` provides a set of awaitable versions of the functions that you usually call as hooks from within components. These are agnostic from React and can be used for these cases.

```tsx
import {getTranslator} from 'next-intl/server';
import {getTranslations} from 'next-intl/server';

// The `locale` is received from Next.js via `params`
const locale = params.locale;

// This creates the same function that is returned by `useTranslations`.
const t = await getTranslator(locale);
const t = await getTranslations(locale);

// Result: "Hello world!"
t('hello', {name: 'world'});
Expand Down
44 changes: 10 additions & 34 deletions docs/pages/docs/environments/metadata-route-handlers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,17 @@ There are a few places in Next.js apps where you might need to apply internation
2. [Metadata files](https://nextjs.org/docs/app/api-reference/file-conventions/metadata)
3. [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers)

`next-intl/server` provides a set of awaitable versions of the functions that you usually call as hooks from within components. Unlike the hooks, these functions require a `locale` that you [receive from Next.js](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes).

```tsx
import {
getTranslator,
getFormatter,
getNow,
getTimeZone,
getMessages
} from 'next-intl/server';

// The `locale` is received from Next.js via `params`
const locale = params.locale;

const t = await getTranslator(locale, 'Metadata');
const format = await getFormatter(locale);
const now = await getNow(locale);
const timeZone = await getTimeZone(locale);
const messages = await getMessages(locale);
```

<Callout>
The request configuration that you've set up in `i18n.ts` is automatically
inherited by these functions. The `locale` is the only exception that needs to
be provided in comparison to the hooks.
</Callout>
`next-intl/server` provides a set of [awaitable functions](/docs/environments/server-client-components#async-components) that can be used in these cases.

### Metadata API

To internationalize metadata like the page title, you can use functionality from `next-intl` in the [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function) function that can be exported from pages and layouts.

```tsx filename="app/[locale]/layout.tsx"
import {getTranslator} from 'next-intl/server';
import {getTranslations} from 'next-intl/server';

export async function generateMetadata({params: {locale}}) {
const t = await getTranslator(locale, 'Metadata');
const t = await getTranslations({locale, namespace: 'Metadata'});

return {
title: t('title')
Expand All @@ -58,27 +33,28 @@ If you need to internationalize content within [metadata files](https://nextjs.o

```tsx filename="app/[locale]/opengraph-image.tsx"
import {ImageResponse} from 'next/og';
import {getTranslator} from 'next-intl/server';
import {getTranslations} from 'next-intl/server';

export default async function Image({params: {locale}}) {
const t = await getTranslator(locale, 'OpenGraph');
export default async function OpenGraphImage({params: {locale}}) {
const t = await getTranslations({locale, namespace: 'OpenGraphImage'});
return new ImageResponse(<div style={{fontSize: 128}}>{t('title')}</div>);
}
```

### Route Handlers

You can use `next-intl` in [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) too. The required `locale` can either be received from a search param, a layout segment or by parsing the `accept-language` header of the request.
You can use `next-intl` in [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) too. The `locale` can either be received from a search param, a layout segment or by parsing the `accept-language` header of the request.

```tsx filename="app/api/hello/route.tsx"
import {NextResponse} from 'next/server';
import {getTranslator} from 'next-intl/server';
import {getTranslations} from 'next-intl/server';

export async function GET(request) {
// Example: Receive the `locale` via a search param
const {searchParams} = new URL(request.url);
const locale = searchParams.get('locale');

const t = await getTranslator(locale, 'Hello');
const t = await getTranslations({locale, namespace: 'Hello'});
return NextResponse.json({title: t('title')});
}
```
130 changes: 85 additions & 45 deletions docs/pages/docs/environments/server-client-components.mdx
Original file line number Diff line number Diff line change
@@ -1,80 +1,120 @@
import Callout from 'components/Callout';

# Internationalization of Server & Client Components in Next.js 13
# Internationalization of Server & Client Components

With the introduction of the App Router in Next.js 13, [React Server Components](https://nextjs.org/docs/getting-started/react-essentials) became publicly available. This new paradigm allows components that don’t require React’s interactive features, such as `useState` and `useEffect`, to remain server-side only.
[React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) allow you to implement components that remain server-side only if they don’t require React’s interactive features, such as `useState` and `useEffect`.

This applies to handling internationalization too.

```tsx filename="app/[locale]/page.tsx"
import {useTranslations} from 'next-intl';

// Since this component doesn't use any interactive features
// from React, it can be implemented as a Server Component.
// from React, it can be run as a Server Component.

export default function Index() {
const t = useTranslations('Index');
return <h1>{t('title')}</h1>;
}
```

Depending on if you import `useTranslations`, `useFormatter`, `useLocale`, `useNow` and `useTimeZone` from a Server or Client Component, `next-intl` will automatically provide an implementation that works best for the given environment.
Moving internationalization to the server side unlocks new levels of performance, leaving the client side for interactive features.

<details>
<summary>Deep dive: How does the Server Components integration work?</summary>
**Benefits of server-side internationalization:**

`next-intl` uses [`react-server` conditional exports](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports) to load code that is optimized for the usage in Server or Client Components. While configuration for hooks like `useTranslations` is read via `useContext` on the client side, on the server side it is loaded via [`i18n.ts`](/docs/usage/configuration#i18nts).
1. Your messages never leave the server and don't need to be serialized for the client side
2. Library code for internationalization doesn't need to be loaded on the client side
3. No need to split your messages, e.g. based on routes or components
4. No runtime cost on the client side
5. No need to handle environment differences like different time zones on the server and client

## Using internationalization in Server Components

Server Components can be declared in two ways:

1. Async components
2. Non-async, regular components

In a typical app, you'll likely find both types of components. `next-intl` provides corresponding APIs that work for the given component type.

### Async components

Hooks are currently primarly known for being used in Client Components since they are typically stateful or don't apply to a server environment. However, hooks like [`useId`](https://react.dev/reference/react/useId) can be used in Server Components too. Similarly, `next-intl` uses a hooks-based API that looks identical, regardless of if it's used in a Server or Client Component. This allows to use hooks like `useTranslations` in [shared components](https://github.com/reactjs/rfcs/blob/bf51f8755ddb38d92e23ad415fc4e3c02b95b331/text/0000-server-components.md#sharing-code-between-server-and-client), which can run both as a Server or a Client Component, depending on where they are imported from.
These are primarly concerned with fetching data and [can not use hooks](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#capabilities--constraints-of-server-and-client-components). Due to this, `next-intl` provides a set of awaitable versions of the functions that you usually call as hooks from within components.

The one restriction that currently comes with this pattern is that hooks can not be called from `async` components. To resolve this,
you can split your component into two, leaving the async code in the first one and
moving the usage of the hook to the second one.
```tsx filename="[locale]/profile/page.tsx"
import {getTranslations} from 'next-intl/server';

**Example:**
export default async function ProfilePage() {
const user = await fetchUser();
const t = await getTranslations('ProfilePage');

```tsx filename="app/[locale]/profile/page.tsx"
export default async function Profile() {
// Use this component for all async code ...
const user = await getUser();
return <ProfileContent user={user} />;
return (
<PageLayout title={t('title', {username: user.name})}>
<UserDetails user={user} />
</PageLayout>
);
}
```

These functions are available:

```tsx
import {
getTranslations,
getFormatter,
getNow,
getTimeZone,
getMessages,
getLocale
} from 'next-intl/server';

const t = await getTranslations('ProfilePage');
const format = await getFormatter();
const now = await getNow();
const timeZone = await getTimeZone();
const messages = await getMessages();
```

### Non-async components [#shared-components]

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.

function ProfileContent({user}) {
// ... and use this one for rendering the fetched data
const t = useTranslations('ProfileContent');
return <p><{t('title', {userName: user.name})}/p>
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');

return (
<section>
<h2>{t('title')}</h2>
<p>{t('followers', {count: user.numFollowers})}</p>
</section>
);
}
```

As a benefit, the extracted component now works both in Server as well as Client Components, depending on where it is rendered.
If you import `useTranslations`, `useFormatter`, `useLocale`, `useNow` and `useTimeZone` from a shared component, `next-intl` will automatically provide an implementation that works best for the environment this component executes in (server or client).

For edge cases in server-only components, you can use [awaitable APIs from `next-intl`](/docs/environments/metadata-route-handlers).
<details>
<summary>How does the Server Components integration work?</summary>

`next-intl` uses [`react-server` conditional exports](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports) to load code that is optimized for the usage in Server or Client Components. While configuration for hooks like `useTranslations` is read via `useContext` on the client side, on the server side it is loaded via [`i18n.ts`](/docs/usage/configuration#i18nts).

Hooks are currently primarly known for being used in Client Components since they are typically stateful or don't apply to a server environment. However, hooks like [`useId`](https://react.dev/reference/react/useId) can be used in Server Components too. Similarly, `next-intl` provides a hooks-based API that looks identical, regardless of if it's used in a Server or Client Component.

The one restriction that currently comes with this pattern is that hooks can not be called from `async` components. `next-intl` therefore provides a separate set of [awaitable APIs](#async-components) for this use case.

</details>

## Benefits of handling i18n in Server Components [#server-components-benefits]
<details>
<summary>Should I use async or non-async functions for my components?</summary>

Moving internationalization to the server side unlocks new levels of performance, leaving the client side for interactive features.
If you implement components that qualify as shared components, it can be beneficial to implement them as non-async functions. This allows to use these components either in a server or client environment, making them really flexible. Even if you don't intend to to ever run a particular component on the client side, this compatibility can still be helpful, e.g. for simplified testing. However, there's no need to dogmatically use non-async functions exclusively for handling internationalization—use what fits your app best.

<Callout emoji="✅" title="Benefits of server-side internationalization:">
<ol className="ml-4 list-decimal">
<li>
Your messages never leave the server and don't need to be serialized for
the client side
</li>
<li>
Library code for internationalization doesn't need to be loaded on the
client side
</li>
<li>No need to split your messages, e.g. based on routes or components</li>
<li>No runtime cost on the client side</li>
<li>
No need to handle environment differences like different time zones on the
server and client
</li>
</ol>
</Callout>
</details>

## Using internationalization in Client Components

Expand Down Expand Up @@ -211,8 +251,8 @@ export default async function LocaleLayout({children, params: {locale}}) {
```

<Callout type="warning">
Note that this is a tradeoff in regard to performance (see [the bullet points
above](#server-components-benefits)).
Note that this is a tradeoff in regard to performance (see the bullet points
at the top of this page).
</Callout>

## Troubleshooting
Expand Down
44 changes: 34 additions & 10 deletions docs/pages/docs/getting-started/app-router.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Callout from 'components/Callout';
import Steps from 'components/Steps';

# Next.js 13: Internationalization (i18n)
# Next.js App Router Internationalization (i18n)

Next.js 13 introduces support for [React Server Components](https://nextjs.org/docs/getting-started/react-essentials) with the App Router and unlocks [many benefits](/docs/environments/server-client-components) when handling internationalization on the server side.
Next.js 13 introduces support for [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) with the App Router and unlocks [many benefits](/docs/environments/server-client-components) when handling internationalization on the server side.

## Getting started

Expand Down Expand Up @@ -155,11 +155,17 @@ That's all it takes!
<li>Exploring `next-intl`? Check out the [usage guide](/docs/usage).</li>
<li>
Ran into an issue? Have a look at [the App Router
example](https://next-intl-example-next-13.vercel.app/)
([source](https://github.com/amannn/next-intl/tree/main/examples/example-next-13)).
example](https://next-intl-example-next-13.vercel.app/).
</li>
<li>
Want to learn more about about using translations across the server and
client? Check out [the Server & Client Components
guide](/docs/environments/server-client-components).
</li>
<li>
Wondering how to link between internationalized pages? Have a look at [the
navigation docs](/docs/routing/navigation).
</li>
<li>Considering using `next-intl` in Client Components? Check out [the Client Components guide](/docs/environments/server-client-components).</li>
<li>Wondering how to link between internationalized pages? Have a look at [the navigation docs](/docs/routing/navigation).</li>
</ul>

</Callout>
Expand Down Expand Up @@ -215,7 +221,7 @@ export default function IndexPage({
}) {
unstable_setRequestLocale(locale);

// Once the request locale is set, you
// Once the request locale is set, you
// can call hooks from `next-intl`
const t = useTranslations('IndexPage');

Expand All @@ -227,8 +233,6 @@ export default function IndexPage({

**Important:** `unstable_setRequestLocale` needs to be called after the `locale` is validated, but before you call any hooks from `next-intl`. Otherwise, you'll get an error when trying to prerender the page.

</Steps>

<details>
<summary>What does "unstable" mean?</summary>

Expand All @@ -237,8 +241,8 @@ export default function IndexPage({
Note that Next.js can render layouts and pages indepently. This means that e.g. when you navigate from `/settings/profile` to `/settings/privacy`, the `/settings` segment might not re-render as part of the request. Due to this, it's important that `unstable_setRequestLocale` is called not only in the parent `settings/layout.tsx`, but also in the individual pages `profile/page.tsx` and `privacy/page.tsx`.

That being said, the API is expected to work reliably if you're cautious to apply it in all relevant places.
</details>

</details>

<details>
<summary>How does unstable_setRequestLocale work?</summary>
Expand All @@ -248,3 +252,23 @@ That being said, the API is expected to work reliably if you're cautious to appl
Note that the store is scoped to a request and therefore doesn't affect other requests that might be handled in parallel while a given request resolves asynchronously.

</details>

### Use the `locale` param in metadata

In addition to the rendering of your pages, also page metadata needs to qualify for static rendering.

To achieve this, you can forward the `locale` that you receive from Next.js via `params` to [the awaitable functions from `next-intl`](/docs/environments/server-client-components#async-components).

```tsx filename="page.tsx"
import {getTranslations} from 'next-intl/server';

export async function generateMetadata({params: {locale}}) {
const t = await getTranslations({locale, namespace: 'Metadata'});

return {
title: t('title')
};
}
```

</Steps>
Loading
Loading