Skip to content

Commit

Permalink
feat: createNavigation (#1316)
Browse files Browse the repository at this point in the history
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))
  • Loading branch information
amannn authored Oct 1, 2024
1 parent 2fb56b1 commit 126013b
Show file tree
Hide file tree
Showing 72 changed files with 3,478 additions and 587 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ To share the configuration between these two places, we'll set up `routing.ts`:

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

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

Depending on your requirements, you may wish to customize your routing configuration later—but let's finish with the setup first.
Expand Down
91 changes: 75 additions & 16 deletions docs/pages/docs/routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Depending on your routing needs, you may wish to consider further settings.

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

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

</Details>

Expand Down Expand Up @@ -84,19 +84,16 @@ export const routing = defineRouting({

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

#### 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
1. You want to use [domain-based routing](#domains) and have only one locale per domain
2. You want to use a cookie to determine the locale while enabling static rendering

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

// Returns 'US'
new Intl.Locale(locale).region;
// Extracts the "US" region
const {region} = new Intl.Locale(locale);
}
```

Expand Down Expand Up @@ -222,13 +219,6 @@ export const routing = defineRouting({

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

<Callout>
If you're using localized pathnames, you should use
`createLocalizedPathnamesNavigation` instead of
`createSharedPathnamesNavigation` for your [navigation
APIs](/docs/routing/navigation).
</Callout>

<Details id="localized-pathnames-revalidation">
<summary>How can I revalidate localized pathnames?</summary>

Expand Down Expand Up @@ -403,3 +393,72 @@ PORT=3001 npm run dev
```

</Details>

<Details id="domains-localeprefix-individual">
<summary>Can I use a different `localePrefix` setting per domain?</summary>

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.

**Example:**

```tsx filename="routing.ts"
import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
locales: ['en', 'fr'],
defaultLocale: 'en',
localePrefix:
process.env.VERCEL_PROJECT_PRODUCTION_URL === 'us.example.com'
? 'never'
: 'always',
domains: [
{
domain: 'us.example.com',
defaultLocale: 'en',
locales: ['en']
},
{
domain: 'ca.example.com',
defaultLocale: 'en'
}
]
});
```

</Details>

<Details id="domains-localeprefix-asneeded">
<summary>Special case: Using `domains` with `localePrefix: 'as-needed'`</summary>

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. [`<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.
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.
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).

</Details>
2 changes: 2 additions & 0 deletions docs/pages/docs/routing/middleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The middleware receives a [`routing`](/docs/routing#define-routing) configuratio
2. Applying relevant redirects & rewrites
3. Providing [alternate links](#alternate-links) for search engines

**Example:**

```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
Expand Down
Loading

0 comments on commit 126013b

Please sign in to comment.