Skip to content

Commit

Permalink
feat: Add async requestLocale param to getRequestConfig for Next.…
Browse files Browse the repository at this point in the history
…js 15 support (#1383)

Since [Next.js is switching `headers()` to be
`async`](vercel/next.js#68812), the `locale`
that is passed to `getRequestConfig` needs to be replaced by an
awaitable alternative. Note that this is only relevant for your app in
case you're using i18n routing.

## tldr;

Switch to the new API and call it a day:

```diff
export default getRequestConfig(async ({
-  locale
+  requestLocale
}) => {
+  const locale = await requestLocale;

  // ...
});
```

If your app worked well before, then this is a 1:1 switch and will get
your app in shape for Next.js 15.

## Details

The new `requestLocale` parameter also offered a chance to get in some
enhancements for edge cases that were previously harder to support.
Therefore, the following migration is generally recommended:

**Before:**

```tsx
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
 
export default getRequestConfig(async ({locale}) => {
  // Validate that the incoming `locale` parameter is valid
  if (!routing.locales.includes(locale as any)) notFound();
 
  return {
    // ...
  };
});
```

**After:**

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

export default getRequestConfig(async ({requestLocale}) => {
  // This typically corresponds to the `[locale]` segment
  let locale = await requestLocale;

  // Ensure that the incoming locale is valid
  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    // ...
  };
});
```

The differences are:
1. `requestLocale` is a promise that needs to be awaited
2. The resolved value can be `undefined`—therefore a default should be
supplied. The default assignment allows handling cases where an error
would be thrown previously, e.g. when using APIs like `useTranslations`
on a global language selection page at `app/page.tsx`.
3. The `locale` should be returned (since you can now adjust it in the
function body).
4. We now recommend calling `notFound()` in response to an invalid
`[locale]` param in
[`app/[locale]/layout.tsx`](https://next-intl-docs-git-feat-async-request-locale-next-intl.vercel.app/docs/getting-started/app-router/with-i18n-routing#layout)
instead of in `i18n/request.ts`. This unlocks another use case, where
APIs like `useTranslations` can now be used on a global
`app/not-found.tsx` page.

See also the [updated getting started
docs](https://next-intl-docs-git-feat-async-request-locale-next-intl.vercel.app/docs/getting-started/app-router/with-i18n-routing#i18n-request).

Note that this change is non-breaking, but the synchronously available
`locale` is now considered deprecated and will be removed in a future
major version.

Contributes to #1375
Addresses #1355
  • Loading branch information
amannn authored Oct 2, 2024
1 parent 1a59754 commit 352ddc7
Show file tree
Hide file tree
Showing 40 changed files with 424 additions and 261 deletions.
50 changes: 25 additions & 25 deletions docs/pages/docs/environments/actions-metadata-route-handlers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,38 @@ import {Tabs, Tab} from 'nextra-theme-docs';

There are a few places in Next.js apps where you can apply internationalization outside of React components:

1. [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)
2. [Metadata API](https://nextjs.org/docs/app/building-your-application/optimizing/metadata)
1. [Metadata API](https://nextjs.org/docs/app/building-your-application/optimizing/metadata)
2. [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)
3. [Open Graph images](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image)
4. [Manifest](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/manifest)
5. [Sitemap](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap)
6. [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers)

`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="layout.tsx"
import {getTranslations} from 'next-intl/server';

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

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

<Callout>
By passing an explicit `locale` to the awaitable functions from `next-intl`,
you can make the metadata handler eligible for [static
rendering](/docs/getting-started/app-router/with-i18n-routing#static-rendering)
if you're using [i18n routing](/docs/getting-started/app-router).
</Callout>

### Server Actions

[Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) provide a mechanism to execute server-side code that is invoked by the client. In case you're returning user-facing messages, you can use `next-intl` to localize them based on the user's locale.
Expand Down Expand Up @@ -85,29 +108,6 @@ See the [App Router without i18n routing example](/examples#app-router-without-i

</Details>

### 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="layout.tsx"
import {getTranslations} from 'next-intl/server';

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

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

<Callout>
By passing an explicit `locale` to the awaitable functions from `next-intl`,
you can make the metadata handler eligible for [static
rendering](/docs/getting-started/app-router/with-i18n-routing#static-rendering)
if you're using [i18n routing](/docs/getting-started/app-router).
</Callout>

### Open Graph images

If you're programmatically generating [Open Graph images](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image), you can apply internationalization by calling functions from `next-intl` in the exported function:
Expand Down
24 changes: 10 additions & 14 deletions docs/pages/docs/environments/error-files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ After this change, all requests that are matched within the `[locale]` segment w

### Catching non-localized requests

When the user requests a route that is not matched by [the `next-intl` middleware](/docs/routing/middleware), there's no locale associated with the request (depending on your [`matcher` config](/docs/routing/middleware#matcher-config), e.g. `/unknown.txt` might not be matched).
When the user requests a route that is not matched by the `next-intl` [middleware](/docs/routing/middleware), there's no locale associated with the request (depending on your [`matcher` config](/docs/routing/middleware#matcher-config), e.g. `/unknown.txt` might not be matched).

You can add a root `not-found` page to handle these cases too.

Expand Down Expand Up @@ -77,25 +77,21 @@ export default function RootLayout({children}) {
}
```

For the 404 page to render, we need to call the `notFound` function in [`i18n/request.ts`](/docs/usage/configuration#i18n-request) when we detect an incoming `locale` param that isn't a valid locale.
For the 404 page to render, we need to call the `notFound` function in the root layout when we detect an incoming `locale` param that isn't a valid locale.

```tsx filename="i18n/request.ts"
```tsx filename="app/[locale]/layout.tsx"
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {routing} from '@/i18n/routing';

export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!routing.locales.includes(locale as any)) notFound();
export default function LocaleLayout({children, params: {locale}}) {
// Ensure that the incoming `locale` is valid
if (!routing.locales.includes(locale as any)) {
notFound();
}

return {
// ...
};
});
// ...
}
```

Note that `next-intl` will also call the `notFound` function internally when it tries to resolve a locale for component usage, but can not find one attached to the request (either from the middleware, or manually via [`unstable_setRequestLocale`](https://next-intl-docs.vercel.app/docs/getting-started/app-router/with-i18n-routing#static-rendering)).

## `error.js`

When an `error` file is defined, Next.js creates [an error boundary within your layout](https://nextjs.org/docs/app/building-your-application/routing/error-handling#how-errorjs-works) that wraps pages accordingly to catch runtime errors:
Expand Down
22 changes: 17 additions & 5 deletions docs/pages/docs/getting-started/app-router/with-i18n-routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,23 @@ export const config = {

### `src/i18n/request.ts` [#i18n-request]

`next-intl` creates a request-scoped configuration object, which you can use to provide messages and other options based on the user's locale to Server Components.
When using features from `next-intl` in Server Components, the relevant configuration is read from a central module that is located at `i18n/request.ts` by convention. This configuration is scoped to the current request and can be used to provide messages and other options based on the user's locale.

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

export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!routing.locales.includes(locale as any)) notFound();
export default getRequestConfig(async ({requestLocale}) => {
// This typically corresponds to the `[locale]` segment
let locale = await requestLocale;

// Ensure that a valid locale is used
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}

return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
Expand Down Expand Up @@ -183,6 +188,8 @@ The `locale` that was matched by the middleware is available via the `locale` pa
```tsx filename="app/[locale]/layout.tsx"
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';

export default async function LocaleLayout({
children,
Expand All @@ -191,6 +198,11 @@ export default async function LocaleLayout({
children: React.ReactNode;
params: {locale: string};
}) {
// Ensure that the incoming `locale` is valid
if (!routing.locales.includes(locale as any)) {
notFound();
}

// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
Expand Down
25 changes: 7 additions & 18 deletions docs/pages/docs/routing/middleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -548,25 +548,14 @@ Note that other [limitations as documented by Next.js](https://nextjs.org/docs/a

## Troubleshooting

### "Unable to find `next-intl` locale because the middleware didn't run on this request." [#unable-to-find-locale]
### "Unable to find `next-intl` locale because the middleware didn't run on this request and no `locale` was returned in `getRequestConfig`." [#unable-to-find-locale]

This can happen either because:
If the middleware is not expected to run on this request (e.g. because you're using a setup [without i18n routing](/docs/getting-started/app-router/without-i18n-routing)), you should explicitly return a `locale` from [`getRequestConfig`](/docs/usage/configuration#i18n-request) to recover from this error.

1. You're using a setup _with_ [i18n routing](/docs/getting-started/app-router) but the middleware is not set up.
2. You're using a setup _without_ [i18n routing](/docs/getting-started/app-router) but are reading the `locale` param passed to the function within `getRequestConfig` or you're not returning a `locale`.
3. The middleware is set up in the wrong file (e.g. you're using the `src` folder, but `middleware.ts` was added in the root folder).
4. The middleware matcher didn't match a request, but you're using APIs from `next-intl` in server code (e.g. a Server Component, a Server Action, etc.).
5. You're attempting to implement static rendering via [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) or [`force-static`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic) but have not followed [the static rendering guide](/docs/getting-started/app-router/with-i18n-routing#static-rendering).
If the error occurs for pathnames where the middleware is expected to run, please make sure that:

To recover from this error, please make sure that:
1. The middleware is set up in the correct file (e.g. `src/middleware.ts`).
2. Your middleware [matcher](#matcher-config) correctly matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g. `/users/jane.doe`).
3. In case you require static rendering, make sure to follow the [static rendering guide](/docs/getting-started/app-router/with-i18n-routing#static-rendering) instead of relying on hacks like [`force-static`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic).

1. You're consistently using a setup with or without [i18n routing](/docs/getting-started/app-router) (i.e. with or without the [routing APIs](/docs/routing)).
2. If you're using a setup _with_ i18n routing:
1. You're using APIs from `next-intl` (including [the navigation APIs](/docs/routing/navigation)) exclusively within the `[locale]` segment.
2. Your [middleware matcher](#matcher-config) matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g. `/users/jane.doe`).
3. If you're using [`localePrefix: 'as-needed'`](/docs/routing#locale-prefix-as-needed), the `locale` segment effectively acts like a catch-all for all unknown routes. You should make sure that the `locale` is [validated](/docs/usage/configuration#i18n-request) before it's used by any APIs from `next-intl`.
4. To implement static rendering, make sure to [provide a static locale](/docs/getting-started/app-router/with-i18n-routing#static-rendering) to `next-intl` instead of using `force-static`.
3. If you're using using a setup _without_ i18n routing:
1. You don't read the `locale` param in `getRequestConfig` but instead return it.

Note that `next-intl` will invoke the `notFound()` function to abort the render if the locale can't be found. You should consider adding [a `not-found` page](/docs/environments/error-files#not-foundjs) due to this.
Note that `next-intl` will invoke the `notFound()` function to abort the render if no locale is available after `getRequestConfig` has run. You should consider adding a [`not-found` page](/docs/environments/error-files#not-foundjs) due to this.
26 changes: 21 additions & 5 deletions docs/pages/docs/usage/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Details from 'components/Details';

Configuration properties that you use across your Next.js app can be set globally.

## Client- and Server Components [#client-server-components]
## Server & Client Components [#server-client-components]

Depending on if you handle [internationalization in Server- or Client Components](/docs/environments/server-client-components), the configuration from `i18n/request.ts` or `NextIntlClientProvider` will be applied respectively.

Expand All @@ -20,15 +20,20 @@ Depending on if you handle [internationalization in Server- or Client Components
<Tab>

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

export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!routing.locales.includes(locale as any)) notFound();
export default getRequestConfig(async ({requestLocale}) => {
// This typically corresponds to the `[locale]` segment.
let locale = await requestLocale;

// Ensure that a valid locale is used
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}

return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
Expand Down Expand Up @@ -75,6 +80,17 @@ const withNextIntl = createNextIntlPlugin(

</Details>

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

### `NextIntlClientProvider`

`NextIntlClientProvider` can be used to provide configuration for **Client Components**.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import {notFound} from 'next/navigation';
import {ReactNode} from 'react';
import {routing} from '@/i18n/routing';

type Props = {
children: ReactNode;
params: {locale: string};
};

export default async function LocaleLayout({children, params}: Props) {
// Ensure that the incoming `locale` is valid
if (!routing.locales.includes(params.locale as any)) {
notFound();
}

return (
<html lang={params.locale}>
<head>
Expand Down
13 changes: 9 additions & 4 deletions examples/example-app-router-migration/src/i18n/request.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';

export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!routing.locales.includes(locale as any)) notFound();
export default getRequestConfig(async ({requestLocale}) => {
// This typically corresponds to the `[locale]` segment
let locale = await requestLocale;

// Ensure that the incoming locale is valid
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}

return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
8 changes: 4 additions & 4 deletions examples/example-app-router-mixed-routing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ An example of how to achieve locale prefixes on public routes while reading the
2. [Setting up `next-intl` without i18n routing](https://next-intl-docs.vercel.app/docs/getting-started/app-router/without-i18n-routing)

**Relevant parts in app code:**
1. `src/middleware.ts`: Add a hint if it's a non-public route that we can read in `i18n.ts`.
2. `src/i18n.ts`: Uses the locale from the pathname segment for public routes or returns a locale from the user profile for internal app routes.
3. `src/navigation.public.ts`: Navigation APIs that automatically consider the `[locale]` segment for public routes. For internal app routes, the navigation APIs from Next.js should be used directly (see `PublicNavigation.tsx` vs `AppNavigation.tsx`).
1. `src/middleware.ts`: Only run middleware on public pages that need to be localized.
2. `src/i18n/request.ts`: Use the locale from the pathname segment for public routes or return a locale from the user profile for internal app routes.
3. `src/navigation.public.ts`: These are the navigation APIs that automatically consider the `[locale]` segment for public routes. For internal app routes, the navigation APIs from Next.js should be used directly (see `PublicNavigation.tsx` vs `AppNavigation.tsx`).

**Note:** Static rendering is currently not supported on public routes since we need to read a header. If this is a requirement, you could alternatively consider a monorepo setup and build the public and internal app separately. This could be a good alternative anyway, if you'd like to separate the code for the public and the internal app.
Note that while this approach works fine, you can alternatively also consider a monorepo setup and build the public and internal app separately if you'd like to separate the code for the two apps.

## Deploy your own

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import {useTranslations} from 'next-intl';
import {unstable_setRequestLocale} from 'next-intl/server';
import PageTitle from '@/components/PageTitle';

export default function About() {
type Props = {
params: {locale: string};
};

export default function About({params: {locale}}: Props) {
// Enable static rendering
unstable_setRequestLocale(locale);

const t = useTranslations('About');
return <PageTitle>{t('title')}</PageTitle>;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import {Metadata} from 'next';
import {notFound} from 'next/navigation';
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
import {getMessages, unstable_setRequestLocale} from 'next-intl/server';
import {ReactNode} from 'react';
import PublicNavigation from './PublicNavigation';
import PublicNavigationLocaleSwitcher from './PublicNavigationLocaleSwitcher';
import Document from '@/components/Document';
import {locales} from '@/config';

type Props = {
children: ReactNode;
params: {locale: string};
};

export function generateStaticParams() {
return locales.map((locale) => ({locale}));
}

export const metadata: Metadata = {
title: 'next-intl-mixed-routing (public)'
};
Expand All @@ -19,6 +25,14 @@ export default async function LocaleLayout({
children,
params: {locale}
}: Props) {
// Enable static rendering
unstable_setRequestLocale(locale);

// Ensure that the incoming locale is valid
if (!locales.includes(locale as any)) {
notFound();
}

// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import {useTranslations} from 'next-intl';
import {unstable_setRequestLocale} from 'next-intl/server';
import PageTitle from '@/components/PageTitle';

export default function Index() {
type Props = {
params: {locale: string};
};

export default function Index({params: {locale}}: Props) {
// Enable static rendering
unstable_setRequestLocale(locale);

const t = useTranslations('Index');
return <PageTitle>{t('title')}</PageTitle>;
}
Loading

0 comments on commit 352ddc7

Please sign in to comment.