Skip to content

Commit 126013b

Browse files
authored
feat: createNavigation (#1316)
This PR provides a new **`createNavigation`** function that supersedes the previously available APIs: 1. `createSharedPathnamesNavigation` 2. `createLocalizedPathnamesNavigation` The new function unifies the API for both use cases and also fixes a few quirks in the previous APIs. **Usage** ```tsx import {createNavigation} from 'next-intl/navigation'; import {defineRouting} from 'next-intl/routing'; export const routing = defineRouting(/* ... */); export const {Link, redirect, usePathname, useRouter} = createNavigation(routing); ``` (see the [updated navigation docs](https://next-intl-docs-git-feat-create-navigation-next-intl.vercel.app/docs/routing/navigation)) **Improvements** 1. A single API can be used both for shared as well as localized pathnames. This reduces the API surface and simplifies the corresponding docs. 2. `Link` can now be composed seamlessly into another component with its `href` prop without having to add a generic type argument. 3. `getPathname` is now available for both shared as well as localized pathnames (fixes #785) 4. `router.push` and `redirect` now accept search params consistently via the object form (e.g. `router.push({pathname: '/users', query: {sortBy: 'name'})`)—regardless of if you're using shared or localized pathnames. 5. When using `localePrefix: 'as-necessary'`, the initial render of `Link` now uses the correct pathname immediately during SSR (fixes [#444](#444)). Previously, a prefix for the default locale was added during SSR and removed during hydration. Also `redirect` now gets the final pathname right without having to add a superfluous prefix (fixes [#1335](#1335)). The only exception is when you use `localePrefix: 'as-necessary'` in combination with `domains` (see [Special case: Using `domains` with `localePrefix: 'as-needed'`](https://next-intl-docs-git-feat-create-navigation-next-intl.vercel.app/docs/routing#domains-localeprefix-asneeded)) 6. `Link` is now compatible with the `asChild` prop of Radix Primitives when rendered in RSC (see [#1322](#1322)) **Migrating to `createNavigation`** `createNavigation` is generally considered a drop-in replacement, but a few changes might be necessary: 1. `createNavigation` is expected to receive your complete routing configuration. Ideally, you define this via the [`defineRouting`](https://next-intl-docs.vercel.app/docs/routing#define-routing) function and pass the result to `createNavigation`. 2. If you've used `createLocalizedPathnamesNavigation` and have [composed the `Link` with its `href` prop](https://next-intl-docs.vercel.app/docs/routing/navigation#link-composition), you should no longer provide the generic `Pathname` type argument (see [updated docs](https://next-intl-docs-git-feat-create-navigation-next-intl.vercel.app/docs/routing/navigation#link-composition)). ```diff - ComponentProps<typeof Link<Pathname>> + ComponentProps<typeof Link> ``` 3. If you've used [`redirect`](https://next-intl-docs.vercel.app/docs/routing/navigation#redirect), you now have to provide an explicit locale (even if it's just [the current locale](https://next-intl-docs.vercel.app/docs/usage/configuration#locale)). This change was necessary for an upcoming change in Next.js 15 where `headers()` turns into a promise (see [#1375](#1375) for details). ```diff - redirect('/about') + redirect({pathname: '/about', locale: 'en'}) ``` 4. If you've used [`getPathname`](https://next-intl-docs.vercel.app/docs/routing/navigation#getpathname) and have previously manually prepended a locale prefix, you should no longer do so—`getPathname` now takes care of this depending on your routing strategy. ```diff - '/'+ locale + getPathname(/* ... */) + getPathname(/* ... */); ``` 5. If you're using a combination of `localePrefix: 'as-necessary'` and `domains` and you're using `getPathname`, you now need to provide a `domain` argument (see [Special case: Using `domains` with `localePrefix: 'as-needed'`](https://next-intl-docs-git-feat-create-navigation-next-intl.vercel.app/docs/routing#domains-localeprefix-asneeded))
1 parent 2fb56b1 commit 126013b

File tree

72 files changed

+3478
-587
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+3478
-587
lines changed

docs/pages/docs/getting-started/app-router/with-i18n-routing.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ To share the configuration between these two places, we'll set up `routing.ts`:
107107

108108
```ts filename="src/i18n/routing.ts"
109109
import {defineRouting} from 'next-intl/routing';
110-
import {createSharedPathnamesNavigation} from 'next-intl/navigation';
110+
import {createNavigation} from 'next-intl/navigation';
111111

112112
export const routing = defineRouting({
113113
// A list of all locales that are supported
@@ -120,7 +120,7 @@ export const routing = defineRouting({
120120
// Lightweight wrappers around Next.js' navigation APIs
121121
// that will consider the routing configuration
122122
export const {Link, redirect, usePathname, useRouter} =
123-
createSharedPathnamesNavigation(routing);
123+
createNavigation(routing);
124124
```
125125

126126
Depending on your requirements, you may wish to customize your routing configuration later—but let's finish with the setup first.

docs/pages/docs/routing.mdx

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Depending on your routing needs, you may wish to consider further settings.
4141

4242
In case you're building an app where locales can be added and removed at runtime, you can provide the routing configuration for the middleware [dynamically per request](/docs/routing/middleware#composing-other-middlewares).
4343

44-
To create the corresponding navigation APIs, you can [omit the `locales` argument](/docs/routing/navigation#locales-unknown) from `createSharedPathnamesNavigation` in this case.
44+
To create the corresponding navigation APIs, you can [omit the `locales` argument](/docs/routing/navigation#locales-unknown) from `createNavigation` in this case.
4545

4646
</Details>
4747

@@ -84,19 +84,16 @@ export const routing = defineRouting({
8484

8585
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.
8686

87-
**Note that:**
88-
89-
1. If you use this strategy, you should make sure that your middleware matcher detects [unprefixed pathnames](/docs/routing/middleware#matcher-no-prefix).
90-
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.
87+
**Note that:** If you use this strategy, you should make sure that your middleware matcher detects [unprefixed pathnames](/docs/routing/middleware#matcher-no-prefix) for the routing to work as expected.
9188

9289
#### Never use a locale prefix [#locale-prefix-never]
9390

9491
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.
9592

9693
However, you can also configure the middleware to never show a locale prefix in the URL, which can be helpful in the following cases:
9794

98-
1. You're using [domain-based routing](#domains) and you support only a single locale per domain
99-
2. You're using a cookie to determine the locale but would like to enable static rendering
95+
1. You want to use [domain-based routing](#domains) and have only one locale per domain
96+
2. You want to use a cookie to determine the locale while enabling static rendering
10097

10198
```tsx filename="routing.ts" {5}
10299
import {defineRouting} from 'next-intl/routing';
@@ -153,8 +150,8 @@ function Component() {
153150
// Assuming the locale is 'en-US'
154151
const locale = useLocale();
155152

156-
// Returns 'US'
157-
new Intl.Locale(locale).region;
153+
// Extracts the "US" region
154+
const {region} = new Intl.Locale(locale);
158155
}
159156
```
160157

@@ -222,13 +219,6 @@ export const routing = defineRouting({
222219

223220
Localized pathnames map to a single internal pathname that is created via the file-system based routing in Next.js. In the example above, `/de/ueber-uns` will be handled by the page at `/[locale]/about/page.tsx`.
224221

225-
<Callout>
226-
If you're using localized pathnames, you should use
227-
`createLocalizedPathnamesNavigation` instead of
228-
`createSharedPathnamesNavigation` for your [navigation
229-
APIs](/docs/routing/navigation).
230-
</Callout>
231-
232222
<Details id="localized-pathnames-revalidation">
233223
<summary>How can I revalidate localized pathnames?</summary>
234224

@@ -403,3 +393,72 @@ PORT=3001 npm run dev
403393
```
404394

405395
</Details>
396+
397+
<Details id="domains-localeprefix-individual">
398+
<summary>Can I use a different `localePrefix` setting per domain?</summary>
399+
400+
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.
401+
402+
However, you can still achieve this by building the app for each domain separately, while injecting diverging routing configuration via an environment variable.
403+
404+
**Example:**
405+
406+
```tsx filename="routing.ts"
407+
import {defineRouting} from 'next-intl/routing';
408+
409+
export const routing = defineRouting({
410+
locales: ['en', 'fr'],
411+
defaultLocale: 'en',
412+
localePrefix:
413+
process.env.VERCEL_PROJECT_PRODUCTION_URL === 'us.example.com'
414+
? 'never'
415+
: 'always',
416+
domains: [
417+
{
418+
domain: 'us.example.com',
419+
defaultLocale: 'en',
420+
locales: ['en']
421+
},
422+
{
423+
domain: 'ca.example.com',
424+
defaultLocale: 'en'
425+
}
426+
]
427+
});
428+
```
429+
430+
</Details>
431+
432+
<Details id="domains-localeprefix-asneeded">
433+
<summary>Special case: Using `domains` with `localePrefix: 'as-needed'`</summary>
434+
435+
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).
436+
437+
1. [`<Link />`](/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.
438+
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 `<Link />`, the middleware will potentially clean up a superfluous prefix.
439+
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).
440+
441+
```tsx
442+
import {getPathname} from '@/i18n/routing';
443+
import {headers} from 'next/headers';
444+
445+
// Case 1: Statically known domain
446+
const domain = 'ca.example.com';
447+
448+
// Case 2: Read at runtime (dynamic rendering)
449+
const domain = headers().get('x-forwarded-host');
450+
451+
// Assuming the current domain is `ca.example.com`,
452+
// the returned pathname will be `/about`
453+
const pathname = getPathname({
454+
href: '/about',
455+
locale: 'en',
456+
domain
457+
});
458+
```
459+
460+
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.
461+
462+
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).
463+
464+
</Details>

docs/pages/docs/routing/middleware.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ The middleware receives a [`routing`](/docs/routing#define-routing) configuratio
1414
2. Applying relevant redirects & rewrites
1515
3. Providing [alternate links](#alternate-links) for search engines
1616

17+
**Example:**
18+
1719
```tsx filename="middleware.ts"
1820
import createMiddleware from 'next-intl/middleware';
1921
import {routing} from './i18n/routing';

0 commit comments

Comments
 (0)