Skip to content

Commit c82d0af

Browse files
authored
fix: Prefix pathnames when switching with useRouter to another locale (#2021)
Fixes #2020
1 parent 07149a1 commit c82d0af

6 files changed

Lines changed: 47 additions & 20 deletions

File tree

examples/example-app-router-playground/src/app/[locale]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import AsyncComponentWithNamespaceAndLocale from '../../components/AsyncComponen
99
import AsyncComponentWithoutNamespace from '../../components/AsyncComponentWithoutNamespace';
1010
import AsyncComponentWithoutNamespaceAndLocale from '../../components/AsyncComponentWithoutNamespaceAndLocale';
1111
import ClientLink from '../../components/ClientLink';
12-
import ClientRouterWithoutProvider from '../../components/ClientRouterWithoutProvider';
12+
import ClientRouter from '../../components/ClientRouter';
1313
import CoreLibrary from '../../components/CoreLibrary';
1414
import LocaleSwitcher from '../../components/LocaleSwitcher';
1515
import PageLayout from '../../components/PageLayout';
@@ -50,7 +50,7 @@ export default function Index(props: Props) {
5050
<MessagesAsPropsCounter />
5151
<MessagesOnClientCounter />
5252
<CoreLibrary />
53-
<ClientRouterWithoutProvider />
53+
<ClientRouter />
5454
<div>
5555
<Link href={{pathname: '/', query: {test: true}}}>
5656
Go to home with query param

examples/example-app-router-playground/src/components/ClientRouterWithoutProvider.tsx renamed to examples/example-app-router-playground/src/components/ClientRouter.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,15 @@
22

33
import {useRouter} from '@/i18n/navigation';
44

5-
export default function ClientRouterWithoutProvider() {
5+
export default function ClientRouter() {
66
const router = useRouter();
77

88
function onClick() {
99
router.push('/nested');
1010
}
1111

1212
return (
13-
<button
14-
data-testid="ClientRouterWithoutProvider-link"
15-
onClick={onClick}
16-
type="button"
17-
>
13+
<button data-testid="ClientRouter-link" onClick={onClick} type="button">
1814
Go to nested page
1915
</button>
2016
);

examples/example-app-router-playground/tests/main.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,15 +469,15 @@ it('can navigate between sibling pages that share a parent layout', async ({
469469

470470
it('prefixes routes as necessary with the router', async ({page}) => {
471471
await page.goto('/');
472-
page.getByTestId('ClientRouterWithoutProvider-link').click();
472+
page.getByTestId('ClientRouter-link').click();
473473
await expect(page).toHaveURL('/nested');
474474

475475
await page.goto('/en');
476-
page.getByTestId('ClientRouterWithoutProvider-link').click();
476+
page.getByTestId('ClientRouter-link').click();
477477
await expect(page).toHaveURL('/nested');
478478

479479
await page.goto('/de');
480-
page.getByTestId('ClientRouterWithoutProvider-link').click();
480+
page.getByTestId('ClientRouter-link').click();
481481
await expect(page).toHaveURL('/de/verschachtelt');
482482
});
483483

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ describe("localePrefix: 'always', custom `prefixes`", () => {
480480
});
481481

482482
describe("localePrefix: 'as-needed'", () => {
483-
const {usePathname, useRouter} = createNavigation({
483+
const {Link, usePathname, useRouter} = createNavigation({
484484
locales,
485485
defaultLocale,
486486
localePrefix: 'as-needed'
@@ -493,6 +493,20 @@ describe("localePrefix: 'as-needed'", () => {
493493
render(<Component />);
494494
}
495495

496+
describe('Link', () => {
497+
it('sets a cookie when switching to the default locale', () => {
498+
mockCurrentLocale('de');
499+
global.document.cookie = 'NEXT_LOCALE=de';
500+
render(
501+
<Link href="/" locale="en">
502+
Test
503+
</Link>
504+
);
505+
fireEvent.click(screen.getByRole('link', {name: 'Test'}));
506+
expect(document.cookie).toContain('NEXT_LOCALE=en');
507+
});
508+
});
509+
496510
describe('useRouter', () => {
497511
const invokeRouter = getInvokeRouter(useRouter);
498512

@@ -514,9 +528,16 @@ describe("localePrefix: 'as-needed'", () => {
514528
expect(useNextRouter()[method]).toHaveBeenCalledWith('/de/about');
515529
});
516530

517-
it('does not prefix the default locale when being switched to', () => {
531+
it('does prefix the default locale when being switched to', () => {
518532
invokeRouter((router) => router[method]('/about', {locale: 'en'}));
519-
expect(useNextRouter()[method]).toHaveBeenCalledWith('/about');
533+
expect(useNextRouter()[method]).toHaveBeenCalledWith('/en/about');
534+
});
535+
536+
it('sets a cookie when switching to the default locale', () => {
537+
global.document.cookie = 'NEXT_LOCALE=de';
538+
mockCurrentLocale('de');
539+
invokeRouter((router) => router[method]('/about', {locale: 'en'}));
540+
expect(document.cookie).toContain('NEXT_LOCALE=en');
520541
});
521542
});
522543

@@ -606,7 +627,7 @@ describe("localePrefix: 'as-needed', with `basePath` and `domains`", () => {
606627
it('can compute the correct pathname when on a secondary locale and navigating to the default locale', () => {
607628
mockCurrentLocale('ja');
608629
invokeRouter((router) => router.push('/test', {locale: 'en'}));
609-
expect(useNextRouter().push).toHaveBeenCalledWith('/test');
630+
expect(useNextRouter().push).toHaveBeenCalledWith('/en/test');
610631
});
611632
});
612633
});
@@ -737,9 +758,9 @@ describe("localePrefix: 'never'", () => {
737758
expect(useNextRouter()[method]).toHaveBeenCalledWith('/about');
738759
});
739760

740-
it('does not prefix a secondary locale', () => {
761+
it('does prefix a pathname when switching to another locale', () => {
741762
invokeRouter((router) => router[method]('/about', {locale: 'de'}));
742-
expect(useNextRouter()[method]).toHaveBeenCalledWith('/about');
763+
expect(useNextRouter()[method]).toHaveBeenCalledWith('/de/about');
743764
});
744765
});
745766

@@ -771,9 +792,9 @@ describe("localePrefix: 'never'", () => {
771792
expect(useNextRouter().prefetch).toHaveBeenCalledWith('/about');
772793
});
773794

774-
it('does not prefix a secondary locale', () => {
795+
it('does prefix a pathname when switching to another locale', () => {
775796
invokeRouter((router) => router.prefetch('/about', {locale: 'de'}));
776-
expect(useNextRouter().prefetch).toHaveBeenCalledWith('/about');
797+
expect(useNextRouter().prefetch).toHaveBeenCalledWith('/de/about');
777798
});
778799
});
779800
});

packages/next-intl/src/navigation/react-client/createNavigation.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,14 @@ export default function createNavigation<
8787

8888
const pathname = getPathname({
8989
href,
90-
locale: nextLocale || curLocale
90+
locale: nextLocale || curLocale,
91+
// Always include a prefix when changing locales. Theoretically,
92+
// this is only necessary for the case described in #2020. However,
93+
// the full detection is rather expensive, and this behavior is
94+
// consistent with the `Link` component. The downside is an
95+
// additional redirect for users in other situations. Locale
96+
// changes should be rare though, so this might be fine.
97+
forcePrefix: nextLocale != null || undefined
9198
});
9299

93100
const args: [href: string, options?: Options] = [pathname];

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ function BaseLink(
3333
const pathname = usePathname() as ReturnType<typeof usePathname> | null;
3434

3535
function onLinkClick(event: MouseEvent<HTMLAnchorElement>) {
36+
// Even though we force a prefix when changing locales,
37+
// this could be a cache hit of the client-side router,
38+
// therefore we sync the cookie to ensure it's up to date.
3639
syncLocaleCookie(localeCookie, pathname, curLocale, locale);
3740
if (onClick) onClick(event);
3841
}

0 commit comments

Comments
 (0)