Skip to content

Commit 5905976

Browse files
authored
feat: Add forcePrefix option for redirect and getPathname (#1865)
When using a [`localePrefix`](https://next-intl.dev/docs/routing#localeprefix) setting other than `always`, you can now enforce a locale prefix by setting the `forcePrefix` option to `true`. This is useful when changing the user's locale and you need to update the [locale cookie](https://next-intl.dev/docs/routing#locale-cookie) first. ```tsx // Will initially redirect to `/en/about` to update the locale // cookie, regardless of your `localePrefix` setting redirect({href: '/about', locale: 'en', forcePrefix: true}); ``` Resolves #1845 → [Updated docs](https://next-intl.dev/docs/routing/navigation#redirect)
1 parent a7b5c84 commit 5905976

File tree

4 files changed

+60
-32
lines changed

4 files changed

+60
-32
lines changed

docs/src/pages/docs/routing/navigation.mdx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,9 @@ const pathname = usePathname();
292292

293293
### `redirect`
294294

295-
If you want to interrupt the render and redirect to another page, you can invoke the `redirect` function. This wraps [the `redirect` function from Next.js](https://nextjs.org/docs/app/api-reference/functions/redirect) and localizes the pathname as necessary.
295+
If you want to interrupt the render and redirect to another page, you can invoke the `redirect` function. This wraps [`redirect` from Next.js](https://nextjs.org/docs/app/api-reference/functions/redirect) and localizes the pathname as necessary.
296296

297-
Note that a `locale` prop is always required, even if you're just passing [the current locale](/docs/usage/configuration#locale).
297+
Note that a `locale` prop is always required, even if you're just passing [the current locale](/docs/usage/configuration#use-locale).
298298

299299
```tsx
300300
import {redirect} from '@/i18n/navigation';
@@ -306,7 +306,7 @@ redirect({href: '/login', locale: 'en'});
306306
redirect({href: '/users', query: {sortBy: 'name'}, locale: 'en'});
307307
```
308308

309-
Depending on if you're using the pathnames setting, dynamic params can either be passed as:
309+
Depending on if you're using the [`pathnames`](/docs/routing#pathnames) setting, dynamic params can either be passed as:
310310

311311
```tsx
312312
// 1. A final string (when not using `pathnames`)
@@ -322,6 +322,14 @@ redirect({
322322
});
323323
```
324324

325+
When using a [`localePrefix`](/docs/routing#localeprefix) setting other than `always`, you can enforce a locale prefix by setting the `forcePrefix` option to `true`. This is useful when changing the user's locale and you need to update the [locale cookie](/docs/routing#locale-cookie) first:
326+
327+
```tsx
328+
// Will initially redirect to `/en/about` to update the locale
329+
// cookie, regardless of your `localePrefix` setting
330+
redirect({href: '/about', locale: 'en', forcePrefix: true});
331+
```
332+
325333
<Callout>
326334
[`permanentRedirect`](https://nextjs.org/docs/app/api-reference/functions/permanentRedirect)
327335
is supported too.

packages/next-intl/.size-limit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const config: SizeLimitConfig = [
2121
name: "import {createNavigation} from 'next-intl/navigation' (react-client)",
2222
path: 'dist/esm/production/navigation.react-client.js',
2323
import: '{createNavigation}',
24-
limit: '2.305 KB'
24+
limit: '2.308 KB'
2525
},
2626
{
2727
name: "import {createNavigation} from 'next-intl/navigation' (react-server)",

packages/next-intl/src/navigation/createNavigation.test.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,12 @@ describe.each([
759759
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
760760
() => getPathname({href: '/about'});
761761
});
762+
763+
it('can force a prefix for the default locale', () => {
764+
expect(
765+
getPathname({locale: 'en', href: '/about', forcePrefix: true})
766+
).toBe('/en/about');
767+
});
762768
});
763769

764770
describe.each([
@@ -781,10 +787,17 @@ describe.each([
781787
);
782788
expect(nextRedirectFn).toHaveBeenLastCalledWith('/', RedirectType.push);
783789
});
790+
791+
it('can force a prefix for the default locale', () => {
792+
runInRender(() =>
793+
redirectFn({href: '/', locale: 'en', forcePrefix: true})
794+
);
795+
expect(nextRedirectFn).toHaveBeenLastCalledWith('/en');
796+
});
784797
});
785798
});
786799

787-
describe('localePrefix: "always", with `prefixes`', () => {
800+
describe("localePrefix: 'always', with `prefixes`", () => {
788801
const {Link, getPathname, permanentRedirect, redirect} = createNavigation({
789802
locales,
790803
defaultLocale,
@@ -1088,6 +1101,12 @@ describe.each([
10881101
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
10891102
() => getPathname({href: '/about'});
10901103
});
1104+
1105+
it('can force a prefix', () => {
1106+
expect(
1107+
getPathname({locale: 'en', href: '/about', forcePrefix: true})
1108+
).toBe('/en/about');
1109+
});
10911110
});
10921111

10931112
describe.each([
@@ -1105,6 +1124,13 @@ describe.each([
11051124
);
11061125
expect(nextRedirectFn).toHaveBeenLastCalledWith('/', RedirectType.push);
11071126
});
1127+
1128+
it('can force a prefix', () => {
1129+
runInRender(() =>
1130+
redirectFn({href: '/', locale: 'en', forcePrefix: true})
1131+
);
1132+
expect(nextRedirectFn).toHaveBeenLastCalledWith('/en');
1133+
});
11081134
});
11091135
});
11101136

packages/next-intl/src/navigation/shared/createSharedNavigationFns.tsx

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,13 @@ export default function createSharedNavigationFns<
101101
: localePromiseOrValue;
102102

103103
const finalPathname = isLocalizable
104-
? getPathname(
105-
{
106-
locale: locale || curLocale,
107-
// @ts-expect-error -- This is ok
108-
href: pathnames == null ? pathname : {pathname, params}
109-
},
110-
locale != null || undefined
111-
)
104+
? getPathname({
105+
locale: locale || curLocale,
106+
// @ts-expect-error -- This is ok
107+
href: pathnames == null ? pathname : {pathname, params},
108+
// Always include a prefix when changing locales
109+
forcePrefix: locale != null || undefined
110+
})
112111
: pathname;
113112

114113
return (
@@ -128,18 +127,17 @@ export default function createSharedNavigationFns<
128127
}
129128
const LinkWithRef = forwardRef(Link);
130129

131-
function getPathname(
132-
args: {
133-
/** @see https://next-intl.dev/docs/routing/navigation#getpathname */
134-
href: [AppPathnames] extends [never]
135-
? string | {pathname: string; query?: QueryParams}
136-
: HrefOrHrefWithParams<keyof AppPathnames>;
137-
locale: Locale;
138-
},
139-
/** @private Removed in types returned below */
140-
_forcePrefix?: boolean
141-
) {
142-
const {href, locale} = args;
130+
function getPathname(args: {
131+
/** @see https://next-intl.dev/docs/routing/navigation#getpathname */
132+
href: [AppPathnames] extends [never]
133+
? string | {pathname: string; query?: QueryParams}
134+
: HrefOrHrefWithParams<keyof AppPathnames>;
135+
/** The locale to compute the pathname for. */
136+
locale: Locale;
137+
/** Will prepend the pathname with the locale prefix, regardless of your `localePrefix` setting. This can be helpful to update a locale cookie when changing locales. */
138+
forcePrefix?: boolean;
139+
}) {
140+
const {forcePrefix, href, locale} = args;
143141

144142
let pathname: string;
145143
if (pathnames == null) {
@@ -161,15 +159,15 @@ export default function createSharedNavigationFns<
161159
});
162160
}
163161

164-
return applyPathnamePrefix(pathname, locale, config, _forcePrefix);
162+
return applyPathnamePrefix(pathname, locale, config, forcePrefix);
165163
}
166164

167165
function getRedirectFn(
168166
fn: typeof nextRedirect | typeof nextPermanentRedirect
169167
) {
170168
/** @see https://next-intl.dev/docs/routing/navigation#redirect */
171169
return function redirectFn(
172-
args: Omit<Parameters<typeof getPathname>[0], 'domain'>,
170+
args: Parameters<typeof getPathname>[0],
173171
...rest: ParametersExceptFirst<typeof nextRedirect>
174172
) {
175173
return fn(getPathname(args), ...rest);
@@ -184,10 +182,6 @@ export default function createSharedNavigationFns<
184182
Link: LinkWithRef,
185183
redirect,
186184
permanentRedirect,
187-
188-
// Remove `_forcePrefix` from public API
189-
getPathname: getPathname as (
190-
args: Parameters<typeof getPathname>[0]
191-
) => string
185+
getPathname
192186
};
193187
}

0 commit comments

Comments
 (0)