diff --git a/docs/pages/blog/next-intl-3-0.mdx b/docs/pages/blog/next-intl-3-0.mdx index 5fc457817..333eac869 100644 --- a/docs/pages/blog/next-intl-3-0.mdx +++ b/docs/pages/blog/next-intl-3-0.mdx @@ -51,9 +51,9 @@ With 3.0, we're cleaning up these APIs by moving them to a shared namespace as w + const {Link, useRouter, usePathname, redirect} = createSharedPathnamesNavigation({locales}); ``` -Typically, you'll want to call this factory function in a central place in your app where you can easily import from (see [the navigation docs](/docs/routing/navigation#shared-pathnames)). +Typically, you'll want to call this factory function in a central place in your app where you can easily import from (see [the navigation docs](/docs/routing/navigation)). -These changes bring the existing APIs in line with the new [`createLocalizedPathnamesNavigation` API](/docs/routing/navigation#localized-pathnames) that allows you to localize pathnames: +These changes bring the existing APIs in line with the new [`createLocalizedPathnamesNavigation` API](/docs/routing/navigation) that allows you to localize pathnames: ```tsx filename="navigation.ts" import {createLocalizedPathnamesNavigation, Pathnames} from 'next-intl/navigation'; @@ -84,14 +84,14 @@ By using a similar API, you can upgrade from shared pathnames to localized pathn ### Switching the middleware default of `localePrefix` to `always` -Previously, the [`localePrefix` of the middleware](/docs/routing/middleware#locale-prefix) defaulted to `as-needed`, meaning that a locale prefix was only added for non-default locales. +Previously, the [`localePrefix` of the middleware](/docs/routing#locale-prefix) defaulted to `as-needed`, meaning that a locale prefix was only added for non-default locales. This default has now been changed to `always` since this has two advantages: 1. We can recommend [a safer default `matcher`](/docs/routing/middleware#matcher-config) that needs no extra treatment for pathnames with dots (e.g. `/users/jane.doe`) -2. It avoids an [edge case of `Link`](/docs/routing/middleware#locale-prefix-as-needed) where we include a prefix for the default locale on the server side but patch this on the client side by removing the prefix (certain SEO tools might report a hint that a link points to a redirect in this case). +2. It avoids an [edge case of `Link`](/docs/routing#locale-prefix-as-needed) where we include a prefix for the default locale on the server side but patch this on the client side by removing the prefix (certain SEO tools might report a hint that a link points to a redirect in this case). -If you want to stay on the `as-needed` strategy, you can [configure this option](/docs/routing/middleware#locale-prefix-as-needed) in the middleware. +If you want to stay on the `as-needed` strategy, you can [configure this option](/docs/routing#locale-prefix-as-needed) in the middleware. ### Static rendering of Server Components diff --git a/docs/pages/docs/environments/metadata-route-handlers.mdx b/docs/pages/docs/environments/metadata-route-handlers.mdx index d4e7529e9..bcce590df 100644 --- a/docs/pages/docs/environments/metadata-route-handlers.mdx +++ b/docs/pages/docs/environments/metadata-route-handlers.mdx @@ -86,7 +86,7 @@ Next.js supports providing alternate URLs per language via the [`alternates` ent -If you're using [shared pathnames](/docs/routing/navigation#shared-pathnames), you can iterate over an array of pathnames that your app supports and generate a sitemap entry for each pathname. +If you're use the same pathnames for all locales (i.e. you're not using the [`pathnames`](/docs/routing#pathnames) setting), you can iterate over an array of pathnames that your app supports and generate a sitemap entry for each pathname. **Example:** @@ -121,7 +121,7 @@ export default function sitemap(): MetadataRoute.Sitemap { -If you're using [localized pathnames](/docs/routing/navigation#localized-pathnames), you can use the keys of your already declared `pathnames` and generate an entry for each locale via the [`getPathname`](/docs/routing/navigation#getpathname) function. +If you're using the [`pathnames`](/docs/routing#pathnames) setting, you can use the keys of your already declared `pathnames` and generate an entry for each locale via the [`getPathname`](/docs/routing/navigation#getpathname) function. ```tsx import {MetadataRoute} from 'next'; @@ -159,8 +159,8 @@ export default function sitemap(): MetadataRoute.Sitemap { Note that your implementation may vary depending on your routing configuration (e.g. if you're using a - [`localePrefix`](/docs/routing/middleware#locale-prefix) other than `always` - or [locale-specific domains](/docs/routing/middleware#domain-based-routing)). + [`localePrefix`](/docs/routing#locale-prefix) other than `always` + or [locale-specific domains](/docs/routing#domains)). ### Route Handlers diff --git a/docs/pages/docs/routing.mdx b/docs/pages/docs/routing.mdx index 0eb655259..f9a92e8e3 100644 --- a/docs/pages/docs/routing.mdx +++ b/docs/pages/docs/routing.mdx @@ -1,3 +1,5 @@ +import {Tabs, Tab} from 'nextra-theme-docs'; +import Details from 'components/Details'; import Card from 'components/Card'; import Cards from 'components/Cards'; import Callout from 'components/Callout'; @@ -9,11 +11,335 @@ import Callout from 'components/Callout'; routing](/docs/getting-started/app-router). -When you provide content in multiple languages, you want to make your pages available under distinct pathnames (e.g. `/en/about`). `next-intl` provides the building blocks to set up internationalized routing as well as the navigation APIs to enable you to link between pages. +`next-intl` integrates with the routing system of Next.js in two places: - - - - +1. [**Middleware**](/docs/routing/middleware): Negotiates the locale and handles redirects & rewrites +2. [**Navigation APIs**](/docs/routing/navigation): Provides APIs to navigate between pages -Note that these features are only relevant if you use the App Router. If you're using [`next-intl` with the Pages Router](/docs/getting-started/pages-router), you can use the [built-in capabilities from Next.js](https://nextjs.org/docs/pages/building-your-application/routing/internationalization). +This enables you to express your app in terms of APIs like ``, while aspects like the locale and user-facing pathnames are automatically handled behind the scenes (e.g. `/de/ueber-uns`). + +## Shared configuration + +While the middleware provides a few more options than the navigation APIs, the majority of the configuration is shared between the two and should be used in coordination. Typically, this can be achieved by moving the shared configuration into a separate file like `src/config.ts`: + +``` +src +├── config.ts +├── middleware.ts +└── navigation.ts +``` + + + + +```tsx filename="config.ts" +// A list of all locales that are supported +export const locales = ['en', 'de'] as const; + +// ... +``` + + + + +```tsx filename="middleware.ts" +import createMiddleware from 'next-intl/middleware'; +import {locales, /* ... */} from './config'; + +export default createMiddleware({ + locales, + // ... + + // Used when no locale matches + defaultLocale: 'en' +}); + +export const config = { + // Match only internationalized pathnames + matcher: ['/', '/(de|en)/:path*'] +}; +``` + + + + +```tsx filename="src/navigation.ts" +import {createSharedPathnamesNavigation} from 'next-intl/navigation'; +import {locales, /* ... */} from './config'; + +export const {Link, redirect, usePathname, useRouter} = + createSharedPathnamesNavigation({locales, /* ... */}); +``` + + + + +### Locale prefix + +By default, the pathnames of your app will be available under a prefix that matches your directory structure (e.g. `app/[locale]/about/page.tsx` → `/en/about`). You can however adapt the routing to optionally remove the prefix or customize it per locale by configuring the `localePrefix` setting. + +#### Always use a locale prefix (default) [#locale-prefix-always] + +By default, pathnames always start with the locale (e.g. `/en/about`). + +```tsx filename="config.ts" +import {LocalePrefix} from 'next-intl/routing'; + +export const localePrefix = 'always' satisfies LocalePrefix; + +// ... +``` + +
+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 only want to include a locale prefix for non-default locales, you can configure your routing accordingly: + +```tsx filename="config.ts" +import {LocalePrefix} from 'next-intl/routing'; + +export const localePrefix = 'as-needed' satisfies LocalePrefix; + +// ... +``` + +In this case, requests where the locale prefix matches the default locale will be redirected (e.g. `/en/about` to `/about`). This will affect both prefix-based as well as domain-based routing. + +**Note that:** + +1. If you use this strategy, you should make sure that your middleware matcher detects [unprefixed pathnames](/docs/routing/middleware#matcher-no-prefix). +2. If you use [the `Link` component](/docs/routing/navigation#link), the initial render will point to the prefixed version but will be patched immediately on the client once the component detects that the default locale has rendered. The prefixed version is still valid, but SEO tools might report a hint that the link points to a redirect. + +#### Never use a locale prefix [#locale-prefix-never] + +If you'd like to provide a locale to `next-intl`, e.g. based on user settings, you can consider setting up `next-intl` [without i18n routing](/docs/getting-started/app-router/without-i18n-routing). This way, you don't need to use the routing integration in the first place. + +However, you can also configure the middleware to never show a locale prefix in the URL, which can be helpful in the following cases: + +1. You're using [domain-based routing](#domains) and you support only a single locale per domain +2. You're using a cookie to determine the locale but would like to enable static rendering + +```tsx filename="config.ts" +import {LocalePrefix} from 'next-intl/routing'; + +export const localePrefix = 'never' satisfies LocalePrefix; + +// ... +``` + +In this case, requests for all locales will be rewritten to have the locale only prefixed internally. You still need to place all your pages inside a `[locale]` folder for the routes to be able to receive the `locale` param. + +**Note that:** + +1. If you use this strategy, you should make sure that your matcher detects [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 in the middleware. 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](/docs/routing/middleware#alternate-links) are disabled in this mode since URLs might not be unique per locale. + +#### Custom prefixes [#locale-prefix-custom] + +If you'd like to customize the user-facing prefix, you can provide a locale-based mapping: + +```tsx filename="config.ts" +import {LocalePrefix} from 'next-intl/routing'; + +export const locales = ['en-US', 'de-AT', 'zh'] as const; + +export const localePrefix = { + mode: 'always', + prefixes: { + 'en-US': '/us', + 'de-AT': '/eu/at' + // (/zh will be used as-is) + } +} satisfies LocalePrefix; +``` + +**Note that:** + +1. Custom prefixes are only visible to the user and rewritten internally to the corresponding locale. Therefore the `[locale]` segment will correspond to the locale, not the prefix. +2. You might have to adapt your [middleware matcher](/docs/routing/middleware#matcher-config) to match the custom prefixes. + +
+Can I read the matched prefix in my app? + +Since the custom prefix is rewritten to the locale internally, you can't access the prefix directly. However, you can extract details like the region from the locale: + +```tsx +import {useLocale} from 'next-intl'; + +function Component() { + // Assuming the locale is 'en-US' + const locale = useLocale(); + + // Returns 'US' + new Intl.Locale(locale).region; +} +``` + +The region must be a valid [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) or a [UN M49 region code](https://en.wikipedia.org/wiki/UN_M49#Code_lists). When passed to `Intl.Locale`, the region code is treated as case-insensitive and normalized to uppercase. You can also combine languages with regions where the language is not natively spoken (e.g. `en-AT` describes English as used in Austria). + +Apart from the region, a locale can [encode further properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale#description), like the numbering system. + +If you'd like to encode custom information in the locale, you can use arbitrary [private extensions](https://tc39.es/proposal-intl-locale/#sec-insert-unicode-extension-and-canonicalize), denoted by the `-x-` prefix (e.g. `en-US-x-usd`). The `Intl.Locale` constructor ignores private extensions, but you can extract them from the locale string manually. + +
+ +### Localized pathnames [#pathnames] + +Many apps choose to localize pathnames, especially when search engine optimization is relevant, e.g.: + +- `/en/about` +- `/de/ueber-uns` + +Since you want to define these routes only once internally, you can use the `next-intl` middleware to [rewrite](https://nextjs.org/docs/api-reference/next.config.js/rewrites) such incoming requests to shared pathnames. + +```tsx filename="config.ts" +export const locales = ['en', 'de'] as const; + +// The `pathnames` object holds pairs of internal and +// external paths. Based on the locale, the external +// paths are rewritten to the shared, internal ones. +export const pathnames = { + // If all locales use the same pathname, a single + // external path can be used for all locales + '/': '/', + '/blog': '/blog', + + // If locales use different paths, you can + // specify each external path per locale + '/about': { + en: '/about', + de: '/ueber-uns' + }, + + // Dynamic params are supported via square brackets + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' + }, + + // 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]' + } +} satisfies Pathnames; +``` + +**Note:** Localized pathnames map to a single internal pathname that is created via the file-system based routing in Next.js. If you're using an external system like a CMS to localize pathnames, you'll typically implement this with a catch-all route like `[locale]/[[...slug]]`. + + + If you're using localized pathnames, you should use `createLocalizedPathnamesNavigation` instead of `createSharedPathnamesNavigation` for your [navigation APIs](/docs/routing/navigation). + + +
+How can I revalidate localized pathnames? + +Depending on if a route is generated statically (at build time) or dynamically (at runtime), [`revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) needs to be called either for the localized or the internal pathname. + +Consider this example: + +``` +app +└── [locale] + └── news + └── [slug] +``` + +… with this middleware configuration: + +```tsx filename="middleware.ts" +import createMiddleware from 'next-intl/middleware'; + +export default createMiddleware({ + defaultLocale: 'en', + locales: ['en', 'fr'], + pathnames: { + '/news/[slug]': { + en: '/news/[slug]', + fr: '/infos/[slug]' + } + } +}); +``` + +Depending on whether `some-article` was included in [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) or not, you can revalidate the route like this: + +```tsx +// Statically generated at build time +revalidatePath('/fr/news/some-article'); + +// Dynamically generated at runtime: +revalidatePath('/fr/infos/some-article'); +``` + +When in doubt, you can revalidate both paths to be on the safe side. + +See also: [`vercel/next.js#59825`](https://github.com/vercel/next.js/issues/59825) + +
+ +
+How can I localize dynamic segments? + +If you have a route like `/news/[articleSlug]-[articleId]`, you may want to localize the `articleSlug` part in the pathname like this: + +``` +/en/news/launch-of-new-product-94812 +/de/neuigkeiten/produktneuheit-94812 +``` + +In this case, the localized slug can either be provided by the backend or generated in the frontend by slugifying the localized article title. + +A good practice is to include the ID in the URL, allowing you to retrieve the article based on this information from the backend. The ID can be further used to implement [self-healing URLs](https://mikebifulco.com/posts/self-healing-urls-nextjs-seo), where a redirect is added if the `articleSlug` doesn't match. + +If you localize the values for dynamic segments, you might want to turn off [alternate links](#alternate-links) and provide your own implementation that considers localized values for dynamic segments. + +
+ +### Domains + +If you want to serve your localized content based on different domains, you can provide a list of mappings between domains and locales via the `domains` setting. + +**Examples:** + +- `us.example.com/en` +- `ca.example.com/en` +- `ca.example.com/fr` + +```tsx filename="config.ts" +export const locales = ['en', 'fr'] as const; + +export const domains: DomainsConfig = [ + { + domain: 'us.example.com', + defaultLocale: 'en', + // Optionally restrict the locales available on this domain + locales: ['en'] + }, + { + domain: 'ca.example.com', + defaultLocale: 'en' + // If there are no `locales` specified on a domain, + // all available locales will be supported here + } +]; +``` + +**Note that:** + +1. You can optionally remove the locale prefix in pathnames by changing the [`localePrefix`](#locale-prefix) setting. +2. If no domain matches, the middleware will fall back to the [`defaultLocale`](/docs/routing/middleware#default-locale) (e.g. on `localhost`). \ No newline at end of file diff --git a/docs/pages/docs/routing/middleware.mdx b/docs/pages/docs/routing/middleware.mdx index edb18b7c0..943b2402c 100644 --- a/docs/pages/docs/routing/middleware.mdx +++ b/docs/pages/docs/routing/middleware.mdx @@ -12,10 +12,11 @@ The middleware handles redirects and rewrites based on the detected user locale. ```tsx filename="middleware.ts" import createMiddleware from 'next-intl/middleware'; +import {locales} from './config'; export default createMiddleware({ // A list of all locales that are supported - locales: ['en', 'de'], + locales, // Used when no locale matches defaultLocale: 'en' @@ -29,22 +30,15 @@ export const config = { In addition to handling i18n routing, the middleware sets the `link` header to inform search engines that your content is available in different languages (see [alternate links](#alternate-links)). -## Strategies - -There are two strategies for detecting the locale: - -1. [Prefix-based routing (default)](#prefix-based-routing) -2. [Domain-based routing](#domain-based-routing) +## Locale detection -Once a locale is detected, it will be saved in the `NEXT_LOCALE` cookie. +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. -### Strategy 1: Prefix-based routing (default) [#prefix-based-routing] +### Prefix-based routing (default) [#location-detection-prefix] -Since your pages are nested within a `[locale]` folder, all routes are by default prefixed with one of your supported locales (e.g. `/en/about`). +By default, [prefix-based routing](/docs/routing#locale-prefix) is used to determine the locale of a request. -#### Locale detection [#prefix-locale-detection] - -The locale is detected based on these priorities: +In this case, the locale is detected based on these priorities: 1. A locale prefix is present in the pathname (e.g. `/en/about`) 2. A cookie is present that contains a previously detected locale @@ -61,11 +55,6 @@ To change the locale, users can visit a prefixed route. This will take precedenc 4. When the user clicks on the link, a request to `/de` is initiated. 5. The middleware will update the cookie value to `de`. - - You can optionally remove the locale prefix in pathnames by changing the - [`localePrefix`](#locale-prefix) setting. - -
Which algorithm is used to match the accept-language header against the available locales? @@ -82,51 +71,9 @@ In contrast, the "best fit" algorithm compares the _distance_ between the user's
-### Strategy 2: Domain-based routing [#domain-based-routing] - -If you want to serve your localized content based on different domains, you can provide a list of mappings between domains and locales to the middleware. +### Domain-based routing [#location-detection-domain] -**Examples:** - -- `us.example.com/en` -- `ca.example.com/en` -- `ca.example.com/fr` - -```tsx filename="middleware.ts" -import createMiddleware from 'next-intl/middleware'; - -export default createMiddleware({ - // All locales across all domains - locales: ['en', 'fr'], - - // Used when no domain matches (e.g. on localhost) - defaultLocale: 'en', - - domains: [ - { - domain: 'us.example.com', - defaultLocale: 'en', - // Optionally restrict the locales available on this domain - locales: ['en'] - }, - { - domain: 'ca.example.com', - defaultLocale: 'en' - // If there are no `locales` specified on a domain, - // all available locales will be supported here - } - ] -}); -``` - - - You can optionally remove the locale prefix in pathnames by changing the - [`localePrefix`](#locale-prefix) setting. - - -#### Locale detection [#domain-locale-detection] - -To match the request against the available domains, the host is read from the `x-forwarded-host` header, with a fallback to `host`. +If you're using [domain-based routing](/docs/routing#domains), the middleware will match the request against the available domains to determine the best-matching locale. To retrieve the domain, the host is read from the `x-forwarded-host` header, with a fallback to `host`. The locale is detected based on these priorities: @@ -156,38 +103,13 @@ The bestmatching domain is detected based on these priorities: -## Further configuration - -### Locale prefix - -By default, the pathnames of your app will be available under a prefix that matches your directory structure (e.g. `src/app/[locale]/about/page.tsx` → `/en/about`). You can however customize the routing to optionally remove the prefix. - -Note that if you're using [the navigation APIs from `next-intl`](/docs/routing/navigation), you want to make sure your `localePrefix` setting matches your middleware configuration. - -#### Always use a locale prefix (default) [#locale-prefix-always] +## Configuration -By default, pathnames always start with the locale (e.g. `/en/about`). +The middleware accepts a number of configuration options that are [shared](/docs/routing#shared-configuration) with the [navigation APIs](/docs/routing/navigation). This list contains all options that are specific to the middleware. -```tsx filename="middleware.ts" {6} -import createMiddleware from 'next-intl/middleware'; - -export default createMiddleware({ - // ... other config - - localePrefix: 'always' // This is the default -}); -``` - -
-How can I redirect unprefixed pathnames? +### Default locale [#default-locale] -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](#matcher-no-prefix) too. - -
- -#### Don't use a locale prefix for the default locale [#locale-prefix-as-needed] - -If you don't want to include a locale prefix for the default locale, but only for non-default locales, you can configure the middleware accordingly: +The `defaultLocale` is used as a fallback when none of the available locales match the user's request. ```tsx filename="middleware.ts" {6} import createMiddleware from 'next-intl/middleware'; @@ -195,54 +117,13 @@ import createMiddleware from 'next-intl/middleware'; export default createMiddleware({ // ... other config - localePrefix: 'as-needed' + defaultLocale: 'de' }); ``` -In this case, requests where the locale prefix matches the default locale will be redirected (e.g. `/en/about` to `/about`). This will affect both prefix-based as well as domain-based routing. +### Turning off locale detection [#locale-detection-false] -**Note that:** - -1. If you use this strategy, you should make sure that [your matcher detects unprefixed pathnames](#matcher-no-prefix). -2. If you use [the `Link` component](/docs/routing/navigation#link), the initial render will point to the prefixed version but will be patched immediately on the client once the component detects that the default locale has rendered. The prefixed version is still valid, but SEO tools might report a hint that the link points to a redirect. - -#### Never use a locale prefix [#locale-prefix-never] - - - If you'd like to provide a locale to `next-intl`, e.g. based on user settings, - you can consider setting up `next-intl` [without i18n - routing](/docs/getting-started/app-router/without-i18n-routing). This way, you - don't need to use the middleware in the first place. - - -In case you're using the middleware, but you don't want your pathnames to be prefixed with a locale, you can configure the middleware to never show a locale prefix in the URL. - -This can be useful in the following cases: - -1. You're using [domain-based routing](#domain-based-routing) and you support only a single locale per domain -2. You're using a cookie to determine the locale but would like to enable static rendering - -```tsx filename="middleware.ts" {6} -import createMiddleware from 'next-intl/middleware'; - -export default createMiddleware({ - // ... other config - - localePrefix: 'never' -}); -``` - -In this case, requests for all locales will be rewritten to have the locale only prefixed internally. You still need to place all your pages inside a `[locale]` folder for the routes to be able to receive the `locale` param. - -**Note that:** - -1. If you use this strategy, you should make sure that [your matcher detects unprefixed pathnames](#matcher-no-prefix). -2. If you don't use domain-based routing, the cookie is now the source of truth for determining the locale in the middleware. 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 there might not be distinct URLs per locale. - -### Locale detection [#locale-detection] - -If you want to rely entirely on the URL to resolve the locale, you can disable locale detection based on the `accept-language` header and a potentially existing cookie value from a previous visit. +If you want to rely entirely on the URL to resolve the locale, you can set the `localeDetection` property to `false`. This will disable locale detection based on the `accept-language` header and a potentially existing cookie value from a previous visit. ```tsx filename="middleware.ts" {6} import createMiddleware from 'next-intl/middleware'; @@ -262,7 +143,7 @@ Note that by setting this option, the middleware will no longer return a `set-co The middleware automatically sets [the `link` header](https://developers.google.com/search/docs/specialty/international/localized-versions#http) to inform search engines that your content is available in different languages. Note that this automatically integrates with your routing strategy and will generate the correct links based on your configuration. -If you prefer to include these links yourself, e.g. because you're using a CMS to manage localized slugs of your pages, you can opt-out of this behavior. +If you prefer to include these links yourself, e.g. because you're using a CMS to manage localized slugs of your pages, you can opt-out of this behavior by setting `alternateLinks` to `false`. ```tsx filename="middleware.ts" {6} import createMiddleware from 'next-intl/middleware'; @@ -324,131 +205,6 @@ export default async function middleware(request: NextRequest) { -### Localizing pathnames - -Many apps choose to localize pathnames, especially when search engine optimization is relevant, e.g.: - -- `/en/about` -- `/de/ueber-uns` - -Since you want to define these routes only once internally, you can use the `next-intl` middleware to [rewrite](https://nextjs.org/docs/api-reference/next.config.js/rewrites) such incoming requests to shared pathnames. - -```tsx filename="middleware.ts" -import createMiddleware from 'next-intl/middleware'; - -export default createMiddleware({ - defaultLocale: 'en', - locales: ['en', 'de'], - - // The `pathnames` object holds pairs of internal and - // external paths. Based on the locale, the external - // paths are rewritten to the shared, internal ones. - pathnames: { - // If all locales use the same pathname, a single - // external path can be used for all locales. - '/': '/', - '/blog': '/blog', - - // If locales use different paths, you can - // specify each external path per locale. - '/about': { - en: '/about', - de: '/ueber-uns' - }, - - // Dynamic params are supported via square brackets - '/news/[articleSlug]-[articleId]': { - en: '/news/[articleSlug]-[articleId]', - de: '/neuigkeiten/[articleSlug]-[articleId]' - }, - - // 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]' - } - } -}); -``` - - - If you have pathname localization set up in the middleware, you likely want to - use the [localized navigation - APIs](/docs/routing/navigation#localized-pathnames) in your components. - - -
-How can I revalidate localized pathnames? - -Depending on if a route is generated statically (at build time) or dynamically (at runtime), [`revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) needs to be called either for the localized or the internal pathname. - -Consider this example: - -``` -app -└── [locale] - └── news - └── [slug] -``` - -… with this middleware configuration: - -```tsx filename="middleware.ts" -import createMiddleware from 'next-intl/middleware'; - -export default createMiddleware({ - defaultLocale: 'en', - locales: ['en', 'fr'], - pathnames: { - '/news/[slug]': { - en: '/news/[slug]', - fr: '/infos/[slug]' - } - } -}); -``` - -Depending on whether `some-article` was included in [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) or not, you can revalidate the route like this: - -```tsx -// Statically generated at build time -revalidatePath('/fr/news/some-article'); - -// Dynamically generated at runtime: -revalidatePath('/fr/infos/some-article'); -``` - -When in doubt, you can revalidate both paths to be on the safe side. - -See also: [`vercel/next.js#59825`](https://github.com/vercel/next.js/issues/59825) - -
- -
-How can I localize dynamic segments? - -If you have a route like `/news/[articleSlug]-[articleId]`, you may want to localize the `articleSlug` part in the pathname like this: - -``` -/en/news/launch-of-new-product-94812 -/de/neuigkeiten/produktneuheit-94812 -``` - -In this case, the localized slug can either be provided by the backend or generated in the frontend by slugifying the localized article title. - -A good practice is to include the ID in the URL, allowing you to retrieve the article based on this information from the backend. The ID can be further used to implement [self-healing URLs](https://mikebifulco.com/posts/self-healing-urls-nextjs-seo), where a redirect is added if the `articleSlug` doesn't match. - -If you localize the values for dynamic segments, you might want to turn off [alternate links](#alternate-links) and provide your own implementation that considers localized values for dynamic segments. - -
- ### Matcher config 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`). @@ -470,17 +226,13 @@ This enables:
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 dynamically. However, in case you're self-hosting Next.js via a Node.js server, you can implement the matcher dynamically instead: +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 dynamically. However, you can implement the matcher dynamically instead: ```tsx filename="middleware.ts" import {NextRequest} from 'next/server'; import createIntlMiddleware from 'next-intl/middleware'; -// Can be imported from a shared module -const locales = ['en', 'de']; - const intlMiddleware = createIntlMiddleware({ - locales // ... }); @@ -504,7 +256,7 @@ export default function middleware(request: NextRequest) { There are two use cases where you might want to match pathnames without a locale prefix: -1. You're using a config for [`localePrefix`](#locale-prefix) other than [`always`](#locale-prefix-always) +1. You're using a config for [`localePrefix`](/docs/routing#locale-prefix) other than [`always`](/docs/routing#locale-prefix-always) 2. You want to implement 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. @@ -528,7 +280,7 @@ export const config = { {/* Keep this in sync with `packages/next-intl/test/middleware/middleware.test.tsx` */} -Note that some third-party providers like [Vercel Analytics](https://vercel.com/analytics) and [umami](https://umami.is/docs/running-on-vercel) typically use internal endpoints that are then rewritten to an external URL (e.g. `/_vercel/insights/view`). Make sure to exclude such requests from your middleware matcher so they aren't accidentally rewritten. +Note that some third-party providers like [Vercel Analytics](https://vercel.com/analytics) and [umami](https://umami.is/docs/running-on-vercel) typically use internal endpoints that are then rewritten to an external URL (e.g. `/_vercel/insights/view`). Make sure to exclude such requests from your middleware matcher so they aren't rewritten by accident. ### Base path @@ -537,8 +289,6 @@ The `next-intl` middleware as well as [the navigation APIs](/docs/routing/naviga Note however that you should make sure that your [middleware `matcher`](#matcher-config) handles the root of your base path: ```tsx filename="middleware.ts" -// ... - export const config = { // The `matcher` is relative to the `basePath` matcher: [ @@ -564,6 +314,7 @@ If you need to incorporate additional behavior, you can either modify the reques ```tsx filename="middleware.ts" import createIntlMiddleware from 'next-intl/middleware'; import {NextRequest} from 'next/server'; +import {locales} from './config'; export default async function middleware(request: NextRequest) { // Step 1: Use the incoming request (example) @@ -571,7 +322,7 @@ export default async function middleware(request: NextRequest) { // Step 2: Create and call the next-intl middleware (example) const handleI18nRouting = createIntlMiddleware({ - locales: ['en', 'de'], + locales, defaultLocale }); const response = handleI18nRouting(request); @@ -597,6 +348,7 @@ This example rewrites requests for `/[locale]/profile` to `/[locale]/profile/new ```tsx filename="middleware.ts" import createIntlMiddleware from 'next-intl/middleware'; import {NextRequest} from 'next/server'; +import {locales} from './config'; export default async function middleware(request: NextRequest) { const [, locale, ...segments] = request.nextUrl.pathname.split('/'); @@ -611,7 +363,7 @@ export default async function middleware(request: NextRequest) { } const handleI18nRouting = createIntlMiddleware({ - locales: ['en', 'de'], + locales, defaultLocale: 'en' }); const response = handleI18nRouting(request); @@ -623,7 +375,7 @@ export const config = { }; ``` -Note that if you use a [`localePrefix`](#locale-prefix) other than `always`, you need to adapt the handling appropriately to handle unprefixed pathnames too. +Note that if you use a [`localePrefix`](/docs/routing#locale-prefix) other than `always`, you need to adapt the handling appropriately to handle unprefixed pathnames too. ### Example: Integrating with Clerk @@ -632,9 +384,10 @@ Note that if you use a [`localePrefix`](#locale-prefix) other than `always`, you ```tsx filename="middleware.ts" import {clerkMiddleware, createRouteMatcher} from '@clerk/nextjs/server'; import createMiddleware from 'next-intl/middleware'; +import {locales} from './config'; const intlMiddleware = createMiddleware({ - locales: ['en', 'de'], + locales, defaultLocale: 'en' }); @@ -662,9 +415,10 @@ You can do so by following the [setup guide from Supabase](https://supabase.com/ import {type NextRequest} from 'next/server'; import {createServerClient, type CookieOptions} from '@supabase/ssr'; import createIntlMiddleware from 'next-intl/middleware'; +import {locales} from './config'; const handleI18nRouting = createIntlMiddleware({ - locales: ['en', 'de'], + locales, defaultLocale: 'en' }); @@ -712,13 +466,12 @@ For pathnames specified in [the `pages` object](https://next-auth.js.org/configu import {withAuth} from 'next-auth/middleware'; import createIntlMiddleware from 'next-intl/middleware'; import {NextRequest} from 'next/server'; +import {locales} from './config'; -const locales = ['en', 'de']; const publicPages = ['/', '/login']; const intlMiddleware = createIntlMiddleware({ locales, - localePrefix: 'as-needed', defaultLocale: 'en' }); @@ -768,13 +521,13 @@ There's a working [example that combines `next-intl` with Auth.js](/examples#app ## Usage without middleware (static export) -If you're using the [static export feature from Next.js](https://nextjs.org/docs/app/building-your-application/deploying/static-exports) (`output: 'export'`), the middleware will not run. You can use [prefix-based routing](#prefix-based-routing) nontheless to internationalize your app, but a few tradeoffs apply. +If you're using the [static export](https://nextjs.org/docs/app/building-your-application/deploying/static-exports) feature from Next.js (`output: 'export'`), the middleware will not run. You can use [prefix-based routing](/docs/routing#locale-prefix) nontheless to internationalize your app, but a few tradeoffs apply. **Static export limitations:** -1. There's no default locale that can be used without a prefix (same as [`localePrefix: 'always'`](#locale-prefix-always)) -2. The locale can't be negotiated at runtime (same as [`localeDetection: false`](#locale-detection)) -3. You can't use [pathname localization](#localizing-pathnames) +1. There's no default locale that can be used without a prefix (same as [`localePrefix: 'always'`](/docs/routing#locale-prefix-always)) +2. The locale can't be negotiated at runtime (same as [`localeDetection: false`](#locale-detection-false)) +3. You can't use [pathname localization](/docs/routing#pathnames) 4. This requires [static rendering](/docs/getting-started/app-router/with-i18n-routing#static-rendering) 5. You need to add a redirect for the root of the app @@ -804,12 +557,12 @@ This can happen either because: To recover from this error, please make sure that: 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 not using i18n routing: - 1. You don't read the `locale` param in `getRequestConfig` but instead return it. -3. If you're using i18n 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'`](#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#i18nts) before it's used by any APIs from `next-intl`. + 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#i18nts) 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. diff --git a/docs/pages/docs/routing/navigation.mdx b/docs/pages/docs/routing/navigation.mdx index 491c67e1d..c06c1a758 100644 --- a/docs/pages/docs/routing/navigation.mdx +++ b/docs/pages/docs/routing/navigation.mdx @@ -9,61 +9,24 @@ import Details from 'components/Details'; routing](/docs/getting-started/app-router). -`next-intl` provides drop-in replacements for common Next.js navigation APIs that automatically handle the user locale behind the scenes. +`next-intl` provides drop-in replacements for common Next.js navigation APIs that automatically handle the user locale and pathnames behind the scenes. -## Strategies +Depending on if you're using localized pathnames (i.e. the [`pathnames`](/docs/routing#pathnames) setting), you can pick from one of these functions to create the corresponding navigation APIs: -There are two strategies that you can use based on your needs. +- `createSharedPathnamesNavigation`: Pathnames are shared across all locales (default) +- `createLocalizedPathnamesNavigation`: Pathnames are provided per locale (use with `pathnames`) -**Shared pathnames:** The simplest case is when your app uses the same pathnames, regardless of the locale. +These functions are typically called in a central module like `src/navigation.ts` in order to provide easy access to navigation APIs in your components and should receive configuration options that are [shared](/docs/routing#shared-configuration) with the middleware. -For example: - -- `/en/about` -- `/de/about` - -**Localized pathnames:** Many apps choose to localize pathnames, especially when search engine optimization is relevant. In this case, you'll provide distinct pathnames based on the user locale. - -For example: - -- `/en/about` -- `/de/ueber-uns` - -**Note:** The terms "shared" and "localized" pathnames are used to refer to pathnames that are created via the file-system based routing in Next.js. If you're using an external system like a CMS to localize pathnames, you'll typically implement this with a catch-all route like `[locale]/[[...slug]]`. - ---- - -Each strategy will provide you with corresponding [navigation APIs](#apis) that you'll typically provide in a central module to easily access them in components (e.g. `src/navigation.ts`). To ensure consistent usage in your app, you can consider [linting for usage of these APIs](/docs/workflows/linting#consistent-usage-of-navigation-apis). - -### Strategy 1: Shared pathnames [#shared-pathnames] - -With this strategy, the pathnames of your app are identical for all locales. This is the simplest case, because the routes you define in Next.js will map directly to the pathnames that a user can request. - -To create [navigation APIs](#apis) for this strategy, use the `createSharedPathnamesNavigation` function: + + ```tsx filename="navigation.ts" import {createSharedPathnamesNavigation} from 'next-intl/navigation'; - -export const locales = ['en', 'de'] as const; -export const localePrefix = 'always'; // Default +import {locales, /* ... */} from './config'; export const {Link, redirect, usePathname, useRouter} = - createSharedPathnamesNavigation({locales, localePrefix}); -``` - -The `locales` as well as the `localePrefix` argument is identical to the configuration that you pass to the middleware. You might want to share these values via a central configuration to keep them in sync. - -```tsx filename="middleware.ts" -import createMiddleware from 'next-intl/middleware'; -import {locales, localePrefix} from './navigation'; - -export default createMiddleware({ - defaultLocale: 'en', - localePrefix, - locales -}); - -// ... + createSharedPathnamesNavigation({locales, /* ... */}); ```
@@ -75,67 +38,15 @@ Note however that the `locales` argument for the middleware is mandatory. You ca
-### Strategy 2: Localized pathnames [#localized-pathnames] - -When using this strategy, you have to provide distinct pathnames for every locale that your app supports. However, the localized variants will be handled by a single route internally, therefore a mapping needs to be provided that is also [consumed by the middleware](/docs/routing/middleware#localizing-pathnames). - -You can use the `createLocalizedPathnamesNavigation` function to create corresponding [navigation APIs](#apis): +
+ ```tsx filename="navigation.ts" -import { - createLocalizedPathnamesNavigation, - Pathnames -} from 'next-intl/navigation'; - -export const locales = ['en', 'de'] as const; -export const localePrefix = 'always'; // Default - -// The `pathnames` object holds pairs of internal -// and external paths, separated by locale. -export const pathnames = { - // If all locales use the same pathname, a - // single external path can be provided. - '/': '/', - '/blog': '/blog', - - // If locales use different paths, you can - // specify each external path per locale. - '/about': { - en: '/about', - de: '/ueber-uns' - }, - - // Dynamic params are supported via square brackets - '/news/[articleSlug]-[articleId]': { - en: '/news/[articleSlug]-[articleId]', - de: '/neuigkeiten/[articleSlug]-[articleId]' - }, - - // Also (optional) catch-all segments are supported - '/categories/[...slug]': { - en: '/categories/[...slug]', - de: '/kategorien/[...slug]' - } -} satisfies Pathnames; +import {createLocalizedPathnamesNavigation} from 'next-intl/navigation'; +import {locales, pathnames, /* ... */} from './config'; export const {Link, redirect, usePathname, useRouter, getPathname} = - createLocalizedPathnamesNavigation({locales, localePrefix, pathnames}); -``` - -The arguments `locales`, `localePrefix` as well as `pathnames` are identical to the configuration that you pass to the middleware. You might want to share these values via a central configuration to make sure they stay in sync. - -```tsx filename="middleware.ts" -import createMiddleware from 'next-intl/middleware'; -import {locales, localePrefix, pathnames} from './navigation'; - -export default createMiddleware({ - defaultLocale: 'en', - localePrefix, - locales, - pathnames -}); - -// ... + createLocalizedPathnamesNavigation({locales, pathnames, /* ... */}); ``` @@ -143,6 +54,16 @@ export default createMiddleware({ working implementation of localized pathnames. + +
+ +
+How can I ensure consistent usage of navigation APIs? + +To ensure consistent usage in your app, you can consider [linting for usage of these APIs](/docs/workflows/linting#consistent-usage-of-navigation-apis). + +
+ ## APIs ### `Link` @@ -210,7 +131,7 @@ See also the Next.js docs on [creating an active link component](https://nextjs. -When using [localized pathnames](#localized-pathnames), the `href` prop corresponds to an internal pathname, but will be mapped to a locale-specific pathname. +When using [localized pathnames](/docs/routing#pathnames), the `href` prop corresponds to an internal pathname, but will be mapped to a locale-specific pathname. ```tsx import {Link} from '../navigation'; @@ -308,7 +229,7 @@ See also the Next.js docs on [creating an active link component](https://nextjs.