Skip to content

Commit 1c68e3b

Browse files
authored
feat: Add localePrefix for navigation APIs for an improved initial render of Link when using localePrefix: never. Also fix edge case in middleware when using localized pathnames for redirects that remove a locale prefix (fixes an infinite loop). (#678)
By accepting an optional `localePrefix` for the navigation APIs, we can get the initial render of the `href` of `Link` right if `localePrefix: 'never'` is set. This can be helpful if domain-based routing is used and you have a single locale per domain. Note that this change is backward-compatible. It's now recommended to set the `localePrefix` for the navigation APIs to get improved behavior for `Link` in case `localePrefix: 'never'` is used, but otherwise your app will keep working with the previous behavior. Ref #444
1 parent ae4c2db commit 1c68e3b

36 files changed

+1368
-736
lines changed

docs/pages/docs/routing/middleware.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ The bestmatching domain is detected based on these priorities:
140140

141141
### Locale prefix
142142

143+
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.
144+
143145
#### Always use a locale prefix [#locale-prefix-always]
144146

145147
By default, pathnames always start with the locale (e.g. `/en/about`).
@@ -200,7 +202,7 @@ In this case, requests for all locales will be rewritten to have the locale only
200202

201203
<Callout>
202204
Note that [alternate links](#disable-alternate-links) are disabled in this
203-
mode since there are no distinct URLs per language.
205+
mode since there might not be distinct URLs per locale.
204206
</Callout>
205207

206208
### Disable automatic locale detection

docs/pages/docs/routing/navigation.mdx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,21 @@ To create [navigation APIs](#apis) for this strategy, use the `createSharedPathn
3939
import {createSharedPathnamesNavigation} from 'next-intl/navigation';
4040

4141
export const locales = ['en', 'de'] as const;
42+
export const localePrefix = 'always'; // Default
4243

4344
export const {Link, redirect, usePathname, useRouter} =
44-
createSharedPathnamesNavigation({locales});
45+
createSharedPathnamesNavigation({locales, localePrefix});
4546
```
4647

47-
The `locales` argument is identical to the configuration that you pass to the middleware. To reuse it there, you can import the `locales` into the middleware.
48+
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.
4849

4950
```tsx filename="middleware.ts"
5051
import createMiddleware from 'next-intl/middleware';
51-
import {locales} from './navigation';
52+
import {locales, localePrefix} from './navigation';
5253

5354
export default createMiddleware({
5455
defaultLocale: 'en',
56+
localePrefix,
5557
locales
5658
});
5759
```
@@ -69,6 +71,7 @@ import {
6971
} from 'next-intl/navigation';
7072

7173
export const locales = ['en', 'de'] as const;
74+
export const localePrefix = 'always'; // Default
7275

7376
// The `pathnames` object holds pairs of internal
7477
// and external paths, separated by locale.
@@ -99,17 +102,18 @@ export const pathnames = {
99102
} satisfies Pathnames<typeof locales>;
100103

101104
export const {Link, redirect, usePathname, useRouter, getPathname} =
102-
createLocalizedPathnamesNavigation({locales, pathnames});
105+
createLocalizedPathnamesNavigation({locales, localePrefix, pathnames});
103106
```
104107

105-
The `pathnames` argument is identical to the configuration that you pass to the middleware for [localizing pathnames](/docs/routing/middleware#localizing-pathnames). Because of this, you might want to import the `locales` and `pathnames` into the middleware.
108+
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.
106109

107110
```tsx filename="middleware.ts"
108111
import createMiddleware from 'next-intl/middleware';
109-
import {locales, pathnames} from './navigation';
112+
import {locales, localePrefix, pathnames} from './navigation';
110113

111114
export default createMiddleware({
112115
defaultLocale: 'en',
116+
localePrefix,
113117
locales,
114118
pathnames
115119
});

examples/example-app-router-playground/src/middleware.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import createMiddleware from 'next-intl/middleware';
2-
import {locales, pathnames} from './navigation';
2+
import {locales, pathnames, localePrefix} from './navigation';
33

44
export default createMiddleware({
55
defaultLocale: 'en',
6-
localePrefix: 'as-needed',
6+
localePrefix,
77
pathnames,
88
locales
99
});

examples/example-app-router-playground/src/navigation.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55

66
export const locales = ['en', 'de', 'es'] as const;
77

8+
export const localePrefix = 'as-needed';
9+
810
export const pathnames = {
911
'/': '/',
1012
'/client': '/client',
@@ -25,5 +27,6 @@ export const pathnames = {
2527
export const {Link, redirect, usePathname, useRouter} =
2628
createLocalizedPathnamesNavigation({
2729
locales,
30+
localePrefix,
2831
pathnames
2932
});

examples/example-app-router/src/components/LocaleSwitcher.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {useLocale, useTranslations} from 'next-intl';
2-
import {locales} from 'config';
2+
import {locales} from '../config';
33
import LocaleSwitcherSelect from './LocaleSwitcherSelect';
44

55
export default function LocaleSwitcher() {

examples/example-app-router/src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ export const pathnames = {
1010
}
1111
} satisfies Pathnames<typeof locales>;
1212

13+
// Use the default: `always`
14+
export const localePrefix = undefined;
15+
1316
export type AppPathnames = keyof typeof pathnames;

examples/example-app-router/src/middleware.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import createMiddleware from 'next-intl/middleware';
2-
import {pathnames, locales} from './config';
2+
import {pathnames, locales, localePrefix} from './config';
33

44
export default createMiddleware({
55
defaultLocale: 'en',
66
locales,
7-
pathnames
7+
pathnames,
8+
localePrefix
89
});
910

1011
export const config = {
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {createLocalizedPathnamesNavigation} from 'next-intl/navigation';
2-
import {locales, pathnames} from './config';
2+
import {locales, pathnames, localePrefix} from './config';
33

44
export const {Link, redirect, usePathname, useRouter} =
55
createLocalizedPathnamesNavigation({
66
locales,
7-
pathnames
7+
pathnames,
8+
localePrefix
89
});

packages/next-intl/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"@types/negotiator": "^0.6.1",
8787
"@types/node": "^17.0.23",
8888
"@types/react": "18.2.34",
89+
"@types/react-dom": "^18.2.17",
8990
"eslint": "^8.54.0",
9091
"eslint-config-molindo": "^7.0.0",
9192
"eslint-plugin-deprecation": "^1.4.1",
@@ -110,11 +111,11 @@
110111
},
111112
{
112113
"path": "dist/production/navigation.react-client.js",
113-
"limit": "2.6 KB"
114+
"limit": "2.62 KB"
114115
},
115116
{
116117
"path": "dist/production/navigation.react-server.js",
117-
"limit": "2.75 KB"
118+
"limit": "2.8 KB"
118119
},
119120
{
120121
"path": "dist/production/server.js",

packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import {AllLocales, Pathnames} from '../shared/types';
2-
3-
type LocalePrefix = 'as-needed' | 'always' | 'never';
1+
import {AllLocales, LocalePrefix, Pathnames} from '../shared/types';
42

53
type RoutingBaseConfig<Locales extends AllLocales> = {
64
/** A list of all locales that are supported. */

packages/next-intl/src/middleware/middleware.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,16 @@ export default function createMiddleware<Locales extends AllLocales>(
178178
response = redirect(pathWithSearch);
179179
}
180180
} else {
181-
const pathWithSearch = getPathWithSearch(
181+
const internalPathWithSearch = getPathWithSearch(
182182
pathname,
183183
request.nextUrl.search
184184
);
185185

186186
if (hasLocalePrefix) {
187-
const basePath = getBasePath(pathWithSearch, pathLocale);
187+
const basePath = getBasePath(
188+
getPathWithSearch(normalizedPathname, request.nextUrl.search),
189+
pathLocale
190+
);
188191

189192
if (configWithDefaults.localePrefix === 'never') {
190193
response = redirect(basePath);
@@ -205,10 +208,10 @@ export default function createMiddleware<Locales extends AllLocales>(
205208
if (domain?.domain !== pathDomain?.domain && !hasUnknownHost) {
206209
response = redirect(basePath, pathDomain?.domain);
207210
} else {
208-
response = rewrite(pathWithSearch);
211+
response = rewrite(internalPathWithSearch);
209212
}
210213
} else {
211-
response = rewrite(pathWithSearch);
214+
response = rewrite(internalPathWithSearch);
212215
}
213216
}
214217
} else {
@@ -221,9 +224,9 @@ export default function createMiddleware<Locales extends AllLocales>(
221224
(configWithDefaults.localePrefix === 'as-needed' ||
222225
configWithDefaults.domains))
223226
) {
224-
response = rewrite(`/${locale}${pathWithSearch}`);
227+
response = rewrite(`/${locale}${internalPathWithSearch}`);
225228
} else {
226-
response = redirect(`/${locale}${pathWithSearch}`);
229+
response = redirect(`/${locale}${internalPathWithSearch}`);
227230
}
228231
}
229232
}

packages/next-intl/src/navigation/react-client/BaseLink.tsx renamed to packages/next-intl/src/navigation/react-client/ClientLink.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
import React, {ComponentProps, ReactElement, forwardRef} from 'react';
22
import useLocale from '../../react-client/useLocale';
3-
import BaseLinkWithLocale from '../../shared/BaseLinkWithLocale';
43
import {AllLocales} from '../../shared/types';
4+
import BaseLink from '../shared/BaseLink';
55

66
type Props<Locales extends AllLocales> = Omit<
7-
ComponentProps<typeof BaseLinkWithLocale>,
7+
ComponentProps<typeof BaseLink>,
88
'locale'
99
> & {
1010
locale?: Locales[number];
1111
};
1212

13-
function BaseLink<Locales extends AllLocales>(
13+
function ClientLink<Locales extends AllLocales>(
1414
{locale, ...rest}: Props<Locales>,
1515
ref: Props<Locales>['ref']
1616
) {
1717
const defaultLocale = useLocale();
1818
const linkLocale = locale || defaultLocale;
1919
return (
20-
<BaseLinkWithLocale
21-
ref={ref}
22-
hrefLang={linkLocale}
23-
locale={linkLocale}
24-
{...rest}
25-
/>
20+
<BaseLink ref={ref} hrefLang={linkLocale} locale={linkLocale} {...rest} />
2621
);
2722
}
2823

@@ -46,8 +41,10 @@ function BaseLink<Locales extends AllLocales>(
4641
* the `set-cookie` response header would cause the locale cookie on the current
4742
* page to be overwritten before the user even decides to change the locale.
4843
*/
49-
const BaseLinkWithRef = forwardRef(BaseLink) as <Locales extends AllLocales>(
44+
const ClientLinkWithRef = forwardRef(ClientLink) as <
45+
Locales extends AllLocales
46+
>(
5047
props: Props<Locales> & {ref?: Props<Locales>['ref']}
5148
) => ReactElement;
52-
(BaseLinkWithRef as any).displayName = 'Link';
53-
export default BaseLinkWithRef;
49+
(ClientLinkWithRef as any).displayName = 'ClientLink';
50+
export default ClientLinkWithRef;
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import useLocale from '../../react-client/useLocale';
2-
import redirectWithLocale from '../../shared/redirectWithLocale';
3-
import {ParametersExceptFirstTwo} from '../../shared/types';
2+
import {LocalePrefix, ParametersExceptFirstTwo} from '../../shared/types';
3+
import baseRedirect from '../shared/baseRedirect';
44

5-
export default function baseRedirect(
6-
pathname: string,
7-
...args: ParametersExceptFirstTwo<typeof redirectWithLocale>
5+
export default function clientRedirect(
6+
params: {localePrefix?: LocalePrefix; pathname: string},
7+
...args: ParametersExceptFirstTwo<typeof baseRedirect>
88
) {
99
let locale;
1010
try {
@@ -18,5 +18,5 @@ export default function baseRedirect(
1818
);
1919
}
2020

21-
return redirectWithLocale(pathname, locale, ...args);
21+
return baseRedirect({...params, locale}, ...args);
2222
}

0 commit comments

Comments
 (0)