diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 09e61d078..8f1cdb6ff 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,7 +7,7 @@ on:
jobs:
build:
name: Build, lint, and test
- runs-on: ubuntu-latest
+ runs-on: macos-15
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease-canary.yml
similarity index 87%
rename from .github/workflows/prerelease.yml
rename to .github/workflows/prerelease-canary.yml
index 19a49df2a..5468e5145 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease-canary.yml
@@ -1,4 +1,4 @@
-name: prerelease
+name: prerelease (canary)
on:
push:
@@ -26,7 +26,7 @@ jobs:
- run: |
sed -i 's/"use-intl": "workspace:\^"/"use-intl": "workspace:"/' ./packages/next-intl/package.json && git commit -am "use fixed version"
- run: pnpm lerna publish 0.0.0-canary-${GITHUB_SHA::7} --no-git-reset --dist-tag canary --no-push --yes
- if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ')}}"
+ if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ') || startsWith(github.event.head_commit.message, 'feat!: ')}}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e6473ff40..9e251c2d3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,7 +24,7 @@ jobs:
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
- run: pnpm run publish
- if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ')}}"
+ if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ') || startsWith(github.event.head_commit.message, 'feat!: ')}}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 8a14edf8d..924523876 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -86,7 +86,7 @@ Note that the exclamation mark syntax (`!`) for indicating breaking changes is c
Other prefixes that are allowed and will _not_ create a release are the following:
-1. `docs`: Documentation-only changes
+1. `docs`: Documentation-only changes and updated examples
2. `test`: Missing tests were added or existing ones corrected
3. `ci`: Changes to CI configuration files and scripts
4. `build`: Changes that affect the build system or external dependencies
diff --git a/docs/src/components/Footer.tsx b/docs/src/components/Footer.tsx
index 81b28f852..8e51c832a 100644
--- a/docs/src/components/Footer.tsx
+++ b/docs/src/components/Footer.tsx
@@ -2,6 +2,7 @@ import {useRouter} from 'next/router';
import config from '@/config';
import FooterLink from './FooterLink';
import FooterSeparator from './FooterSeparator';
+import FooterVersionSelector from './FooterVersionSelector';
export default function Footer() {
const router = useRouter();
@@ -19,6 +20,8 @@ export default function Footer() {
Examples
Blog
+
+
diff --git a/docs/src/components/FooterVersionSelector.tsx b/docs/src/components/FooterVersionSelector.tsx
new file mode 100644
index 000000000..ee21de8c8
--- /dev/null
+++ b/docs/src/components/FooterVersionSelector.tsx
@@ -0,0 +1,19 @@
+import {ChangeEvent} from 'react';
+
+export default function FooterVersionSelector() {
+ function onChange(event: ChangeEvent) {
+ const version = event.target.value;
+ window.location.href = `https://${version}.next-intl.dev`;
+ }
+
+ return (
+
+ );
+}
diff --git a/docs/src/pages/blog/_meta.tsx b/docs/src/pages/blog/_meta.tsx
index d2289e251..badbdc76f 100644
--- a/docs/src/pages/blog/_meta.tsx
+++ b/docs/src/pages/blog/_meta.tsx
@@ -3,7 +3,7 @@ export default {
title: 'Overview'
},
'next-intl-4-0': {
- title: 'next-intl 4.0 beta',
+ title: 'next-intl 4.0',
display: 'hidden'
},
'next-intl-3-22': {
diff --git a/docs/src/pages/blog/index.mdx b/docs/src/pages/blog/index.mdx
index 5bbe075db..736da7080 100644
--- a/docs/src/pages/blog/index.mdx
+++ b/docs/src/pages/blog/index.mdx
@@ -6,8 +6,8 @@ import StayUpdated from '@/components/StayUpdated.mdx';
Dec 23, 2024 · by Jan Amann
+Mar 12, 2025 · by Jan Amann
After a year of feature development, this release focuses on streamlining the API surface while maintaining the core architecture of `next-intl`. With many improvements already released in [previous minor versions](/blog/next-intl-3-22), this update introduces several enhancements that will improve your development experience and make working with internationalization even more seamless.
@@ -44,7 +44,7 @@ declare module 'next-intl' {
}
```
-See the updated [TypeScript augmentation](https://v4.next-intl.dev/docs/workflows/typescript) guide.
+See the updated [TypeScript augmentation](/docs/workflows/typescript) guide.
## Strictly-typed locale
@@ -65,7 +65,7 @@ declare module 'next-intl' {
By doing so, APIs like `useLocale()` or `` that either return or receive a `locale` will now pick up your app-specific `Locale` type, improving type safety across your app.
-To simplify narrowing of `string`-based locales, a `hasLocale` function has been added. This can for example be used in [`i18n/request.ts`](https://v4.next-intl.dev/docs/getting-started/app-router/with-i18n-routing#i18n-request) to return a valid locale:
+To simplify narrowing of `string`-based locales, a `hasLocale` function has been added. This can for example be used in [`i18n/request.ts`](/docs/getting-started/app-router/with-i18n-routing#i18n-request) to return a valid locale:
```tsx
import {getRequestConfig} from 'next-intl/server';
@@ -148,13 +148,13 @@ t('followers', {count: 30000});
"{count, number} followers"
```
-Due to a current limitation in TypeScript, this feature is opt-in for now. Please refer to the [strict arguments](https://v4.next-intl.dev/docs/workflows/typescript#messages-arguments) docs to learn how to enable it.
+Due to a current limitation in TypeScript, this feature is opt-in for now. Please refer to the [strict arguments](/docs/workflows/typescript#messages-arguments) docs to learn how to enable it.
## GDPR compliance [#gdpr-compliance]
In order to comply with the current GDPR regulations, the following changes have been made and are relevant to you if you're using the `next-intl` middleware for i18n routing:
-1. The locale cookie now defaults to a session cookie that expires when a browser is closed.
+1. The locale cookie now defaults to a session cookie that expires when the browser is closed.
2. The locale cookie is now only set when a user switches to a locale that doesn't match the `accept-language` header.
If you want to increase the cookie expiration, e.g. because you're informing users about the usage of cookies or if GDPR doesn't apply to your app, you can use the `maxAge` attribute to do so:
@@ -174,11 +174,11 @@ export const routing = defineRouting({
});
```
-Since the cookie is now only available after a locale switch, make sure to not rely on it always being present. E.g. if you need access to the user's locale in a [Route Handler](https://v4.next-intl.dev/docs/environments/actions-metadata-route-handlers#route-handlers), a reliable option is to provide the locale as a search param (e.g. `/api/posts/12?locale=en`).
+Since the cookie is now only available after a locale switch, make sure to not rely on it always being present. E.g. if you need access to the user's locale in a [Route Handler](/docs/environments/actions-metadata-route-handlers#route-handlers), a reliable option is to provide the locale as a search param (e.g. `/api/posts/12?locale=en`).
-As part of this change, disabling a cookie now requires you to set [`localeCookie: false`](https://v4.next-intl.dev/docs/routing#locale-cookie) in your routing configuration. Previously, `localeDetection: false` ambiguously also disabled the cookie from being set, but since a separate `localeCookie` option was introduced recently, this should now be used instead.
+As part of this change, disabling a cookie now requires you to set [`localeCookie: false`](/docs/routing#locale-cookie) in your routing configuration. Previously, `localeDetection: false` ambiguously also disabled the cookie from being set, but since a separate `localeCookie` option was introduced recently, this should now be used instead.
-Learn more in the [locale cookie](https://v4.next-intl.dev/docs/routing#locale-cookie) docs.
+Learn more in the [locale cookie](/docs/routing#locale-cookie) docs.
## Modernized build output
@@ -266,7 +266,7 @@ This will create the following structure:
- `example.no`: `no-NO`
- `example.no/en`: `en-NO`
-Learn more in the updated docs for [`domains`](https://v4.next-intl.dev/docs/routing#domains).
+Learn more in the updated docs for [`domains`](/docs/routing#domains).
## Preparation for upcoming Next.js features [#nextjs-future]
@@ -300,11 +300,9 @@ For a smooth upgrade, please initially upgrade to the latest v3.x version and ch
Afterwards, you can upgrade by running:
```
-npm install next-intl@v4-beta
+npm install next-intl@4
```
-The beta docs are available here: [v4.next-intl.dev](https://v4.next-intl.dev)
-
I'd love to hear about your experiences with `next-intl@4.0`! Join the conversation in the [discussions](https://github.com/amannn/next-intl/discussions/1631).
## Thank you!
@@ -315,6 +313,8 @@ A special thank you goes to Crow
—Jan
+(this post has been updated from an initial announcement for the 3.0 release candidate)
+
PS: Have you heard that [learn.next-intl.dev](https://learn.next-intl.dev) is coming?
diff --git a/docs/src/pages/docs/environments/actions-metadata-route-handlers.mdx b/docs/src/pages/docs/environments/actions-metadata-route-handlers.mdx
index 1111998f5..1a7cd17bf 100644
--- a/docs/src/pages/docs/environments/actions-metadata-route-handlers.mdx
+++ b/docs/src/pages/docs/environments/actions-metadata-route-handlers.mdx
@@ -173,8 +173,9 @@ Note that by default, `next-intl` returns [the `link` response header](/docs/rou
Next.js supports providing alternate URLs per language via the [`alternates` entry](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generate-a-localized-sitemap). You can construct a list of entries for each pathname and locale as follows:
-```tsx filename="app/sitemap.ts" {4-5,8-9}
+```tsx filename="app/sitemap.ts" {5-6,9-10}
import {MetadataRoute} from 'next';
+import {Locale} from 'next-intl';
import {routing, getPathname} from '@/i18n/routing';
// Adapt this as necessary
@@ -198,7 +199,7 @@ function getEntries(href: Href) {
}));
}
-function getUrl(href: Href, locale: (typeof routing.locales)[number]) {
+function getUrl(href: Href, locale: Locale) {
const pathname = getPathname({locale, href});
return host + pathname;
}
@@ -230,12 +231,17 @@ You can use `next-intl` in [Route Handlers](https://nextjs.org/docs/app/building
```tsx filename="app/api/hello/route.tsx"
import {NextResponse} from 'next/server';
+import {hasLocale} from 'next-intl';
import {getTranslations} from 'next-intl/server';
+import {routing} from '@/i18n/routing';
export async function GET(request) {
// Example: Receive the `locale` via a search param
const {searchParams} = new URL(request.url);
const locale = searchParams.get('locale');
+ if (!hasLocale(routing.locales, locale)) {
+ return NextResponse.json({error: 'Invalid locale'}, {status: 400});
+ }
const t = await getTranslations({locale, namespace: 'Hello'});
return NextResponse.json({title: t('title')});
diff --git a/docs/src/pages/docs/environments/error-files.mdx b/docs/src/pages/docs/environments/error-files.mdx
index 1d6c60190..c8f4001c6 100644
--- a/docs/src/pages/docs/environments/error-files.mdx
+++ b/docs/src/pages/docs/environments/error-files.mdx
@@ -77,16 +77,16 @@ export default function RootLayout({children}) {
}
```
-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.
+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 valid.
```tsx filename="app/[locale]/layout.tsx"
+import {hasLocale} from 'next-intl';
import {notFound} from 'next/navigation';
+import {routing} from '@/i18n/routing';
-export default async function LocaleLayout({children, params}) {
+export default function LocaleLayout({children, params}) {
const {locale} = await params;
-
- // Ensure that the incoming `locale` is valid
- if (!routing.locales.includes(locale as any)) {
+ if (!hasLocale(routing.locales, locale)) {
notFound();
}
diff --git a/docs/src/pages/docs/environments/server-client-components.mdx b/docs/src/pages/docs/environments/server-client-components.mdx
index 0c8a1dbca..865e5201b 100644
--- a/docs/src/pages/docs/environments/server-client-components.mdx
+++ b/docs/src/pages/docs/environments/server-client-components.mdx
@@ -69,7 +69,7 @@ These functions are available:
Components that aren't declared with the `async` keyword and don't use interactive features like `useState`, are referred to as [shared components](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#sharing-code-between-server-and-client). These can render either as a Server or Client Component, depending on where they are imported from.
-In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components.
+In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components:
```tsx filename="UserDetails.tsx"
import {useTranslations} from 'next-intl';
@@ -77,6 +77,9 @@ import {useTranslations} from 'next-intl';
export default function UserDetails({user}) {
const t = useTranslations('UserProfile');
+ // This component will execute as a Server Component by default.
+ // However, if it is imported from a Client Component, it will
+ // execute as a Client Component.
return (
{t('title')}
@@ -112,11 +115,13 @@ Regarding performance, async functions and hooks can be used interchangeably. Th
## Using internationalization in Client Components
-Depending on your situation, you may need to handle internationalization in Client Components. While providing all messages to the client side is typically the easiest way to [get started](/docs/getting-started/app-router#layout) and a reasonable approach for many apps, you can be more selective about which messages are passed to the client side if you're interested in optimizing the performance of your app.
+Depending on your situation, you may need to handle internationalization in Client Components. Providing all messages to the client side is the easiest way to get started, therefore `next-intl` automatically does this when you render [`NextIntlClientProvider`](/docs/usage/configuration#nextintlclientprovider). This is a reasonable approach for many apps.
+
+However, you can be more selective about which messages are passed to the client side if you're interested in optimizing the performance of your app.
There are several options for using translations from `next-intl` in Client Components, listed here in order of enabling the best performance:
-### Option 1: Passing translations to Client Components
+### Option 1: Passing translated labels to Client Components
The preferred approach is to pass the processed labels as props or `children` from a Server Component.
@@ -275,8 +280,6 @@ In particular, page and search params are often a great option because they offe
### Option 3: Providing individual messages
-To reduce bundle size, `next-intl` doesn't automatically provide [messages](/docs/usage/configuration#messages) or [formats](/docs/usage/configuration#formats) to Client Components.
-
If you need to incorporate dynamic state into components that can not be moved to the server side, you can wrap these components with `NextIntlClientProvider` and provide the relevant messages.
```tsx filename="Counter.tsx"
@@ -312,22 +315,16 @@ An automatic, compiler-driven approach is being evaluated in [`next-intl#1`](htt
### Option 4: Providing all messages
-If you're building a highly dynamic app where most components use React's interactive features, you may prefer to make all messages available to Client Components.
+If you're building a highly dynamic app where most components use React's interactive features, you may prefer to make all messages available to Client Components—this is the default behavior of `next-intl`.
```tsx filename="layout.tsx" /NextIntlClientProvider/
import {NextIntlClientProvider} from 'next-intl';
-import {getMessages} from 'next-intl/server';
export default async function RootLayout(/* ... */) {
- // Receive messages provided in `i18n/request.ts`
- const messages = await getMessages();
-
return (
-
- {children}
-
+ {children}
);
@@ -366,7 +363,6 @@ The component accepts the following props that are not serializable:
1. [`onError`](/docs/usage/configuration#error-handling)
2. [`getMessageFallback`](/docs/usage/configuration#error-handling)
-3. Rich text elements for [`defaultTranslationValues`](/docs/usage/configuration#default-translation-values)
To configure these, you can wrap `NextIntlClientProvider` with another component that is marked with `'use client'` and defines the relevant props.
diff --git a/docs/src/pages/docs/getting-started/app-router/with-i18n-routing.mdx b/docs/src/pages/docs/getting-started/app-router/with-i18n-routing.mdx
index e1cc96fe4..45adc4f02 100644
--- a/docs/src/pages/docs/getting-started/app-router/with-i18n-routing.mdx
+++ b/docs/src/pages/docs/getting-started/app-router/with-i18n-routing.mdx
@@ -62,7 +62,7 @@ The simplest option is to add JSON files in your local project folder:
### `next.config.ts` [#next-config]
-Now, set up the plugin which creates an alias to provide a request-specific i18n configuration to Server Components—more on this in the following steps.
+Now, set up the plugin which creates an alias to provide a request-specific i18n configuration like your messages to Server Components—more on this in the following steps.
@@ -128,6 +128,8 @@ Once we have our routing configuration in place, we can use it to set up the nav
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';
+// Lightweight wrappers around Next.js' navigation
+// APIs that consider the routing configuration
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);
```
@@ -143,27 +145,53 @@ import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
- // Match only internationalized pathnames
- matcher: ['/', '/(de|en)/:path*']
+ // Match all pathnames except for
+ // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
+ // - … the ones containing a dot (e.g. `favicon.ico`)
+ matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
```
+
+ How can I match pathnames that contain dots like `/users/jane.doe`?
+
+If you have pathnames where dots are expected, you can match them with explicit entries:
+
+```tsx filename="src/middleware.ts" {10,11}
+// ...
+
+export const config = {
+ matcher: [
+ // Match all pathnames except for
+ // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
+ // - … the ones containing a dot (e.g. `favicon.ico`)
+ '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
+
+ // Match all pathnames within `{/:locale}/users`
+ '/([\\w-]+)?/users/(.+)'
+ ];
+}
+```
+
+This will match e.g. `/users/jane.doe`, also optionally with a locale prefix.
+
+
+
### `src/i18n/request.ts` [#i18n-request]
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 {getRequestConfig} from 'next-intl/server';
+import {hasLocale} from 'next-intl';
import {routing} from './routing';
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;
- }
+ // Typically corresponds to the `[locale]` segment
+ const requested = await requestLocale;
+ const locale = hasLocale(routing.locales, requested)
+ ? requested
+ : routing.defaultLocale;
return {
locale,
@@ -193,8 +221,7 @@ const withNextIntl = createNextIntlPlugin(
The `locale` that was matched by the middleware is available via the `locale` param and can be used to configure the document language. Additionally, we can use this place to pass configuration from `i18n/request.ts` to Client Components via `NextIntlClientProvider`.
```tsx filename="app/[locale]/layout.tsx"
-import {NextIntlClientProvider} from 'next-intl';
-import {getMessages} from 'next-intl/server';
+import {NextIntlClientProvider, Locale, hasLocale} from 'next-intl';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';
@@ -207,28 +234,20 @@ export default async function LocaleLayout({
}) {
// Ensure that the incoming `locale` is valid
const {locale} = await params;
- if (!routing.locales.includes(locale as any)) {
+ if (!hasLocale(routing.locales, locale)) {
notFound();
}
- // Providing all messages to the client
- // side is the easiest way to get started
- const messages = await getMessages();
-
return (
-
- {children}
-
+ {children}
);
}
```
-Note that `NextIntlClientProvider` automatically inherits configuration from `i18n/request.ts` here, but `messages` need to be passed explicitly.
-
### `src/app/[locale]/page.tsx` [#page]
And that's it!
@@ -305,14 +324,13 @@ export function generateStaticParams() {
```tsx filename="app/[locale]/layout.tsx"
import {setRequestLocale} from 'next-intl/server';
+import {hasLocale} from 'next-intl';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';
export default async function LocaleLayout({children, params}) {
const {locale} = await params;
-
- // Ensure that the incoming `locale` is valid
- if (!routing.locales.includes(locale as any)) {
+ if (!hasLocale(routing.locales, locale)) {
notFound();
}
diff --git a/docs/src/pages/docs/getting-started/app-router/without-i18n-routing.mdx b/docs/src/pages/docs/getting-started/app-router/without-i18n-routing.mdx
index 890ccad34..af25be7c0 100644
--- a/docs/src/pages/docs/getting-started/app-router/without-i18n-routing.mdx
+++ b/docs/src/pages/docs/getting-started/app-router/without-i18n-routing.mdx
@@ -128,7 +128,7 @@ The `locale` that was provided in `i18n/request.ts` is available via `getLocale`
```tsx filename="app/layout.tsx"
import {NextIntlClientProvider} from 'next-intl';
-import {getLocale, getMessages} from 'next-intl/server';
+import {getLocale} from 'next-intl/server';
export default async function RootLayout({
children
@@ -137,24 +137,16 @@ export default async function RootLayout({
}) {
const locale = await getLocale();
- // Providing all messages to the client
- // side is the easiest way to get started
- const messages = await getMessages();
-
return (
-
- {children}
-
+ {children}
);
}
```
-Note that `NextIntlClientProvider` automatically inherits configuration from `i18n/request.ts` here, but `messages` need to be passed explicitly.
-
### `app/page.tsx` [#page]
Use translations in your page components or anywhere else!
diff --git a/docs/src/pages/docs/getting-started/pages-router.mdx b/docs/src/pages/docs/getting-started/pages-router.mdx
index 4c7a5a056..9261760b2 100644
--- a/docs/src/pages/docs/getting-started/pages-router.mdx
+++ b/docs/src/pages/docs/getting-started/pages-router.mdx
@@ -109,9 +109,3 @@ export async function getStaticProps() {
-
-## Support for legacy Next.js versions
-
-Next.js version 10, 11 and 12 are still supported. Note however that instead of installing `next-intl`, you'll have to import functionality like `useTranslations` from [`use-intl`](/docs/environments/core-library#react-apps).
-
-See the [legacy example](https://github.com/amannn/next-intl/tree/main/examples/example-pages-router-legacy).
diff --git a/docs/src/pages/docs/routing.mdx b/docs/src/pages/docs/routing.mdx
index 4f295f42c..f22f60b55 100644
--- a/docs/src/pages/docs/routing.mdx
+++ b/docs/src/pages/docs/routing.mdx
@@ -63,13 +63,6 @@ export const routing = defineRouting({
});
```
-
-How can I redirect unprefixed pathnames?
-
-If you want to redirect unprefixed pathnames like `/about` to a prefixed alternative like `/en/about`, you can adjust your middleware matcher to [match unprefixed pathnames](/docs/routing/middleware#matcher-no-prefix) too.
-
-
-
#### Don't use a locale prefix for the default locale [#locale-prefix-as-needed]
If you want to use no prefix for the default locale (e.g. `/about`), you can configure your routing accordingly:
@@ -83,9 +76,10 @@ export const routing = defineRouting({
});
```
-**Important**: For this routing strategy to work as expected, you should additionally adapt your middleware matcher to detect [unprefixed pathnames](/docs/routing/middleware#matcher-no-prefix).
+**Note that:**
-Note that if a superfluous locale prefix like `/en/about` is requested, the middleware will automatically redirect to the unprefixed version `/about`. This can be helpful in case you're redirecting from another locale and you want to update a potential cookie value first (e.g. [``](/docs/routing/navigation#link) relies on this mechanism).
+1. If you use this routing strategy, make sure that your [middleware matcher](/docs/routing/middleware#matcher-config) detects unprefixed pathnames.
+2. If a superfluous locale prefix like `/en/about` is requested, the middleware will automatically redirect to the unprefixed version `/about`. This can be helpful in case you're redirecting from another locale and you want to update a potential cookie value first (e.g. [``](/docs/routing/navigation#link) relies on this mechanism).
#### Never use a locale prefix [#locale-prefix-never]
@@ -109,9 +103,9 @@ In this case, requests for all locales will be rewritten to have the locale only
**Note that:**
-1. If you use this strategy, you should adapt your matcher to detect [unprefixed pathnames](/docs/routing/middleware#matcher-no-prefix).
-2. If you don't use domain-based routing, the cookie is now the source of truth for determining the locale. Make sure that your hosting solution reliably returns the `set-cookie` header from the middleware (e.g. Vercel and Cloudflare are known to potentially [strip this header](https://developers.cloudflare.com/cache/concepts/cache-behavior/#interaction-of-set-cookie-response-header-with-cache) for cacheable requests).
-3. [Alternate links](#alternate-links) are disabled in this mode since URLs might not be unique per locale. Due to this, consider including these yourself, or set up a [sitemap](/docs/environments/actions-metadata-route-handlers#sitemap) that links localized pages via `alternates`.
+1. If you use this routing strategy, make sure that your [middleware matcher](/docs/routing/middleware#matcher-config) detects unprefixed pathnames.
+2. [Alternate links](#alternate-links) are disabled in this mode since URLs might not be unique per locale. Due to this, consider including these yourself, or set up a [sitemap](/docs/environments/actions-metadata-route-handlers#sitemap) that links localized pages via `alternates`.
+3. You can consider increasing the [`maxAge`](#locale-cookie) attribute of the locale cookie to a longer duration to remember the user's preference across sessions.
#### Custom prefixes [#locale-prefix-custom]
@@ -177,8 +171,8 @@ Since you typically want to define these routes only once internally, you can us
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
- locales: ['en', 'de'],
- defaultLocale: 'en',
+ locales: ['en-US', 'en-UK', 'de'],
+ defaultLocale: 'en-US',
// The `pathnames` object holds pairs of internal and
// external paths. Based on the locale, the external
@@ -189,29 +183,25 @@ export const routing = defineRouting({
'/': '/',
'/blog': '/blog',
- // If locales use different paths, you can
- // specify each external path per locale
+ // If some locales use different paths, you can
+ // specify the relevant external pathnames
'/about': {
- en: '/about',
de: '/ueber-uns'
},
// Dynamic params are supported via square brackets
- '/news/[articleSlug]-[articleId]': {
- en: '/news/[articleSlug]-[articleId]',
- de: '/neuigkeiten/[articleSlug]-[articleId]'
+ '/news/[articleSlug]': {
+ de: '/neuigkeiten/[articleSlug]'
},
// Static pathnames that overlap with dynamic segments
// will be prioritized over the dynamic segment
'/news/just-in': {
- en: '/news/just-in',
de: '/neuigkeiten/aktuell'
},
// Also (optional) catch-all segments are supported
'/categories/[...slug]': {
- en: '/categories/[...slug]',
de: '/kategorien/[...slug]'
}
}
@@ -297,11 +287,12 @@ In case you're using a system like a CMS to configure localized pathnames, you'l
```tsx filename="page.tsx"
import {notFound} from 'next';
+import {Locale} from 'next-intl';
import {fetchContent} from './cms';
type Props = {
params: Promise<{
- locale: string;
+ locale: Locale;
slug: Array;
}>;
};
@@ -327,37 +318,49 @@ If you want to serve your localized content based on different domains, you can
**Examples:**
-- `us.example.com/en`
-- `ca.example.com/en`
-- `ca.example.com/fr`
+- `us.example.com`: `en-US`
+- `ca.example.com`: `en-CA`
+- `ca.example.com/fr`: `fr-CA`
+- `fr.example.com`: `fr-FR`
+
+In many cases, `domains` are combined with a [`localePrefix`](#locale-prefix) setting to achieve results as shown above. Also [custom prefixes](#locale-prefix-custom) can be used to customize the user-facing prefix per locale.
```tsx filename="routing.ts"
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
- locales: ['en', 'fr'],
- defaultLocale: 'en',
+ locales: ['en-US', 'en-CA', 'fr-CA', 'fr-FR'],
+ defaultLocale: 'en-US',
domains: [
{
domain: 'us.example.com',
- defaultLocale: 'en',
- // Optionally restrict the locales available on this domain
- locales: ['en']
+ defaultLocale: 'en-US',
+ locales: ['en-US']
},
{
domain: 'ca.example.com',
- defaultLocale: 'en'
- // If there are no `locales` specified on a domain,
- // all available locales will be supported here
+ defaultLocale: 'en-CA',
+ locales: ['en-CA', 'fr-CA']
+ },
+ {
+ domain: 'fr.example.com',
+ defaultLocale: 'fr-FR',
+ locales: ['fr-FR']
}
- ]
+ ],
+ localePrefix: {
+ mode: 'as-needed',
+ prefixes: {
+ // Cleaner prefix for `ca.example.com/fr`
+ 'fr-CA': '/fr'
+ }
+ }
});
```
-**Note that:**
+Locales are required to be unique across domains, therefore regional variants are typically used to avoid conflicts. Note however that you don't necessarily need to [provide messages for each locale](/docs/usage/configuration#messages-per-locale) if the overall language is sufficient for your use case.
-1. You can optionally remove the locale prefix in pathnames by changing the [`localePrefix`](#locale-prefix) setting. E.g. [`localePrefix: 'never'`](#locale-prefix-never) can be helpful in case you have unique domains per locale.
-2. If no domain matches, the middleware will fall back to the [`defaultLocale`](/docs/routing/middleware#default-locale) (e.g. on `localhost`).
+If no domain matches, the middleware will fall back to the general [`defaultLocale`](/docs/routing/middleware#default-locale) (e.g. on `localhost`).
How can I locally test if my setup is working?
@@ -399,9 +402,7 @@ PORT=3001 npm run dev
Can I use a different `localePrefix` setting per domain?
-Since such a configuration would require reading the domain at runtime, this would prevent the ability to render pages statically. Due to this, `next-intl` doesn't support this configuration out of the box.
-
-However, you can still achieve this by building the app for each domain separately, while injecting diverging routing configuration via an environment variable.
+While this is currently not supported out of the box, you can still achieve this by building the app for each domain separately while injecting diverging routing configuration via an environment variable.
**Example:**
@@ -412,48 +413,14 @@ const isUsDomain =
process.env.VERCEL_PROJECT_PRODUCTION_URL === 'us.example.com';
export const routing = defineRouting({
- locales: isUsDomain ? ['en'] : ['en', 'fr'],
- defaultLocale: 'en',
+ locales: isUsDomain ? ['en-US'] : ['en-CA', 'fr-CA'],
+ defaultLocale: isUsDomain ? 'en-US' : 'en-CA',
localePrefix: isUsDomain ? 'never' : 'always'
});
```
-
-Special case: Using `domains` with `localePrefix: 'as-needed'`
-
-Since domains can have different default locales, this combination requires some tradeoffs that apply to the [navigation APIs](/docs/routing/navigation) in order for `next-intl` to avoid reading the current host on the server side (which would prevent the usage of static rendering).
-
-1. [``](/docs/routing/navigation#link): This component will always render a locale prefix on the server side, even for the default locale of a given domain. However, during hydration on the client side, the prefix is potentially removed, if the default locale of the current domain is used. Note that the temporarily prefixed pathname will always be valid, however the middleware will potentially clean up a superfluous prefix via a redirect if the user clicks on a link before hydration.
-2. [`redirect`](/docs/routing/navigation#redirect): When calling this function, a locale prefix is always added, regardless of the provided locale. However, similar to the handling with ``, the middleware will potentially clean up a superfluous prefix.
-3. [`getPathname`](/docs/routing/navigation#getpathname): This function requires that a `domain` is passed as part of the arguments in order to avoid ambiguity. This can either be provided statically (e.g. when used in a sitemap), or read from a header like [`x-forwarded-host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host).
-
-```tsx
-import {getPathname} from '@/i18n/routing';
-import {headers} from 'next/headers';
-
-// Case 1: Statically known domain
-const domain = 'ca.example.com';
-
-// Case 2: Read at runtime (dynamic rendering)
-const domain = headers().get('x-forwarded-host');
-
-// Assuming the current domain is `ca.example.com`,
-// the returned pathname will be `/about`
-const pathname = getPathname({
- href: '/about',
- locale: 'en',
- domain
-});
-```
-
-A `domain` can optionally also be passed to `redirect` in the same manner to ensure that a prefix is only added when necessary. Alternatively, you can also consider redirecting in the middleware or via [`useRouter`](/docs/routing/navigation#usrouter) on the client side.
-
-If you need to avoid these tradeoffs, you can consider building the same app for each domain separately, while injecting diverging routing configuration via an [environment variable](#domains-localeprefix-individual).
-
-
-
### Turning off locale detection [#locale-detection]
The middleware will [detect a matching locale](/docs/routing/middleware#locale-detection) based on your routing configuration & the incoming request and will either pass the request through for a matching locale or redirect to one that matches.
@@ -473,11 +440,10 @@ In this case, only the locale prefix and a potentially [matching domain](#domain
### Locale cookie [#locale-cookie]
-By default, the middleware will set a cookie called `NEXT_LOCALE` that contains the most recently detected locale. This is used to [remember the user's locale](/docs/routing/middleware#locale-detection) preference for future requests.
+If a user changes the locale to a value that doesn't match the `accept-language` header, `next-intl` will set a session cookie called `NEXT_LOCALE` that contains the most recently detected locale. This is used to [remember the user's locale](/docs/routing/middleware#locale-detection) preference for subsequent requests.
By default, the cookie will be configured with the following attributes:
-1. [**`maxAge`**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#max-agenumber): This value is set to 1 year so that the preference of the user is kept as long as possible.
2. [**`sameSite`**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value): This value is set to `lax` so that the cookie can be set when coming from an external site.
3. [**`path`**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value): This value is not set by default, but will use the value of your [`basePath`](#base-path) if configured.
@@ -493,8 +459,8 @@ export const routing = defineRouting({
localeCookie: {
// Custom cookie name
name: 'USER_LOCALE',
- // Expire in one day
- maxAge: 60 * 60 * 24
+ // Expire in one year
+ maxAge: 60 * 60 * 24 * 365
}
});
```
@@ -511,7 +477,20 @@ export const routing = defineRouting({
});
```
-Note that the cookie is only set when the user switches the locale and is not updated on every request.
+
+Which `maxAge` value should I consider for GDPR compliance?
+
+The [Article 29 Working Party opinion 04/2012](https://ec.europa.eu/justice/article-29/documentation/opinion-recommendation/files/2012/wp194_en.pdf) provides a guideline for the expiration of cookies that are used to remember the user's language in section 3.6 "UI customization cookies". In this policy, a language preference cookie set as a result of an explicit user action, such as using a language switcher, is allowed to remain active for "a few additional hours" after a browser session has ended.
+
+To be compliant out of the box, `next-intl` does not set the `max-age` value of the cookie, making it a session cookie that expires when a browser is closed.
+
+Note that this can be beneficial in comparison to an expiration of a few hours since the cookie is guaranteed to remain active while the browser session lasts. On mobile devices, where browsers are infrequently closed but rather suspended to the background, the cookie can remain active even for an extended period.
+
+The Working Party also states that if additional information about the use of cookies is provided in a prominent location (e.g. a "uses cookies" notice next to the language switcher), the cookie can be configured to remember the user's preference for "a longer duration". If you're providing such a notice, you can consider increasing `maxAge` accordingly.
+
+Please note that legal requirements may vary by region, so it's advisable to verify them independently. While we strive to keep this information as up-to-date as possible, we cannot guarantee its accuracy.
+
+
### Alternate links [#alternate-links]
@@ -549,12 +528,7 @@ link: ; rel="alternate"; hreflang="en",
The [`x-default`](https://developers.google.com/search/docs/specialty/international/localized-versions#xdefault) entry is included to point to a variant that can be used if no other language matches the user's browser setting. This special entry is reserved for language selection & detection, in our case issuing a 307 redirect to the best matching locale.
-Note that middleware configuration is automatically incorporated with the following special cases:
-
-1. **`localePrefix: 'always'` (default)**: The `x-default` entry is only included for `/`, not for nested pathnames like `/about`. The reason is that the default [matcher](#matcher-config) doesn't handle unprefixed pathnames apart from `/`, therefore these URLs could be 404s. Note that this only applies to the optional `x-default` entry, locale-specific URLs are always included.
-2. **`localePrefix: 'never'`**: Alternate links are entirely turned off since there might not be unique URLs per locale.
-
-Other configuration options like `domains`, `pathnames` and `basePath` are automatically considered.
+Your middleware configuration, including options like `domains`, `pathnames` and `basePath`, is automatically incorporated.
diff --git a/docs/src/pages/docs/routing/middleware.mdx b/docs/src/pages/docs/routing/middleware.mdx
index caffb9c98..8bfec5d32 100644
--- a/docs/src/pages/docs/routing/middleware.mdx
+++ b/docs/src/pages/docs/routing/middleware.mdx
@@ -23,14 +23,16 @@ import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
- // Match only internationalized pathnames
- matcher: ['/', '/(de|en)/:path*']
+ // Match all pathnames except for
+ // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
+ // - … the ones containing a dot (e.g. `favicon.ico`)
+ matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
```
## Locale detection [#locale-detection]
-The locale is negotiated based on your [`localePrefix`](/docs/routing#locale-prefix) and [`domains`](/docs/routing#domains) setting. Once a locale is detected, it will be remembered for future requests by being stored in the `NEXT_LOCALE` cookie.
+The locale is negotiated based on your routing configuration, taking into account your settings for [`localePrefix`](/docs/routing#locale-prefix), [`domains`](/docs/routing#domains), [`localeDetection`](/docs/routing#locale-detection), and [`localeCookie`](/docs/routing#locale-cookie).
### Prefix-based routing (default) [#location-detection-prefix]
@@ -48,10 +50,11 @@ To change the locale, users can visit a prefixed route. This will take precedenc
**Example workflow:**
1. A user requests `/` and based on the `accept-language` header, the `en` locale is matched.
-2. The `en` locale is saved in a cookie and the user is redirected to `/en`.
+2. The user is redirected to `/en`.
3. The app renders `Switch to German` to allow the user to change the locale to `de`.
4. When the user clicks on the link, a request to `/de` is initiated.
-5. The middleware will update the cookie value to `de`.
+5. The middleware will add a cookie to remember the preference for the `de` locale.
+6. The user later requests `/` again and the middleware will redirect to `/de` based on the cookie.
Which algorithm is used to match the accept-language header against the available locales?
@@ -90,14 +93,13 @@ Since the middleware is aware of all your domains, if a domain receives a reques
4. The middleware recognizes that the user wants to switch to another domain and responds with a redirect to `ca.example.com/fr`.
-How is the best matching domain for a given locale detected?
+How is the best-matching domain for a given locale detected?
-The bestmatching domain is detected based on these priorities:
+The best-matching domain is detected based on these priorities:
1. Stay on the current domain if the locale is supported here
2. Use an alternative domain where the locale is configured as the `defaultLocale`
-3. Use an alternative domain where the available `locales` are restricted and the locale is supported
-4. Use an alternative domain that supports all locales
+3. Use an alternative domain that supports the locale
@@ -105,58 +107,6 @@ The bestmatching domain is detected based on these priorities:
The middleware is intended to only run on pages, not on arbitrary files that you serve independently of the user locale (e.g. `/favicon.ico`).
-Because of this, the following config is generally recommended:
-
-```tsx filename="middleware.ts"
-export const config = {
- // Match only internationalized pathnames
- matcher: ['/', '/(de|en)/:path*']
-};
-```
-
-This enables:
-
-1. A redirect at `/` to a suitable locale
-2. Internationalization of all pathnames starting with a locale (e.g. `/en/about`)
-
-
-Can I avoid hardcoding the locales in the `matcher` config?
-
-A [Next.js `matcher`](https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher) needs to be statically analyzable, therefore you can't use variables to generate this value. However, you can alternatively implement a programmatic condition in the middleware:
-
-```tsx filename="middleware.ts"
-import {NextRequest} from 'next/server';
-import createMiddleware from 'next-intl/middleware';
-import {routing} from './i18n/routing';
-
-const handleI18nRouting = createMiddleware(routing);
-
-export default function middleware(request: NextRequest) {
- const {pathname} = request.nextUrl;
-
- // Matches '/', as well as all paths that start with a locale like '/en'
- const shouldHandle =
- pathname === '/' ||
- new RegExp(`^/(${locales.join('|')})(/.*)?$`).test(
- request.nextUrl.pathname
- );
- if (!shouldHandle) return;
-
- return handleI18nRouting(request);
-}
-```
-
-
-
-### Pathnames without a locale prefix [#matcher-no-prefix]
-
-There are two use cases where you might want to match pathnames without a locale prefix:
-
-1. You're using a config for [`localePrefix`](/docs/routing#locale-prefix) other than [`always`](/docs/routing#locale-prefix-always)
-2. You want to enable redirects that add a locale for unprefixed pathnames (e.g. `/about` → `/en/about`)
-
-For these cases, the middleware should run on requests for pathnames without a locale prefix as well.
-
A popular strategy is to match all routes that don't start with certain segments (e.g. `/_next`) and also none that include a dot (`.`) since these typically indicate static files. However, if you have some routes where a dot is expected (e.g. `/users/jane.doe`), you should explicitly provide a matcher for these.
```tsx filename="middleware.ts"
@@ -168,6 +118,7 @@ export const config = {
// - … if they start with `/api`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
'/((?!api|_next|_vercel|.*\\..*).*)',
+
// However, match all pathnames within `/users`, optionally with a locale prefix
'/([\\w-]+)?/users/(.+)'
]
diff --git a/docs/src/pages/docs/routing/navigation.mdx b/docs/src/pages/docs/routing/navigation.mdx
index e23f7f150..8b8973fb0 100644
--- a/docs/src/pages/docs/routing/navigation.mdx
+++ b/docs/src/pages/docs/routing/navigation.mdx
@@ -260,8 +260,8 @@ Note that if you're using the [`pathnames`](/docs/routing#pathnames) setting, th
// When the user is on `/de/ueber-uns`, this will be `/about`
const pathname = usePathname();
-// When the user is on `/de/neuigkeiten/produktneuheit-94812`,
-// this will be `/news/[articleSlug]-[articleId]`
+// When the user is on `/de/neuigkeiten/produktneuheit`,
+// this will be `/news/[articleSlug]`
const pathname = usePathname();
```
@@ -372,14 +372,3 @@ const pathname = getPathname({
}
});
```
-
-## Legacy APIs
-
-`next-intl@3.0.0` brought the first release of the navigation APIs with these functions:
-
-- `createSharedPathnamesNavigation`
-- `createLocalizedPathnamesNavigation`
-
-As part of `next-intl@3.22.0`, these functions have been replaced by a single `createNavigation` function, which unifies the API for both use cases and also fixes a few quirks in the previous APIs. Going forward, `createNavigation` is recommended and the previous functions are marked as deprecated.
-
-While `createNavigation` is mostly API-compatible, there are some minor differences that should be noted. Please refer to the [3.22 announcement post](/blog/next-intl-3-22#create-navigation) for full details.
diff --git a/docs/src/pages/docs/usage/configuration.mdx b/docs/src/pages/docs/usage/configuration.mdx
index 523969519..8dfb5a2e5 100644
--- a/docs/src/pages/docs/usage/configuration.mdx
+++ b/docs/src/pages/docs/usage/configuration.mdx
@@ -15,51 +15,21 @@ Depending on if you handle [internationalization in Server- or Client Components
`i18n/request.ts` can be used to provide configuration for **server-only** code, i.e. Server Components, Server Actions & friends. The configuration is provided via the `getRequestConfig` function and needs to be set up based on whether you're using [i18n routing](/docs/getting-started/app-router) or not.
-
-
-
-
```tsx filename="i18n/request.ts"
import {getRequestConfig} from 'next-intl/server';
import {routing} from '@/i18n/routing';
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
- };
-});
-```
-
-
-
-
-```tsx filename="i18n/request.ts"
-import {getRequestConfig} from 'next-intl/server';
-
-export default getRequestConfig(async () => {
- // Provide a static locale, fetch a user setting,
- // read from `cookies()`, `headers()`, etc.
- const locale = 'en';
-
- return {
- locale,
- messages: (await import(`../../messages/${locale}.json`)).default
+ messages
+ // ...
};
});
```
-
-
-
The configuration object is created once for each request by internally using React's [`cache`](https://react.dev/reference/react/cache). The first component to use internationalization will call the function defined with `getRequestConfig`.
Since this function is executed during the Server Components render pass, you can call functions like [`cookies()`](https://nextjs.org/docs/app/api-reference/functions/cookies) and [`headers()`](https://nextjs.org/docs/app/api-reference/functions/headers) to return configuration that is request-specific.
@@ -80,17 +50,6 @@ 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**.
@@ -100,16 +59,12 @@ import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
export default async function RootLayout(/* ... */) {
- // Providing all messages to the client
- // side is the easiest way to get started
- const messages = await getMessages();
+ // ...
return (
-
- {children}
-
+ {children}
);
@@ -119,51 +74,38 @@ export default async function RootLayout(/* ... */) {
These props are inherited if you're rendering `NextIntlClientProvider` from a Server Component:
1. `locale`
-2. `now`
-3. `timeZone`
+2. `messages`
+3. `now`
+4. `timeZone`
+5. `formats`
In contrast, these props can be provided as necessary:
-1. `messages` (see [Internationalization in Client Components](/docs/environments/server-client-components#using-internationalization-in-client-components))
-2. `formats`
-3. `defaultTranslationValues`
-4. `onError` and `getMessageFallback`
+1. `onError`
+2. `getMessageFallback`
+
+Additionally, nested instances of `NextIntlClientProvider` will inherit configuration from their respective ancestors. Note however that individual props are treated as atomic, therefore e.g. `messages` need to be merged manually—if necessary.
How can I provide non-serializable props like `onError` to `NextIntlClientProvider`?
-React limits the types of props that can be passed to Client Components to the ones that are [serializable](https://react.dev/reference/rsc/use-client#serializable-types). Since `onError`, `getMessageFallback` and `defaultTranslationValues` can receive functions, these configuration options can't be automatically inherited by the client side.
+React limits the types of props that can be passed to Client Components to the ones that are [serializable](https://react.dev/reference/rsc/use-client#serializable-types). Since `onError` and `getMessageFallback` can receive functions, these configuration options can't be automatically inherited by the client side.
-In order to define these values, you can wrap `NextIntlClientProvider` with another component that is marked with `'use client'` and defines the relevant props:
+In order to define these values on the client side, you can add a provider that defines these props:
-```tsx filename="IntlProvider.tsx"
+```tsx filename="IntlErrorHandlingProvider.tsx"
'use client';
import {NextIntlClientProvider} from 'next-intl';
-export default function IntlProvider({
- locale,
- now,
- timeZone,
- messages,
- formats
-}) {
+export default function IntlErrorHandlingProvider({children}) {
return (
{text}
- }}
onError={(error) => console.error(error)}
getMessageFallback={({namespace, key}) => `${namespace}.${key}`}
- // Make sure to forward these props to avoid markup mismatches
- locale={locale}
- now={now}
- timeZone={timeZone}
- // Provide as necessary
- messages={messages}
- formats={formats}
- />
+ >
+ {children}
+
);
}
```
@@ -171,26 +113,19 @@ export default function IntlProvider({
Once you have defined your client-side provider component, you can use it in a Server Component:
```tsx filename="layout.tsx"
-import IntlProvider from './IntlProvider';
-import {getLocale, getNow, getTimeZone, getMessages} from 'next-intl/server';
+import {NextIntlClientProvider} from 'next-intl';
+import {getLocale} from 'next-intl/server';
+import IntlErrorHandlingProvider from './IntlErrorHandlingProvider';
export default async function RootLayout({children}) {
const locale = await getLocale();
- const now = await getNow();
- const timeZone = await getTimeZone();
- const messages = await getMessages();
return (
-
- {children}
-
+
+ {children}
+
);
@@ -199,10 +134,149 @@ export default async function RootLayout({children}) {
By doing this, your provider component will already be part of the client-side bundle and can therefore define and pass functions as props.
-**Important:** Be sure to pass explicit `locale`, `timeZone` and `now` props to `NextIntlClientProvider` in this case, since these aren't automatically inherited from a Server Component when you import `NextIntlClientProvider` from a Client Component.
+Note that the inner `NextIntlClientProvider` inherits the configuration from the outer one, only the `onError` and `getMessageFallback` functions are added.
+
+
+
+## Locale
+
+The `locale` represents an identifier that contains the language and formatting preferences of users, optionally including regional information (e.g. `en-US`). Locales are specified as [IETF BCP 47 language tags](https://en.wikipedia.org/wiki/IETF_language_tag).
+
+
+
+
+Depending on if you're using [i18n routing](/docs/getting-started/app-router), you can read the locale from the `requestLocale` parameter or provide a value on your own:
+
+**With i18n routing:**
+
+```tsx filename="i18n/request.ts"
+export default getRequestConfig(async ({requestLocale}) => {
+ // Typically corresponds to the `[locale]` segment
+ const requested = await requestLocale;
+ const locale = hasLocale(routing.locales, requested)
+ ? requested
+ : routing.defaultLocale;
+
+ return {
+ locale
+ // ...
+ };
+});
+```
+
+**Without i18n routing:**
+
+```tsx filename="i18n/request.ts"
+export default getRequestConfig(async () => {
+ // Provide a static locale, fetch a user setting,
+ // read from `cookies()`, `headers()`, etc.
+ const locale = 'en';
+
+ return {
+ locale
+ // ...
+ };
+});
+```
+
+
+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.
+
+
+
+```tsx
+...
+```
+
+
+
+
+
+How can I change the locale?
+
+Depending on if you're using [i18n routing](/docs/getting-started/app-router), the locale can be changed as follows:
+
+1. **With i18n routing**: The locale is managed by the router and can be changed by using navigation APIs from `next-intl` like [`Link`](/docs/routing/navigation#link) or [`useRouter`](/docs/routing/navigation#userouter).
+2. **Without i18n routing**: You can change the locale by updating the value where the locale is read from (e.g. a cookie, a user setting, etc.). If you're looking for inspiration, you can have a look at the [App Router without i18n routing example](/examples#app-router-without-i18n-routing) that manages the locale via a cookie.
+
+
+
+### `useLocale` & `getLocale` [#use-locale]
+
+The current locale of your app is automatically incorporated into hooks like `useTranslations` & `useFormatter` and will affect the rendered output.
+
+In case you need to use this value in other places of your app, e.g. to implement a locale switcher or to pass it to API calls, you can read it via `useLocale` or `getLocale`:
+
+```tsx
+// Regular components
+import {useLocale} from 'next-intl';
+const locale = useLocale();
+
+// Async Server Components
+import {getLocale} from 'next-intl/server';
+const locale = await getLocale();
+```
+
+
+Which value is returned from `useLocale`?
+
+Depending on how a component renders, the returned locale corresponds to:
+
+1. **Server Components**: The locale represents the value returned in [`i18n/request.ts`](#i18n-request).
+2. **Client Components**: The locale is received from [`NextIntlClientProvider`](#nextintlclientprovider).
+
+Note that `NextIntlClientProvider` automatically inherits the locale if it is rendered by a Server Component, therefore you rarely need to pass a locale to `NextIntlClientProvider` yourself.
+
+
+
+
+I'm using the Pages Router, how can I access the locale?
+
+If you use [internationalized routing with the Pages Router](https://nextjs.org/docs/pages/building-your-application/routing/internationalization), you can receive the locale from the router in order to pass it to `NextIntlClientProvider`:
+
+```tsx filename="_app.tsx"
+import {useRouter} from 'next/router';
+
+// ...
+
+const router = useRouter();
+
+return (
+
+ ...
+ ;
+);
+```
+
+
+
+### `Locale` type [#locale-type]
+
+When passing a `locale` to another function, you can use the `Locale` type for the receiving parameter:
+
+```tsx
+import {Locale} from 'next-intl';
+
+async function getPosts(locale: Locale) {
+ // ...
+}
+```
+
+
+ By default, `Locale` is typed as `string`. However, you can optionally provide
+ a strict union based on your supported locales for this type by [augmenting
+ the `Locale` type](/docs/workflows/typescript#locale).
+
+
## Messages
The most crucial aspect of internationalization is providing labels based on the user's language. The recommended workflow is to store your messages in your repository along with the code.
@@ -235,40 +309,6 @@ export default getRequestConfig(async () => {
After messages are configured, they can be used via [`useTranslations`](/docs/usage/messages#rendering-messages-with-usetranslations).
-In case you require access to messages in a component, you can read them via `useMessages()` or `getMessages()` from your configuration:
-
-```tsx
-// Regular components
-import {useMessages} from 'next-intl';
-const messages = useMessages();
-
-// Async Server Components
-import {getMessages} from 'next-intl/server';
-const messages = await getMessages();
-```
-
-
-
-
-```tsx
-import {NextIntlClientProvider} from 'next-intl';
-import {getMessages} from 'next-intl/server';
-
-async function Component({children}) {
- // Read messages configured via `i18n/request.ts`
- const messages = await getMessages();
-
- return (
-
- {children}
-
- );
-}
-```
-
-
-
-
How can I load messages from remote sources?
@@ -318,6 +358,67 @@ Note that [the VSCode integration for `next-intl`](/docs/workflows/vscode-integr
+
+Do I need separate messages for each locale that my app supports?
+
+Since you have full control over how messages are loaded, you can choose to load messages for example merely based on the overall language, ignoring any regional variants:
+
+```tsx
+import {getRequestConfig} from 'next-intl/server';
+
+export default getRequestConfig(async () => {
+ // E.g. "en-US", "en-CA", …
+ const locale = 'en-US';
+
+ // E.g. "en"
+ const language = new Intl.Locale(locale).language;
+
+ // Load messages based on the language
+ const messages = (await import(`../../messages/${language}.json`)).default;
+
+ // ...
+});
+```
+
+
+
+### `useMessages` & `getMessages` [#use-messages]
+
+In case you require access to messages in a component, you can read them via `useMessages()` or `getMessages()` from your configuration:
+
+```tsx
+// Regular components
+import {useMessages} from 'next-intl';
+const messages = useMessages();
+
+// Async Server Components
+import {getMessages} from 'next-intl/server';
+const messages = await getMessages();
+```
+
+
+
+
+```tsx
+import {NextIntlClientProvider} from 'next-intl';
+import {getMessages} from 'next-intl/server';
+import pick from 'lodash/pick';
+
+async function Component({children}) {
+ // Read messages configured via `i18n/request.ts`
+ const messages = await getMessages();
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+
+
+
## Time zone
Specifying a time zone affects the rendering of dates and times. By default, the time zone of the server runtime will be used, but can be customized as necessary.
@@ -357,6 +458,10 @@ const timeZone = 'Europe/Vienna';
The available time zone names can be looked up in [the tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
+The time zone in Client Components is automatically inherited from the server side if you wrap the relevant components in a `NextIntlClientProvider` that is rendered by a Server Component. For all other cases, you can specify the value explicitly on a wrapping `NextIntlClientProvider`.
+
+### `useTimeZone` & `getTimeZone` [#use-time-zone]
+
The configured time zone can be read via `useTimeZone` or `getTimeZone` in components:
```tsx
@@ -369,16 +474,9 @@ import {getTimeZone} from 'next-intl/server';
const timeZone = await getTimeZone();
```
-The time zone in Client Components is automatically inherited from the server
-side if you wrap the relevant components in a `NextIntlClientProvider` that is
-rendered by a Server Component. For all other cases, you can specify the value
-explicitly on a wrapping `NextIntlClientProvider`.
-
## Now value [#now]
-When formatting [relative dates and times](/docs/usage/dates-times#relative-times), `next-intl` will format times in relation to a reference point in time that is referred to as "now". By default, this is the time a component renders.
-
-If you prefer to override the default, you can provide an explicit value for `now`:
+When formatting [relative dates and times](/docs/usage/dates-times#relative-times), `next-intl` will format times in relation to a reference point in time that is referred to as "now". While it can be beneficial in terms of caching to [provide this value](/docs/usage/dates-times#relative-times-usenow) where necessary, you can provide a global value for `now`, e.g. to ensure consistency when running tests.
@@ -388,11 +486,7 @@ import {getRequestConfig} from 'next-intl/server';
export default getRequestConfig(async () => {
return {
- // This is the default, a single date instance will be
- // used by all Server Components to ensure consistency.
- // Tip: This value can be mocked to a constant value
- // for consistent results in end-to-end-tests.
- now: new Date()
+ now: new Date('2024-11-14T10:36:01.516Z')
// ...
};
@@ -403,7 +497,7 @@ export default getRequestConfig(async () => {
```tsx
-const now = new Date('2020-11-20T10:36:01.516Z');
+const now = new Date('2024-11-14T10:36:01.516Z');
...;
```
@@ -411,6 +505,10 @@ const now = new Date('2020-11-20T10:36:01.516Z');
+If a `now` value is provided in `i18n/request.ts`, this will automatically be inherited by Client Components if you wrap them in a `NextIntlClientProvider` that is rendered by a Server Component.
+
+### `useNow` & `getNow` [#use-now]
+
The configured `now` value can be read in components via `useNow` or `getNow`:
```tsx
@@ -423,10 +521,7 @@ import {getNow} from 'next-intl/server';
const now = await getNow();
```
-Similarly to the `timeZone`, the `now` value in Client Components is
-automatically inherited from the server side if you wrap the relevant
-components in a `NextIntlClientProvider` that is rendered by a Server
-Component.
+Note that the returned value defaults to the current date and time, therefore making this hook useful when [providing `now`](/docs/usage/dates-times#relative-times-usenow) for `format.relativeTime` even when you haven't configured a global `now` value.
## Formats
@@ -466,8 +561,6 @@ export default getRequestConfig(async () => {
});
```
-Note that `formats` are not automatically inherited by Client Components. If you want to make this available in Client Components, you should provide the same configuration to [`NextIntlClientProvider`](#nextintlclientprovider).
-
@@ -516,9 +609,9 @@ function Component() {
```
- You can optionally [specify a global type for
- `formats`](/docs/workflows/typescript#formats) to get autocompletion and type
- safety.
+ By default, format names are loosely typed as `string`. However, you can
+ optionally use strict types by [augmenting the `Formats`
+ type](/docs/workflows/typescript#formats).
Global formats for numbers, dates and times can be referenced in messages too:
@@ -541,53 +634,7 @@ function Component() {
}
```
-## Default translation values (deprecated) [#default-translation-values]
-
-
- This feature is deprecated and will be removed in the next major version of `next-intl` ([alternative](/docs/usage/messages#rich-text-reuse-tags)).
-
-
-
-To achieve consistent usage of translation values and reduce redundancy, you can define a set of global default values. This configuration can also be used to apply consistent styling of commonly used rich text elements.
-
-
-
-
-```tsx filename="i18n/request.tsx"
-import {getRequestConfig} from 'next-intl/server';
-
-export default getRequestConfig(async () => {
- return {
- defaultTranslationValues: {
- important: (chunks) => {chunks},
- value: 123
- }
-
- // ...
- };
-});
-```
-
-Note that `defaultTranslationValues` are not automatically inherited by Client Components. If you want to make this available in Client Components, you should provide the same configuration to [`NextIntlClientProvider`](#nextintlclientprovider).
-
-
-
-
-```tsx
- {chunks},
- value: 123
- }}
->
- ...
-
-```
-
-Note that `NextIntlClientProvider` is a Client Component, therefore if you render it from a Server Component, the props need to be serializable across the server/client boundary (see: [How can I provide non-serializable props to `NextIntlClientProvider`](#nextintlclientprovider-non-serializable-props)).
-
-
-
+Formats are automatically inherited from the server side if you wrap the relevant components in a `NextIntlClientProvider` that is rendered by a Server Component.
## Error handling (`onError` & `getMessageFallback`) [#error-handling]
@@ -629,7 +676,7 @@ export default getRequestConfig(async () => {
});
```
-Note that `onError` and `getMessageFallback` are not automatically inherited by Client Components. If you want to make this functionality available in Client Components, you should provide the same configuration to [`NextIntlClientProvider`](#nextintlclientprovider).
+Note that `onError` and `getMessageFallback` are not automatically inherited by Client Components. If you want to make this functionality available in Client Components too, you can however create a [client-side provider](#nextintlclientprovider-non-serializable-props) that defines these props.
@@ -667,61 +714,3 @@ function getMessageFallback({namespace, key, error}) {
-
-## Locale
-
-The current locale of your app is automatically incorporated into hooks like `useTranslations` & `useFormatter` and will affect the rendered output.
-
-In case you need to use this value in other places of your app, e.g. to implement a locale switcher or to pass it to API calls, you can read it via `useLocale` or `getLocale`:
-
-```tsx
-// Regular components
-import {useLocale} from 'next-intl';
-const locale = useLocale();
-
-// Async Server Components
-import {getLocale} from 'next-intl/server';
-const locale = await getLocale();
-```
-
-
-How can I change the locale?
-
-Depending on if you're using [i18n routing](/docs/getting-started/app-router), the locale can be changed as follows:
-
-1. **With i18n routing**: The locale is managed by the router and can be changed by using navigation APIs from `next-intl` like [`Link`](/docs/routing/navigation#link) or [`useRouter`](/docs/routing/navigation#userouter).
-2. **Without i18n routing**: You can change the locale by updating the value where the locale is read from (e.g. a cookie, a user setting, etc.). If you're looking for inspiration, you can have a look at the [App Router without i18n routing example](/examples#app-router-without-i18n-routing) that manages the locale via a cookie.
-
-
-
-
-Which value is returned from `useLocale`?
-
-The returned value is resolved based on these priorities:
-
-1. **Server Components**: If you're using [i18n routing](/docs/getting-started/app-router), the returned locale is the one that you've either provided via [`setRequestLocale`](/docs/getting-started/app-router/with-i18n-routing#static-rendering) or alternatively the one in the `[locale]` segment that was matched by the middleware. If you're not using i18n routing, the returned locale is the one that you've provided via `getRequestConfig`.
-2. **Client Components**: In this case, the locale is received from `NextIntlClientProvider` or alternatively `useParams().locale`. Note that `NextIntlClientProvider` automatically inherits the locale if the component is rendered by a Server Component. For all other cases, you can specify the value
- explicitly.
-
-
-
-
-I'm using the Pages Router, how can I access the locale?
-
-If you use [internationalized routing with the Pages Router](https://nextjs.org/docs/pages/building-your-application/routing/internationalization), you can receive the locale from the router in order to pass it to `NextIntlClientProvider`:
-
-```tsx filename="_app.tsx"
-import {useRouter} from 'next/router';
-
-// ...
-
-const router = useRouter();
-
-return (
-
- ...
- ;
-);
-```
-
-
diff --git a/docs/src/pages/docs/usage/dates-times.mdx b/docs/src/pages/docs/usage/dates-times.mdx
index a8627c853..62f8c5fe8 100644
--- a/docs/src/pages/docs/usage/dates-times.mdx
+++ b/docs/src/pages/docs/usage/dates-times.mdx
@@ -34,7 +34,11 @@ See [the MDN docs about `DateTimeFormat`](https://developer.mozilla.org/en-US/do
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument:
```js
+// Use a global format
format.dateTime(dateTime, 'short');
+
+// Optionally override some options
+format.dateTime(dateTime, 'short', {year: 'numeric'});
```
@@ -67,32 +71,84 @@ function Component() {
const format = useFormatter();
const dateTime = new Date('2020-11-20T08:30:00.000Z');
- // At 2020-11-20T10:36:00.000Z,
- // this will render "2 hours ago"
- format.relativeTime(dateTime);
+ // A reference point in time
+ const now = new Date('2020-11-20T10:36:00.000Z');
+
+ // This will render "2 hours ago"
+ format.relativeTime(dateTime, now);
}
```
Note that values are rounded, so e.g. if 126 minutes have passed, "2 hours ago" will be returned.
-### Supplying `now`
+### `useNow` [#relative-times-usenow]
-By default, `relativeTime` will use [the global value for `now`](/docs/usage/configuration#now). If you want to use a different value, you can explicitly pass this as the second parameter.
+Since providing `now` is a common pattern, `next-intl` provides a convenience hook that can be used to retrieve the current date and time:
-```js
-import {useFormatter} from 'next-intl';
+```tsx {4}
+import {useNow, useFormatter} from 'next-intl';
-function Component() {
+function FormattedDate({date}) {
+ const now = useNow();
const format = useFormatter();
- const dateTime = new Date('2020-11-20T08:30:00.000Z');
- const now = new Date('2020-11-20T10:36:00.000Z');
- // Renders "2 hours ago"
- format.relativeTime(dateTime, now);
+ format.relativeTime(date, now);
+}
+```
+
+In contrast to simply calling `new Date()` in your component, `useNow` has some benefits:
+
+1. The returned value is consistent across re-renders on the client side.
+2. The value can optionally be [updated continuously](#relative-times-update) based on an interval.
+3. The value can optionally be initialized from a [global value](/docs/usage/configuration#now), e.g. allowing you to use a static `now` value to ensure consistency when running tests. If a global value is not provided, `useNow` will use the current time.
+
+
+How can I avoid hydration errors with `useNow`?
+
+If you're using `useNow` in a component that renders both on the server as well as the client and you're not using a global `now` value, you can consider using [`suppressHydrationWarning`](https://react.dev/reference/react-dom/client/hydrateRoot#suppressing-unavoidable-hydration-mismatch-errors) to tell React that this particular text is expected to potentially be updated on the client side:
+
+```tsx {7}
+import {useNow, useFormatter} from 'next-intl';
+
+function FormattedDate({date}) {
+ const now = useNow();
+ const format = useFormatter();
+
+ return {format.relativeTime(date, now)};
+}
+```
+
+While this prop has a somewhat intimidating name, it's an escape hatch that was purposefully designed for cases like this.
+
+
+
+
+How can I use `now` in Server Components with `dynamicIO`?
+
+If you're using [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO), Next.js may prompt you to specify a cache expiration in case you're using `useNow` in a Server Component.
+
+You can do so by annotating your component with the `'use cache'` directive, while converting it to an async function:
+
+```tsx
+import {getNow, getFormatter} from 'next-intl/server';
+
+async function FormattedDate({date}) {
+ 'use cache';
+
+ const now = await getNow();
+ const format = await getFormatter();
+
+ return format.relativeTime(date, now);
}
```
-If you want the relative time value to update over time, you can do so with [the `useNow` hook](/docs/usage/configuration#now):
+Alternatively, if you don't want to use any caching, you can mark the component with [`await connection()`](https://nextjs.org/docs/app/api-reference/functions/connection) instead to render at request time.
+
+
+
+### `updateInterval` [#relative-times-update]
+
+In case you want a relative time value to update over time, you can do so with [the `useNow` hook](/docs/usage/configuration#use-now):
```js
import {useNow, useFormatter} from 'next-intl';
@@ -112,9 +168,9 @@ function Component() {
}
```
-### Customizing the unit
+### Customizing the unit [#relative-times-unit]
-By default, `relativeTime` will pick a unit based on the difference between the passed date and `now` (e.g. 3 seconds, 40 minutes, 4 days, etc.).
+By default, `relativeTime` will pick a unit based on the difference between the passed date and `now` like "3 seconds" or "5 days".
If you want to use a specific unit, you can provide options via the second argument:
@@ -152,10 +208,14 @@ function Component() {
}
```
-If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the trailing argument:
+If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the third argument:
```js
+// Use a global format
format.dateTimeRange(dateTimeA, dateTimeB, 'short');
+
+// Optionally override some options
+format.dateTimeRange(dateTimeA, dateTimeB, 'short', {year: 'numeric'});
```
## Dates and times within messages
diff --git a/docs/src/pages/docs/usage/messages.mdx b/docs/src/pages/docs/usage/messages.mdx
index 34dc62614..27f2e9907 100644
--- a/docs/src/pages/docs/usage/messages.mdx
+++ b/docs/src/pages/docs/usage/messages.mdx
@@ -8,7 +8,7 @@ The main part of handling internationalization (typically referred to as _i18n_)
## Terminology
-- **Locale**: We use this term to describe an identifier that contains the language and formatting preferences of users. Apart from the language, a locale can include optional regional information (e.g. `en-US`). Locales are specified as [IETF BCP 47 language tags](https://en.wikipedia.org/wiki/IETF_language_tag).
+- **Locale**: We use this term to describe an identifier that contains the language and formatting preferences of users. Apart from the language, a locale can include optional regional information (e.g. `en-US`).
- **Messages**: These are collections of namespace-label pairs that are grouped by locale (e.g. `en-US.json`).
## Structuring messages
diff --git a/docs/src/pages/docs/usage/numbers.mdx b/docs/src/pages/docs/usage/numbers.mdx
index fb8dbb287..26c2e4370 100644
--- a/docs/src/pages/docs/usage/numbers.mdx
+++ b/docs/src/pages/docs/usage/numbers.mdx
@@ -31,7 +31,11 @@ See [the MDN docs about `NumberFormat`](https://developer.mozilla.org/en-US/docs
If you have [global formats](/docs/usage/configuration#formats) configured, you can reference them by passing a name as the second argument:
```js
+// Use a global format
format.number(499.9, 'precise');
+
+// Optionally override some options
+format.number(499.9, 'price', {currency: 'USD'});
```
## Numbers within messages
diff --git a/docs/src/pages/docs/workflows.mdx b/docs/src/pages/docs/workflows.mdx
index 746ad13d5..89b597c1c 100644
--- a/docs/src/pages/docs/workflows.mdx
+++ b/docs/src/pages/docs/workflows.mdx
@@ -3,10 +3,10 @@ import Cards from '@/components/Cards';
# Workflows & integrations
-To get the most out of `next-intl`, you can choose from these integrations to improve your workflow when developing and collaborating with translators.
+To get the most out of `next-intl`, you can choose from these integrations to improve your workflow.
-
+
- The [TypeScript integration of `next-intl`](/docs/workflows/typescript) can
+ The [TypeScript augmentation of `next-intl`](/docs/workflows/typescript) can
help you to validate at compile time that your app is in sync with your
translation bundles.
diff --git a/docs/src/pages/docs/workflows/typescript.mdx b/docs/src/pages/docs/workflows/typescript.mdx
index 1597b4b8e..e7626f25e 100644
--- a/docs/src/pages/docs/workflows/typescript.mdx
+++ b/docs/src/pages/docs/workflows/typescript.mdx
@@ -1,12 +1,88 @@
+import Details from '@/components/Details';
+import {Tabs} from 'nextra/components';
import Callout from '@/components/Callout';
-# TypeScript integration
+# TypeScript augmentation
`next-intl` integrates seamlessly with TypeScript right out of the box, requiring no additional setup.
-However, you can optionally provide supplemental type definitions for your messages and formats to enable autocompletion and improve type safety.
+However, you can optionally provide supplemental definitions to augment the types that `next-intl` works with, enabling improved autocompletion and type safety across your app.
-## Messages
+```tsx filename="global.d.ts"
+import {routing} from '@/i18n/routing';
+import {formats} from '@/i18n/request';
+import messages from './messages/en.json';
+
+declare module 'next-intl' {
+ interface AppConfig {
+ Locale: (typeof routing.locales)[number];
+ Messages: typeof messages;
+ Formats: typeof formats;
+ }
+}
+```
+
+Type augmentation is available for:
+
+- [`Locale`](#locale)
+- [`Messages`](#messages)
+- [`Formats`](#formats)
+
+## `Locale`
+
+Augmenting the `Locale` type will affect all APIs from `next-intl` that either return or receive a locale:
+
+```tsx
+import {useLocale} from 'next-intl';
+
+// ✅ 'en' | 'de'
+const locale = useLocale();
+```
+
+```tsx
+import {Link} from '@/i18n/routing';
+
+// ✅ Passes the validation
+;
+```
+
+Additionally, `next-intl` provides a [`Locale`](/docs/usage/configuration#locale-type) type that can be used when passing the locale as an argument.
+
+To enable this validation, you can adapt `AppConfig` as follows:
+
+
+
+
+```tsx filename="global.d.ts"
+import {routing} from '@/i18n/routing';
+
+declare module 'next-intl' {
+ interface AppConfig {
+ // ...
+ Locale: (typeof routing.locales)[number];
+ }
+}
+```
+
+
+
+
+```tsx filename="global.d.ts"
+// Potentially imported from a shared config
+const locales = ['en', 'de'] as const;
+
+declare module 'next-intl' {
+ interface AppConfig {
+ // ...
+ Locale: (typeof locales)[number];
+ }
+}
+```
+
+
+
+
+## `Messages`
Messages can be strictly typed to ensure you're using valid keys.
@@ -31,44 +107,158 @@ function About() {
}
```
-To enable this validation, add a global type definition file in your project root (e.g. `global.d.ts`):
+To enable this validation, you can adapt `AppConfig` as follows:
```ts filename="global.d.ts"
-import en from './messages/en.json';
+import messages from './messages/en.json';
+
+declare module 'next-intl' {
+ interface AppConfig {
+ // ...
+ Messages: typeof messages;
+ }
+}
+```
+
+You can freely define the interface, but if you have your messages available locally, it can be helpful to automatically create the type based on the messages from your default locale.
+
+
+Does this affect the performance of type checking?
+
+While the size of your messages file can have an effect on the time it takes to run the TypeScript compiler on your project, the overhead of augmenting `Messages` should be reasonably fast.
-type Messages = typeof en;
+Here's a benchmark from a sample project with 340 messages:
+
+- No type augmentation for messages: ~2.20s
+- Type-safe keys: ~2.82s
+- Type-safe arguments: ~2.85s
+
+This was observed on a MacBook Pro 2019 (Intel).
+
+---
+
+If you experience performance issues on larger projects, you can consider:
+
+1. Using type augmentation of messages only on your continuous integration pipeline as a safety net
+2. Splitting your project into multiple packages in a monorepo, allowing you to work with separate messages per package
+
+
+
+
+Does this affect the performance of my editor?
+
+Generally, type augmentation for `Messages` should be [reasonably fast](#messages-performance-tsc).
+
+In case you notice your editor performance related to saving files to be impacted, it might be caused by running ESLint on save when using [type-aware](https://typescript-eslint.io/troubleshooting/typed-linting/performance/) rules from `@typescript-eslint`.
+
+To ensure your editor performance is optimal, you can consider running expensive, type-aware rules only on your continuous integration pipeline:
+
+```tsx filename="eslint.config.js"
+// ...
-declare global {
- // Use type safe message keys with `next-intl`
- interface IntlMessages extends Messages {}
+ // Run expensive, type-aware linting only on CI
+ '@typescript-eslint/no-misused-promises': process.env.CI
+ ? 'error'
+ : 'off'
+```
+
+
+
+### Type-safe arguments [#messages-arguments]
+
+Apart from strictly typing message keys, you can also ensure type safety for message arguments:
+
+```json filename="messages/en.json"
+{
+ "UserProfile": {
+ "title": "Hello {firstName}"
+ }
}
```
-You can freely define the interface, but if you have your messages available locally, it can be helpful to automatically create the interface based on the messages from your default locale by importing it.
+```tsx
+function UserProfile({user}) {
+ const t = useTranslations('UserProfile');
+
+ // ✖️ Missing argument
+ t('title');
+
+ // ✅ Argument is provided
+ t('title', {firstName: user.firstName});
+}
+```
+
+TypeScript currently has a [limitation](https://github.com/microsoft/TypeScript/issues/32063) where it infers values of imported JSON modules as loose types like `string` instead of the actual value. To bridge this gap for the time being, `next-intl` can generate an accompanying `.d.json.ts` file for the messages that you're assigning to your `AppConfig`.
+
+**Usage:**
+
+1. Add support for JSON type declarations in your `tsconfig.json`:
+
+```json filename="tsconfig.json"
+{
+ "compilerOptions": {
+ // ...
+ "allowArbitraryExtensions": true
+ }
+}
+```
+
+2. Configure the `createMessagesDeclaration` setting in your Next.js config:
+
+```tsx filename="next.config.mjs"
+import {createNextIntlPlugin} from 'next-intl/plugin';
+
+const withNextIntl = createNextIntlPlugin({
+ experimental: {
+ // Provide the path to the messages that you're using in `AppConfig`
+ createMessagesDeclaration: './messages/en.json'
+ }
+ // ...
+});
+
+// ...
+```
+
+With this setup in place, you'll see a new declaration file generated in your `messages` directory once you run `next dev` or `next build`:
-## Formats
+```diff
+ messages/en.json
++ messages/en.d.json.ts
+```
+
+This declaration file will provide the exact types for the JSON messages that you're importing and assigning to `AppConfig`, enabling type safety for message arguments.
-[Global formats](/docs/usage/configuration#formats) that are referenced in calls like `format.dateTime` can be strictly typed to ensure you're using valid format names across your app.
+To keep your code base tidy, you can ignore this file in Git:
+
+```text filename=".gitignore"
+messages/*.d.json.ts
+```
+
+Please consider upvoting [`TypeScript#32063`](https://github.com/microsoft/TypeScript/issues/32063) to potentially remove this workaround in the future.
+
+## `Formats`
+
+If you're using [global formats](/docs/usage/configuration#formats), you can strictly type the format names that are referenced in calls to `format.dateTime`, `format.number` and `format.list`.
```tsx
function Component() {
const format = useFormatter();
- // ✅ Valid format
- format.number(2, 'precise');
-
- // ✅ Valid format
- format.list(['HTML', 'CSS', 'JavaScript'], 'enumeration');
-
// ✖️ Unknown format string
format.dateTime(new Date(), 'unknown');
// ✅ Valid format
format.dateTime(new Date(), 'short');
+
+ // ✅ Valid format
+ format.number(2, 'precise');
+
+ // ✅ Valid format
+ format.list(['HTML', 'CSS', 'JavaScript'], 'enumeration');
}
```
-To enable this validation, export the formats that you're using in your request configuration:
+To enable this validation, export the formats that you're using e.g. from your request configuration:
```ts filename="i18n/request.ts"
import {Formats} from 'next-intl';
@@ -97,16 +287,16 @@ export const formats = {
// ...
```
-Now, a global type definition file in the root of your project can pick up the shape of your formats and use them for declaring the `IntlFormats` interface:
+Now, you can include the `formats` in your `AppConfig`:
```ts filename="global.d.ts"
-import {formats} from './src/i18n/request';
-
-type Formats = typeof formats;
+import {formats} from '@/i18n/request';
-declare global {
- // Use type safe formats with `next-intl`
- interface IntlFormats extends Formats {}
+declare module 'next-intl' {
+ interface AppConfig {
+ // ...
+ Formats: typeof formats;
+ }
}
```
@@ -114,8 +304,7 @@ declare global {
If you're encountering problems, double check that:
-1. Your interface uses the correct name.
-2. You're using TypeScript version 5 or later.
-3. You're using correct paths for all modules you're importing into your global declaration file.
-4. Your type declaration file is included in `tsconfig.json`.
-5. Your editor has loaded the most recent type declarations. When in doubt, you can restart.
+1. The interface uses the correct name `AppConfig`.
+2. You're using correct paths for all modules you're importing into your global declaration file.
+3. Your type declaration file is included in `tsconfig.json`.
+4. Your editor has loaded the latest types. When in doubt, restart your editor.
diff --git a/docs/src/pages/index.mdx b/docs/src/pages/index.mdx
index d219d0aaf..9aee3b5d6 100644
--- a/docs/src/pages/index.mdx
+++ b/docs/src/pages/index.mdx
@@ -20,6 +20,10 @@ import GetStartedBackground from '@/components/GetStartedBackground';
description="Support multiple languages, with your app code becoming simpler instead of more complex."
getStarted="Get started"
viewExample="View an example"
+ announcement={{
+ href: '/blog/next-intl-4-0',
+ label: 'next-intl 4.0 is out now!'
+ }}
/>
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 3ef5664b2..482775518 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,5 @@
import {notFound} from 'next/navigation';
+import {NextIntlClientProvider, hasLocale} from 'next-intl';
import {ReactNode} from 'react';
import {routing} from '@/i18n/routing';
@@ -8,10 +9,9 @@ type Props = {
};
export default async function LocaleLayout({children, params}: Props) {
- const {locale} = await params;
-
// Ensure that the incoming `locale` is valid
- if (!routing.locales.includes(locale as any)) {
+ const {locale} = await params;
+ if (!hasLocale(routing.locales, locale)) {
notFound();
}
@@ -20,7 +20,9 @@ export default async function LocaleLayout({children, params}: Props) {
next-intl
- {children}
+
+ {children}
+