diff --git a/docs/pages/docs/environments/actions-metadata-route-handlers.mdx b/docs/pages/docs/environments/actions-metadata-route-handlers.mdx
index 36104a611..355e45031 100644
--- a/docs/pages/docs/environments/actions-metadata-route-handlers.mdx
+++ b/docs/pages/docs/environments/actions-metadata-route-handlers.mdx
@@ -6,8 +6,8 @@ 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)
@@ -15,6 +15,29 @@ There are a few places in Next.js apps where you can apply internationalization
`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')
+ };
+}
+```
+
+
+ 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).
+
+
### 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.
@@ -85,29 +108,6 @@ See the [App Router without i18n routing example](/examples#app-router-without-i
-### 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')
- };
-}
-```
-
-
- 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).
-
-
### 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:
diff --git a/docs/pages/docs/environments/error-files.mdx b/docs/pages/docs/environments/error-files.mdx
index 9d713a88e..d2bef6eed 100644
--- a/docs/pages/docs/environments/error-files.mdx
+++ b/docs/pages/docs/environments/error-files.mdx
@@ -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.
@@ -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:
diff --git a/docs/pages/docs/getting-started/app-router/with-i18n-routing.mdx b/docs/pages/docs/getting-started/app-router/with-i18n-routing.mdx
index 633ea2bea..9261d2028 100644
--- a/docs/pages/docs/getting-started/app-router/with-i18n-routing.mdx
+++ b/docs/pages/docs/getting-started/app-router/with-i18n-routing.mdx
@@ -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
};
});
@@ -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,
@@ -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();
diff --git a/docs/pages/docs/routing/middleware.mdx b/docs/pages/docs/routing/middleware.mdx
index f59072fba..ec6f4aad6 100644
--- a/docs/pages/docs/routing/middleware.mdx
+++ b/docs/pages/docs/routing/middleware.mdx
@@ -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.
diff --git a/docs/pages/docs/usage/configuration.mdx b/docs/pages/docs/usage/configuration.mdx
index 8a7ad9fe4..a70908101 100644
--- a/docs/pages/docs/usage/configuration.mdx
+++ b/docs/pages/docs/usage/configuration.mdx
@@ -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.
@@ -20,15 +20,20 @@ Depending on if you handle [internationalization in Server- or Client Components
```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
};
});
@@ -75,6 +80,17 @@ const withNextIntl = createNextIntlPlugin(
+
+Which values can the `requestLocale` parameter hold?
+
+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.
+
+
+
### `NextIntlClientProvider`
`NextIntlClientProvider` can be used to provide configuration for **Client Components**.
diff --git a/examples/example-app-router-migration/src/app/[locale]/layout.tsx b/examples/example-app-router-migration/src/app/[locale]/layout.tsx
index c611d4fdd..5d3c87400 100644
--- a/examples/example-app-router-migration/src/app/[locale]/layout.tsx
+++ b/examples/example-app-router-migration/src/app/[locale]/layout.tsx
@@ -1,4 +1,6 @@
+import {notFound} from 'next/navigation';
import {ReactNode} from 'react';
+import {routing} from '@/i18n/routing';
type Props = {
children: ReactNode;
@@ -6,6 +8,11 @@ type Props = {
};
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 (
diff --git a/examples/example-app-router-migration/src/i18n/request.ts b/examples/example-app-router-migration/src/i18n/request.ts
index c65915183..70066e964 100644
--- a/examples/example-app-router-migration/src/i18n/request.ts
+++ b/examples/example-app-router-migration/src/i18n/request.ts
@@ -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
};
});
diff --git a/examples/example-app-router-mixed-routing/README.md b/examples/example-app-router-mixed-routing/README.md
index 8bb21cdee..abcfa4379 100644
--- a/examples/example-app-router-mixed-routing/README.md
+++ b/examples/example-app-router-mixed-routing/README.md
@@ -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
diff --git a/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/about/page.tsx b/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/about/page.tsx
index e6e3ed836..a5988d5a7 100644
--- a/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/about/page.tsx
+++ b/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/about/page.tsx
@@ -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 {t('title')};
}
diff --git a/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/layout.tsx b/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/layout.tsx
index 89fe0be41..d56ec0849 100644
--- a/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/layout.tsx
+++ b/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/layout.tsx
@@ -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)'
};
@@ -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();
diff --git a/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/page.tsx b/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/page.tsx
index dbc5424c0..f626fcb49 100644
--- a/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/page.tsx
+++ b/examples/example-app-router-mixed-routing/src/app/(public)/[locale]/page.tsx
@@ -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 {t('title')};
}
diff --git a/examples/example-app-router-mixed-routing/src/i18n/request.ts b/examples/example-app-router-mixed-routing/src/i18n/request.ts
index 5af925362..ed7b043f6 100644
--- a/examples/example-app-router-mixed-routing/src/i18n/request.ts
+++ b/examples/example-app-router-mixed-routing/src/i18n/request.ts
@@ -1,34 +1,22 @@
-import {headers} from 'next/headers';
-import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
-import {locales} from '../config';
+import {defaultLocale, locales} from '../config';
import {getUserLocale} from '../db';
-async function getConfig(locale: string) {
- // Validate that the incoming `locale` parameter is valid
- if (!locales.includes(locale as any)) notFound();
+export default getRequestConfig(async ({requestLocale}) => {
+ let locale = await requestLocale;
+
+ if (!locale) {
+ // The user is logged in
+ locale = await getUserLocale();
+ }
+
+ // Ensure that the incoming locale is valid
+ if (!locales.includes(locale as any)) {
+ locale = defaultLocale;
+ }
return {
+ locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
-}
-
-export default getRequestConfig(async (params) => {
- // Read a hint that was set in the middleware
- const isAppRoute = headers().get('x-app-route') === 'true';
-
- if (isAppRoute) {
- const locale = await getUserLocale();
-
- return {
- // Return a locale to `next-intl` in case we've read
- // it from user settings instead of the pathname
- locale,
- ...(await getConfig(locale))
- };
- } else {
- // Be careful to only read from params if the route is public
- const locale = params.locale;
- return getConfig(locale);
- }
});
diff --git a/examples/example-app-router-mixed-routing/src/middleware.ts b/examples/example-app-router-mixed-routing/src/middleware.ts
index 01134cf75..f413e76bc 100644
--- a/examples/example-app-router-mixed-routing/src/middleware.ts
+++ b/examples/example-app-router-mixed-routing/src/middleware.ts
@@ -1,23 +1,9 @@
-import {NextRequest, NextResponse} from 'next/server';
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing.public';
-export default function middleware(request: NextRequest) {
- const pathname = request.nextUrl.pathname;
- const isAppRoute = pathname === '/app' || pathname.startsWith('/app/');
-
- const intlMiddleware = createMiddleware(routing);
-
- if (isAppRoute) {
- // Add a hint that we can read in `i18n.ts`
- request.headers.set('x-app-route', 'true');
- return NextResponse.next({request: {headers: request.headers}});
- } else {
- return intlMiddleware(request);
- }
-}
+export default createMiddleware(routing);
export const config = {
- // Match only internationalized pathnames
- matcher: ['/', '/(de|en)/:path*', '/app/:path*']
+ // Match only public pathnames
+ matcher: ['/', '/(de|en)/:path*']
};
diff --git a/examples/example-app-router-next-auth/src/app/[locale]/layout.tsx b/examples/example-app-router-next-auth/src/app/[locale]/layout.tsx
index a909a3773..0acc48b28 100644
--- a/examples/example-app-router-next-auth/src/app/[locale]/layout.tsx
+++ b/examples/example-app-router-next-auth/src/app/[locale]/layout.tsx
@@ -1,13 +1,24 @@
-import {NextIntlClientProvider, useMessages} from 'next-intl';
+import {notFound} from 'next/navigation';
+import {NextIntlClientProvider} from 'next-intl';
+import {getMessages} from 'next-intl/server';
import {ReactNode} from 'react';
+import {routing} from '@/i18n/routing';
type Props = {
children: ReactNode;
params: {locale: string};
};
-export default function LocaleLayout({children, params: {locale}}: Props) {
- const messages = useMessages();
+export default async function LocaleLayout({
+ children,
+ params: {locale}
+}: Props) {
+ // Ensure that the incoming `locale` is valid
+ if (!routing.locales.includes(locale as any)) {
+ notFound();
+ }
+
+ const messages = await getMessages();
return (
diff --git a/examples/example-app-router-next-auth/src/i18n/request.ts b/examples/example-app-router-next-auth/src/i18n/request.ts
index c65915183..70066e964 100644
--- a/examples/example-app-router-next-auth/src/i18n/request.ts
+++ b/examples/example-app-router-next-auth/src/i18n/request.ts
@@ -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
};
});
diff --git a/examples/example-app-router-playground/src/app/[locale]/layout.tsx b/examples/example-app-router-playground/src/app/[locale]/layout.tsx
index 9a70b0512..9a45e8c4e 100644
--- a/examples/example-app-router-playground/src/app/[locale]/layout.tsx
+++ b/examples/example-app-router-playground/src/app/[locale]/layout.tsx
@@ -1,4 +1,5 @@
import {Metadata} from 'next';
+import {notFound} from 'next/navigation';
import {
getFormatter,
getNow,
@@ -7,6 +8,7 @@ import {
} from 'next-intl/server';
import {ReactNode} from 'react';
import Navigation from '../../components/Navigation';
+import {routing} from '@/i18n/routing';
type Props = {
children: ReactNode;
@@ -33,6 +35,11 @@ export async function generateMetadata({
}
export default function LocaleLayout({children, params: {locale}}: Props) {
+ // Ensure that the incoming `locale` is valid
+ if (!routing.locales.includes(locale as any)) {
+ notFound();
+ }
+
return (
diff --git a/examples/example-app-router-playground/src/i18n/request.tsx b/examples/example-app-router-playground/src/i18n/request.tsx
index f2ffe6e95..79af138bc 100644
--- a/examples/example-app-router-playground/src/i18n/request.tsx
+++ b/examples/example-app-router-playground/src/i18n/request.tsx
@@ -1,5 +1,4 @@
import {headers} from 'next/headers';
-import {notFound} from 'next/navigation';
import {Formats} from 'next-intl';
import {getRequestConfig} from 'next-intl/server';
import defaultMessages from '../../messages/en.json';
@@ -26,9 +25,14 @@ export const formats = {
}
} satisfies Formats;
-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;
+ }
const now = headers().get('x-now');
const timeZone = headers().get('x-time-zone') ?? 'Europe/Vienna';
@@ -37,6 +41,7 @@ export default getRequestConfig(async ({locale}) => {
const messages = {...defaultMessages, ...localeMessages};
return {
+ locale,
now: now ? new Date(now) : undefined,
timeZone,
messages,
diff --git a/examples/example-app-router-playground/tests/main.spec.ts b/examples/example-app-router-playground/tests/main.spec.ts
index c62eb3a7b..b4783fa40 100644
--- a/examples/example-app-router-playground/tests/main.spec.ts
+++ b/examples/example-app-router-playground/tests/main.spec.ts
@@ -682,7 +682,10 @@ it('can use `getPahname` to define a canonical link', async ({page}) => {
await expect(getCanonicalPathname()).resolves.toBe('/de/neuigkeiten/3');
});
-it('provides a `Link` that works with Radix Primitives', async ({page}) => {
+// https://github.com/radix-ui/primitives/issues/3165
+it.skip('provides a `Link` that works with Radix Primitives', async ({
+ page
+}) => {
await page.goto('/');
await page.getByRole('button', {name: 'Toggle dropdown'}).click();
await page.keyboard.press('ArrowDown');
diff --git a/examples/example-app-router/src/app/[locale]/layout.tsx b/examples/example-app-router/src/app/[locale]/layout.tsx
index d5f7ab421..beace3ad2 100644
--- a/examples/example-app-router/src/app/[locale]/layout.tsx
+++ b/examples/example-app-router/src/app/[locale]/layout.tsx
@@ -1,17 +1,9 @@
-import clsx from 'clsx';
-import {Inter} from 'next/font/google';
-import {NextIntlClientProvider} from 'next-intl';
-import {
- getMessages,
- getTranslations,
- unstable_setRequestLocale
-} from 'next-intl/server';
+import {notFound} from 'next/navigation';
+import {getTranslations, unstable_setRequestLocale} from 'next-intl/server';
import {ReactNode} from 'react';
-import Navigation from '@/components/Navigation';
+import BaseLayout from '@/components/BaseLayout';
import {routing} from '@/i18n/routing';
-const inter = Inter({subsets: ['latin']});
-
type Props = {
children: ReactNode;
params: {locale: string};
@@ -35,21 +27,13 @@ export default async function LocaleLayout({
children,
params: {locale}
}: Props) {
+ // Ensure that the incoming `locale` is valid
+ if (!routing.locales.includes(locale as any)) {
+ notFound();
+ }
+
// Enable static rendering
unstable_setRequestLocale(locale);
- // Providing all messages to the client
- // side is the easiest way to get started
- const messages = await getMessages();
-
- return (
-
-
-
-
- {children}
-
-
-
- );
+ return {children};
}
diff --git a/examples/example-app-router/src/app/[locale]/not-found.tsx b/examples/example-app-router/src/app/[locale]/not-found.tsx
index 24991d40a..c1fd4979b 100644
--- a/examples/example-app-router/src/app/[locale]/not-found.tsx
+++ b/examples/example-app-router/src/app/[locale]/not-found.tsx
@@ -1,15 +1,4 @@
-import {useTranslations} from 'next-intl';
-import PageLayout from '@/components/PageLayout';
-
// Note that `app/[locale]/[...rest]/page.tsx`
// is necessary for this page to render.
-export default function NotFoundPage() {
- const t = useTranslations('NotFoundPage');
-
- return (
-
- {t('description')}
-
- );
-}
+export {default} from '@/components/NotFoundPage';
diff --git a/examples/example-app-router/src/app/not-found.tsx b/examples/example-app-router/src/app/not-found.tsx
index ed4705cb1..69e748520 100644
--- a/examples/example-app-router/src/app/not-found.tsx
+++ b/examples/example-app-router/src/app/not-found.tsx
@@ -1,17 +1,15 @@
-'use client';
+import BaseLayout from '@/components/BaseLayout';
+import NotFoundPage from '@/components/NotFoundPage';
+import {routing} from '@/i18n/routing';
-import Error from 'next/error';
+// This page renders when a route like `/unknown.txt` is requested.
+// In this case, the layout at `app/[locale]/layout.tsx` receives
+// an invalid value as the `[locale]` param and calls `notFound()`.
-// Render the default Next.js 404 page when a route
-// is requested that doesn't match the middleware and
-// therefore doesn't have a locale associated with it.
-
-export default function NotFound() {
+export default function GlobalNotFound() {
return (
-
-
-
-
-
+
+
+
);
}
diff --git a/examples/example-app-router/src/components/BaseLayout.tsx b/examples/example-app-router/src/components/BaseLayout.tsx
new file mode 100644
index 000000000..8907d4e9b
--- /dev/null
+++ b/examples/example-app-router/src/components/BaseLayout.tsx
@@ -0,0 +1,30 @@
+import {clsx} from 'clsx';
+import {Inter} from 'next/font/google';
+import {NextIntlClientProvider} from 'next-intl';
+import {getMessages} from 'next-intl/server';
+import {ReactNode} from 'react';
+import Navigation from '@/components/Navigation';
+
+const inter = Inter({subsets: ['latin']});
+
+type Props = {
+ children: ReactNode;
+ locale: string;
+};
+
+export default async function BaseLayout({children, locale}: Props) {
+ // Providing all messages to the client
+ // side is the easiest way to get started
+ const messages = await getMessages();
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/examples/example-app-router/src/components/NotFoundPage.tsx b/examples/example-app-router/src/components/NotFoundPage.tsx
new file mode 100644
index 000000000..9fdf94f35
--- /dev/null
+++ b/examples/example-app-router/src/components/NotFoundPage.tsx
@@ -0,0 +1,12 @@
+import {useTranslations} from 'next-intl';
+import PageLayout from './PageLayout';
+
+export default function NotFoundPage() {
+ const t = useTranslations('NotFoundPage');
+
+ return (
+
+ {t('description')}
+
+ );
+}
diff --git a/examples/example-app-router/src/i18n/request.ts b/examples/example-app-router/src/i18n/request.ts
index b04a04503..df242f13d 100644
--- a/examples/example-app-router/src/i18n/request.ts
+++ b/examples/example-app-router/src/i18n/request.ts
@@ -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 (locale === 'en'
? // When using Turbopack, this will enable HMR for `en`
diff --git a/examples/example-app-router/tests/main.spec.ts b/examples/example-app-router/tests/main.spec.ts
index b597def9b..0456be01b 100644
--- a/examples/example-app-router/tests/main.spec.ts
+++ b/examples/example-app-router/tests/main.spec.ts
@@ -28,9 +28,10 @@ it("handles not found pages for routes that don't match the middleware", async (
page
}) => {
await page.goto('/test.png');
- page.getByRole('heading', {name: 'This page could not be found.'});
+ page.getByRole('heading', {name: 'Page not found'});
+
await page.goto('/api/hello');
- page.getByRole('heading', {name: 'This page could not be found.'});
+ page.getByRole('heading', {name: 'Page not found'});
});
it('sets caching headers', async ({request}) => {
diff --git a/packages/next-intl/.size-limit.ts b/packages/next-intl/.size-limit.ts
index 3e357fb49..b338be391 100644
--- a/packages/next-intl/.size-limit.ts
+++ b/packages/next-intl/.size-limit.ts
@@ -9,7 +9,7 @@ const config: SizeLimitConfig = [
{
name: 'import * from \'next-intl\' (react-server)',
path: 'dist/production/index.react-server.js',
- limit: '14.665 KB'
+ limit: '14.725 KB'
},
{
name: 'import {createSharedPathnamesNavigation} from \'next-intl/navigation\' (react-client)',
@@ -33,19 +33,19 @@ const config: SizeLimitConfig = [
name: 'import {createSharedPathnamesNavigation} from \'next-intl/navigation\' (react-server)',
path: 'dist/production/navigation.react-server.js',
import: '{createSharedPathnamesNavigation}',
- limit: '16.515 KB'
+ limit: '16.635 KB'
},
{
name: 'import {createLocalizedPathnamesNavigation} from \'next-intl/navigation\' (react-server)',
path: 'dist/production/navigation.react-server.js',
import: '{createLocalizedPathnamesNavigation}',
- limit: '16.545 KB'
+ limit: '16.635 KB'
},
{
name: 'import {createNavigation} from \'next-intl/navigation\' (react-server)',
path: 'dist/production/navigation.react-server.js',
import: '{createNavigation}',
- limit: '16.495 KB'
+ limit: '16.645 KB'
},
{
name: 'import * from \'next-intl/server\' (react-client)',
@@ -55,7 +55,7 @@ const config: SizeLimitConfig = [
{
name: 'import * from \'next-intl/server\' (react-server)',
path: 'dist/production/server.react-server.js',
- limit: '13.865 KB'
+ limit: '13.995 KB'
},
{
name: 'import createMiddleware from \'next-intl/middleware\'',
diff --git a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.test.tsx b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.test.tsx
index d1e307eb8..848e5d1c6 100644
--- a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.test.tsx
+++ b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.test.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import {renderToString} from 'react-dom/server';
import {it, describe, vi, expect, beforeEach} from 'vitest';
import {defineRouting, Pathnames} from '../routing';
-import {getRequestLocale} from '../server/react-server/RequestLocale';
+import {getRequestLocale} from '../server/react-server/RequestLocaleLegacy';
import {getLocalePrefix} from '../shared/utils';
import createLocalizedPathnamesNavigationClient from './react-client/createLocalizedPathnamesNavigation';
import createLocalizedPathnamesNavigationServer from './react-server/createLocalizedPathnamesNavigation';
@@ -48,7 +48,7 @@ vi.mock('../../src/navigation/react-server/ServerLink', () => ({
);
}
}));
-vi.mock('../../src/server/react-server/RequestLocale', () => ({
+vi.mock('../../src/server/react-server/RequestLocaleLegacy', () => ({
getRequestLocale: vi.fn(() => 'en')
}));
diff --git a/packages/next-intl/src/navigation/createNavigation.test.tsx b/packages/next-intl/src/navigation/createNavigation.test.tsx
index c7d2978d3..e244ea673 100644
--- a/packages/next-intl/src/navigation/createNavigation.test.tsx
+++ b/packages/next-intl/src/navigation/createNavigation.test.tsx
@@ -31,7 +31,6 @@ function mockCurrentLocale(locale: string) {
(localePromise as any).status = 'fulfilled';
(localePromise as any).value = locale;
- // @ts-expect-error -- Async values are allowed
vi.mocked(getRequestLocale).mockImplementation(() => localePromise);
vi.mocked(nextUseParams<{locale: string}>).mockImplementation(() => ({
diff --git a/packages/next-intl/src/navigation/createSharedPathnamesNavigation.test.tsx b/packages/next-intl/src/navigation/createSharedPathnamesNavigation.test.tsx
index b54612181..29868c92a 100644
--- a/packages/next-intl/src/navigation/createSharedPathnamesNavigation.test.tsx
+++ b/packages/next-intl/src/navigation/createSharedPathnamesNavigation.test.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import {renderToString} from 'react-dom/server';
import {it, describe, vi, expect, beforeEach} from 'vitest';
import {defineRouting} from '../routing';
-import {getRequestLocale} from '../server/react-server/RequestLocale';
+import {getRequestLocale} from '../server/react-server/RequestLocaleLegacy';
import {getLocalePrefix} from '../shared/utils';
import createSharedPathnamesNavigationClient from './react-client/createSharedPathnamesNavigation';
import createSharedPathnamesNavigationServer from './react-server/createSharedPathnamesNavigation';
@@ -48,7 +48,7 @@ vi.mock('../../src/navigation/react-server/ServerLink', () => ({
);
}
}));
-vi.mock('../../src/server/react-server/RequestLocale', () => ({
+vi.mock('../../src/server/react-server/RequestLocaleLegacy', () => ({
getRequestLocale: vi.fn(() => 'en')
}));
diff --git a/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx
index 808f45484..01e05e493 100644
--- a/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx
+++ b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx
@@ -9,7 +9,7 @@ import {
Locales,
Pathnames
} from '../../routing/types';
-import {getRequestLocale} from '../../server/react-server/RequestLocale';
+import {getRequestLocale} from '../../server/react-server/RequestLocaleLegacy';
import {ParametersExceptFirst} from '../../shared/types';
import {
HrefOrHrefWithParams,
diff --git a/packages/next-intl/src/navigation/react-server/createNavigation.tsx b/packages/next-intl/src/navigation/react-server/createNavigation.tsx
index 8e5f5c0bc..13055861a 100644
--- a/packages/next-intl/src/navigation/react-server/createNavigation.tsx
+++ b/packages/next-intl/src/navigation/react-server/createNavigation.tsx
@@ -35,7 +35,7 @@ export default function createNavigation<
type Locale = AppLocales extends never ? string : AppLocales[number];
function getLocale() {
- return getRequestLocale() as Locale;
+ return getRequestLocale() as Promise;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
diff --git a/packages/next-intl/src/navigation/react-server/redirects.tsx b/packages/next-intl/src/navigation/react-server/redirects.tsx
index 435e5dfa9..8b848a9cf 100644
--- a/packages/next-intl/src/navigation/react-server/redirects.tsx
+++ b/packages/next-intl/src/navigation/react-server/redirects.tsx
@@ -1,4 +1,4 @@
-import {getRequestLocale} from '../../server/react-server/RequestLocale';
+import {getRequestLocale} from '../../server/react-server/RequestLocaleLegacy';
import {ParametersExceptFirst} from '../../shared/types';
import {baseRedirect, basePermanentRedirect} from '../shared/redirects';
diff --git a/packages/next-intl/src/server/react-client/index.test.tsx b/packages/next-intl/src/server/react-client/index.test.tsx
index a97ad1c2a..de0a04679 100644
--- a/packages/next-intl/src/server/react-client/index.test.tsx
+++ b/packages/next-intl/src/server/react-client/index.test.tsx
@@ -14,8 +14,8 @@ describe('getRequestConfig', () => {
const getConfig = getRequestConfig(({locale}) => ({
messages: {hello: 'Hello ' + locale}
}));
- expect(() => getConfig({locale: 'en'})).toThrow(
- '`getRequestConfig` is not supported in Client Components.'
- );
+ expect(() =>
+ getConfig({locale: 'en', requestLocale: Promise.resolve('en')})
+ ).toThrow('`getRequestConfig` is not supported in Client Components.');
});
});
diff --git a/packages/next-intl/src/server/react-server/RequestLocale.tsx b/packages/next-intl/src/server/react-server/RequestLocale.tsx
index 970bb4de5..e92e93397 100644
--- a/packages/next-intl/src/server/react-server/RequestLocale.tsx
+++ b/packages/next-intl/src/server/react-server/RequestLocale.tsx
@@ -1,51 +1,43 @@
import {headers} from 'next/headers';
-import {notFound} from 'next/navigation';
import {cache} from 'react';
import {HEADER_LOCALE_NAME} from '../../shared/constants';
+import {getCachedRequestLocale} from './RequestLocaleCache';
-function getLocaleFromHeaderImpl() {
+async function getHeadersImpl(): Promise {
+ const promiseOrValue = headers();
+
+ // Compatibility with Next.js <15
+ return promiseOrValue instanceof Promise
+ ? await promiseOrValue
+ : promiseOrValue;
+}
+const getHeaders = cache(getHeadersImpl);
+
+async function getLocaleFromHeaderImpl(): Promise {
let locale;
try {
- locale = headers().get(HEADER_LOCALE_NAME);
+ locale = (await getHeaders()).get(HEADER_LOCALE_NAME) || undefined;
} catch (error) {
if (
error instanceof Error &&
(error as any).digest === 'DYNAMIC_SERVER_USAGE'
) {
- throw new Error(
+ const wrappedError = new Error(
'Usage of next-intl APIs in Server Components currently opts into dynamic rendering. This limitation will eventually be lifted, but as a stopgap solution, you can use the `unstable_setRequestLocale` API to enable static rendering, see https://next-intl-docs.vercel.app/docs/getting-started/app-router/with-i18n-routing#static-rendering',
{cause: error}
);
+ (wrappedError as any).digest = (error as any).digest;
+ throw wrappedError;
} else {
throw error;
}
}
- if (!locale) {
- if (process.env.NODE_ENV !== 'production') {
- console.error(
- `\nUnable to find \`next-intl\` locale because the middleware didn't run on this request. See https://next-intl-docs.vercel.app/docs/routing/middleware#unable-to-find-locale. The \`notFound()\` function will be called as a result.\n`
- );
- }
- notFound();
- }
-
return locale;
}
const getLocaleFromHeader = cache(getLocaleFromHeaderImpl);
-// Workaround until `createServerContext` is available
-function getCacheImpl() {
- const value: {locale?: string} = {locale: undefined};
- return value;
-}
-const getCache = cache(getCacheImpl);
-
-export function setRequestLocale(locale: string) {
- getCache().locale = locale;
-}
-
-export function getRequestLocale(): string {
- return getCache().locale || getLocaleFromHeader();
+export async function getRequestLocale() {
+ return getCachedRequestLocale() || (await getLocaleFromHeader());
}
diff --git a/packages/next-intl/src/server/react-server/RequestLocaleCache.tsx b/packages/next-intl/src/server/react-server/RequestLocaleCache.tsx
new file mode 100644
index 000000000..a8bc80194
--- /dev/null
+++ b/packages/next-intl/src/server/react-server/RequestLocaleCache.tsx
@@ -0,0 +1,17 @@
+import {cache} from 'react';
+
+// See https://github.com/vercel/next.js/discussions/58862
+function getCacheImpl() {
+ const value: {locale?: string} = {locale: undefined};
+ return value;
+}
+
+const getCache = cache(getCacheImpl);
+
+export function getCachedRequestLocale() {
+ return getCache().locale;
+}
+
+export function setCachedRequestLocale(locale: string) {
+ getCache().locale = locale;
+}
diff --git a/packages/next-intl/src/server/react-server/RequestLocaleLegacy.tsx b/packages/next-intl/src/server/react-server/RequestLocaleLegacy.tsx
new file mode 100644
index 000000000..31727d6d9
--- /dev/null
+++ b/packages/next-intl/src/server/react-server/RequestLocaleLegacy.tsx
@@ -0,0 +1,48 @@
+import {headers} from 'next/headers';
+import {notFound} from 'next/navigation';
+import {cache} from 'react';
+import {HEADER_LOCALE_NAME} from '../../shared/constants';
+import {getCachedRequestLocale} from './RequestLocaleCache';
+
+// This was originally built for Next.js <14, where `headers()` was not async.
+// With https://github.com/vercel/next.js/pull/68812, the API became async.
+// This file can be removed once we remove the legacy navigation APIs.
+function getHeaders() {
+ return headers();
+}
+
+function getLocaleFromHeaderImpl() {
+ let locale;
+
+ try {
+ locale = getHeaders().get(HEADER_LOCALE_NAME);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ (error as any).digest === 'DYNAMIC_SERVER_USAGE'
+ ) {
+ throw new Error(
+ 'Usage of next-intl APIs in Server Components currently opts into dynamic rendering. This limitation will eventually be lifted, but as a stopgap solution, you can use the `unstable_setRequestLocale` API to enable static rendering, see https://next-intl-docs.vercel.app/docs/getting-started/app-router/with-i18n-routing#static-rendering',
+ {cause: error}
+ );
+ } else {
+ throw error;
+ }
+ }
+
+ if (!locale) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.error(
+ `\nUnable to find \`next-intl\` locale because the middleware didn't run on this request. See https://next-intl-docs.vercel.app/docs/routing/middleware#unable-to-find-locale. The \`notFound()\` function will be called as a result.\n`
+ );
+ }
+ notFound();
+ }
+
+ return locale;
+}
+const getLocaleFromHeader = cache(getLocaleFromHeaderImpl);
+
+export function getRequestLocale(): string {
+ return getCachedRequestLocale() || getLocaleFromHeader();
+}
diff --git a/packages/next-intl/src/server/react-server/createRequestConfig.tsx b/packages/next-intl/src/server/react-server/createRequestConfig.tsx
index 29013cbbd..56abb735a 100644
--- a/packages/next-intl/src/server/react-server/createRequestConfig.tsx
+++ b/packages/next-intl/src/server/react-server/createRequestConfig.tsx
@@ -1,8 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import getRuntimeConfig from 'next-intl/config';
-import type {IntlConfig} from 'use-intl/core';
-import type {GetRequestConfigParams} from './getRequestConfig';
+import type {GetRequestConfigParams, RequestConfig} from './getRequestConfig';
export default getRuntimeConfig as unknown as (
params: GetRequestConfigParams
-) => IntlConfig | Promise;
+) => RequestConfig | Promise;
diff --git a/packages/next-intl/src/server/react-server/getConfig.tsx b/packages/next-intl/src/server/react-server/getConfig.tsx
index 32c0268fb..0a789b0ba 100644
--- a/packages/next-intl/src/server/react-server/getConfig.tsx
+++ b/packages/next-intl/src/server/react-server/getConfig.tsx
@@ -1,3 +1,4 @@
+import {notFound} from 'next/navigation';
import {cache} from 'react';
import {
initializeConfig,
@@ -6,7 +7,9 @@ import {
_createCache
} from 'use-intl/core';
import {getRequestLocale} from './RequestLocale';
+import {getRequestLocale as getRequestLocaleLegacy} from './RequestLocaleLegacy';
import createRequestConfig from './createRequestConfig';
+import {GetRequestConfigParams} from './getRequestConfig';
// Make sure `now` is consistent across the request in case none was configured
function getDefaultNowImpl() {
@@ -41,15 +44,18 @@ See also: https://next-intl-docs.vercel.app/docs/usage/configuration#i18n-reques
);
}
- let hasReadLocale = false;
-
- // In case the consumer doesn't read `params.locale` and instead provides the
- // `locale` (either in a single-language workflow or because the locale is
- // read from the user settings), don't attempt to read the request locale.
- const params = {
+ const params: GetRequestConfigParams = {
+ // In case the consumer doesn't read `params.locale` and instead provides the
+ // `locale` (either in a single-language workflow or because the locale is
+ // read from the user settings), don't attempt to read the request locale.
get locale() {
- hasReadLocale = true;
- return localeOverride || getRequestLocale();
+ return localeOverride || getRequestLocaleLegacy();
+ },
+
+ get requestLocale() {
+ return localeOverride
+ ? Promise.resolve(localeOverride)
+ : getRequestLocale();
}
};
@@ -58,25 +64,20 @@ See also: https://next-intl-docs.vercel.app/docs/usage/configuration#i18n-reques
result = await result;
}
- if (process.env.NODE_ENV !== 'production') {
- if (hasReadLocale) {
- if (result.locale) {
- console.error(
- "\nYou've read the `locale` param that was passed to `getRequestConfig` but have also returned one from the function. This is likely an error, please ensure that you're consistently using a setup with or without i18n routing: https://next-intl-docs.vercel.app/docs/getting-started/app-router\n"
- );
- }
- } else {
- if (!result.locale) {
- console.error(
- "\nYou haven't read the `locale` param that was passed to `getRequestConfig` and also haven't returned one from the function. This is likely an error, please ensure that you're consistently using a setup with or without i18n routing: https://next-intl-docs.vercel.app/docs/getting-started/app-router\n"
- );
- }
+ const locale = result.locale || (await params.requestLocale);
+
+ if (!locale) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.error(
+ `\nUnable to find \`next-intl\` locale because the middleware didn't run on this request and no \`locale\` was returned in \`getRequestConfig\`. See https://next-intl-docs.vercel.app/docs/routing/middleware#unable-to-find-locale. The \`notFound()\` function will be called as a result.\n`
+ );
}
+ notFound();
}
return {
...result,
- locale: result.locale || params.locale,
+ locale,
now: result.now || getDefaultNow(),
timeZone: result.timeZone || getDefaultTimeZone()
};
diff --git a/packages/next-intl/src/server/react-server/getRequestConfig.tsx b/packages/next-intl/src/server/react-server/getRequestConfig.tsx
index d00645709..e735d4be6 100644
--- a/packages/next-intl/src/server/react-server/getRequestConfig.tsx
+++ b/packages/next-intl/src/server/react-server/getRequestConfig.tsx
@@ -1,23 +1,51 @@
import type {IntlConfig} from 'use-intl/core';
-type RequestConfig = Omit & {
+export type RequestConfig = Omit & {
/**
- * Instead of reading a `locale` from the argument that's passed to the
+ * Instead of reading a `requestLocale` from the argument that's passed to the
* function within `getRequestConfig`, you can include a locale as part of the
* returned request configuration.
*
- * This is helpful for apps that only support a single language and for apps
- * where the locale should be read from user settings instead of the pathname.
+ * This can be helpful for the following use cases:
+ * - Apps that only support a single language
+ * - Apps where the locale should be read from user settings instead of the pathname
+ * - Providing a fallback locale in case the locale was not matched by the middleware
**/
locale?: IntlConfig['locale'];
};
export type GetRequestConfigParams = {
+ /**
+ * Deprecated in favor of `requestLocale` (see https://github.com/amannn/next-intl/pull/1383).
+ *
+ * The locale that was matched by the `[locale]` path segment. Note however
+ * that this can be overridden in async APIs when the `locale` is explicitly
+ * passed (e.g. `getTranslations({locale: 'en'})`).
+ *
+ * @deprecated
+ */
locale: string;
+
+ /**
+ * Typically corresponds to the `[locale]` segment that was matched by the middleware.
+ *
+ * However, there are three special cases to consider:
+ * 1. **Overrides**: When an explicit `locale` is passed to awaitable functions
+ * like `getTranslations({locale: 'en'})`, then this value will be used
+ * instead of the segment.
+ * 2. **`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`).
+ * 3. **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.
+ *
+ * @see https://next-intl-docs.vercel.app/docs/usage/configuration#i18n-request
+ */
+ requestLocale: Promise;
};
/**
- * Should be called in `i18n.ts` to create the configuration for the current request.
+ * Should be called in `i18n/request.ts` to create the configuration for the current request.
*/
export default function getRequestConfig(
createRequestConfig: (
diff --git a/packages/next-intl/src/server/react-server/index.tsx b/packages/next-intl/src/server/react-server/index.tsx
index c3d85a3ea..0ce84ce95 100644
--- a/packages/next-intl/src/server/react-server/index.tsx
+++ b/packages/next-intl/src/server/react-server/index.tsx
@@ -10,4 +10,4 @@ export {default as getTranslations} from './getTranslations';
export {default as getMessages} from './getMessages';
export {default as getLocale} from './getLocale';
-export {setRequestLocale as unstable_setRequestLocale} from './RequestLocale';
+export {setCachedRequestLocale as unstable_setRequestLocale} from './RequestLocaleCache';