Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Accept locale with useRouter APIs #409

Merged
merged 2 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/pages/docs/environments/error-files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ When an `error` file is defined, Next.js creates [an error boundary within your

Since the `error` file must be defined as a Client Component, you have to use [`NextIntlClientProvider`](/docs/configuration#client-server-components) to provide messages in case the `error` file renders.

If you've [set up `next-intl` to be used in Client Components](http://localhost:3001/docs/getting-started/app-router-client-components), this is already the case and there's no additional setup needed. If you're using [the Server Components beta](/docs/getting-started/app-router-server-components) though, you have to provide the relevant messages in the wrapping layout.
If you've [set up `next-intl` to be used in Client Components](/docs/getting-started/app-router-client-components), this is already the case and there's no additional setup needed. If you're using [the Server Components beta](/docs/getting-started/app-router-server-components) though, you have to provide the relevant messages in the wrapping layout.

<figure>

Expand Down
35 changes: 28 additions & 7 deletions docs/pages/docs/routing/navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import Callout from 'components/Callout';

`next-intl` provides drop-in replacements for common Next.js navigation APIs that automatically handle the user locale behind the scenes.

### `Link`
## `Link`

This component wraps `next/link` and automatically prefixes the `href` with the current locale as necessary. If the default locale is matched, the `href` remains unchanged and no prefix is added.
This component wraps [`next/link`](https://nextjs.org/docs/app/api-reference/components/link) and automatically prefixes the `href` with the current locale as necessary. If the default locale is matched, the `href` remains unchanged and no prefix is added.

```tsx
import Link from 'next-intl/link';
Expand All @@ -18,9 +18,9 @@ import Link from 'next-intl/link';
<Link href="/" locale="de">Switch to German</Link>
```

### `useRouter`
## `useRouter`

If you need to navigate programmatically, e.g. in response to a form submission, `next-intl` provides a convience API that wraps `useRouter` from Next.js and automatically applies the locale of the user.
If you need to navigate programmatically, e.g. in an event handler, `next-intl` provides a convience API that wraps [`useRouter` from Next.js](https://nextjs.org/docs/app/api-reference/functions/use-router) and automatically applies the locale of the user.

```tsx
'use client';
Expand All @@ -31,9 +31,30 @@ const router = useRouter();

// When the user is on `/en`, the router will navigate to `/en/about`
router.push('/about');

// You can override the `locale` to switch to another language
router.push('/about', {locale: 'de'});
```

<details>
<summary>How can I change the locale for the current page?</summary>

By combining [`usePathname`](#usepathname) with [`useRouter`](#userouter), you can change the locale for the current page programmatically.

```tsx
'use client';

import {usePathname, useRouter} from 'next-intl/client';

const pathname = usePathname();
const router = useRouter();

router.replace(pathname, {locale: 'de'});
```

### `usePathname`
</details>

## `usePathname`

To retrieve the pathname without a potential locale prefix, you can call `usePathname`.

Expand All @@ -46,14 +67,14 @@ import {usePathname} from 'next-intl/client';
const pathname = usePathname();
```

### `redirect`
## `redirect`

<Callout type="warning">
This API is only available in [the Server Components
beta](/docs/getting-started/app-router-server-components).
</Callout>

If you want to interrupt the render of a Server Component and redirect to another page, you can invoke the `redirect` function from `next-intl/server`. This wraps [the `redirect` function from Next.js](https://nextjs.org/docs/app/api-reference/functions/redirect) and automatically applies the current locale.
If you want to interrupt the render and redirect to another page, you can invoke the `redirect` function from `next-intl/server`. This wraps [the `redirect` function from Next.js](https://nextjs.org/docs/app/api-reference/functions/redirect) and automatically applies the current locale.

```tsx {1, 8}
import {redirect} from 'next-intl/server';
Expand Down
6 changes: 3 additions & 3 deletions examples/example-next-13/src/components/LocaleSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
'use client';

import clsx from 'clsx';
import {useRouter} from 'next/navigation';
import {useLocale, useTranslations} from 'next-intl';
import {usePathname} from 'next-intl/client';
import {usePathname, useRouter} from 'next-intl/client';
import {ChangeEvent, useTransition} from 'react';

export default function LocaleSwitcher() {
Expand All @@ -14,8 +13,9 @@ export default function LocaleSwitcher() {
const pathname = usePathname();

function onSelectChange(event: ChangeEvent<HTMLSelectElement>) {
const nextLocale = event.target.value;
startTransition(() => {
router.replace(`/${event.target.value}${pathname}`);
router.replace(pathname, {locale: nextLocale});
});
}

Expand Down
64 changes: 56 additions & 8 deletions packages/next-intl/src/client/useRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import {useMemo} from 'react';
import {localizeHref} from '../shared/utils';
import useClientLocale from './useClientLocale';

type IntlNavigateOptions = {
locale?: string;
};

/**
* Returns a wrapped instance of `useRouter` from `next/navigation` that
* will automatically localize the `href` parameters it receives.
Expand All @@ -17,27 +21,71 @@ import useClientLocale from './useClientLocale';
*
* // When the user is on `/en`, the router will navigate to `/en/about`
* router.push('/about');
*
* // Optionally, you can switch the locale by passing the second argument
* router.push('/about', {locale: 'de'});
* ```
*/
export default function useRouter() {
const router = useNextRouter();
const locale = useClientLocale();

return useMemo(() => {
function localize(href: string) {
return localizeHref(href, locale, locale, window.location.pathname);
function localize(href: string, nextLocale?: string) {
return localizeHref(
href,
nextLocale || locale,
locale,
window.location.pathname
);
}

return {
...router,
push(...[href, ...args]: Parameters<typeof router.push>) {
return router.push(localize(href), ...args);
push(
href: string,
options?: Parameters<typeof router.push>[1] & IntlNavigateOptions
) {
const {locale: nextLocale, ...rest} = options || {};
const args: [
href: string,
options?: Parameters<typeof router.push>[1]
] = [localize(href, nextLocale)];
if (Object.keys(rest).length > 0) {
args.push(rest);
}
return router.push(...args);
},
replace(...[href, ...args]: Parameters<typeof router.replace>) {
return router.replace(localize(href), ...args);

replace(
href: string,
options?: Parameters<typeof router.replace>[1] & IntlNavigateOptions
) {
const {locale: nextLocale, ...rest} = options || {};
const args: [
href: string,
options?: Parameters<typeof router.replace>[1]
] = [localize(href, nextLocale)];
if (Object.keys(rest).length > 0) {
args.push(rest);
}
return router.replace(...args);
},
prefetch(...[href, ...args]: Parameters<typeof router.prefetch>) {
return router.prefetch(localize(href), ...args);

prefetch(
href: string,
options?: Parameters<typeof router.prefetch>[1] & IntlNavigateOptions
) {
const {locale: nextLocale, ...rest} = options || {};
const args: [
href: string,
options?: Parameters<typeof router.prefetch>[1]
] = [localize(href, nextLocale)];
if (Object.keys(rest).length > 0) {
// @ts-expect-error TypeScript thinks `rest` can be an empty object
args.push(rest);
}
return router.prefetch(...args);
}
};
}, [locale, router]);
Expand Down
22 changes: 21 additions & 1 deletion packages/next-intl/test/client/useRouter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {render} from '@testing-library/react';
import {PrefetchKind} from 'next/dist/client/components/router-reducer/router-reducer-types';
import {AppRouterInstance} from 'next/dist/shared/lib/app-router-context';
import {useRouter as useNextRouter} from 'next/navigation';
import React, {useEffect} from 'react';
Expand All @@ -20,7 +21,7 @@ vi.mock('next/navigation', () => {
};
});

function callRouter(cb: (router: AppRouterInstance) => void) {
function callRouter(cb: (router: ReturnType<typeof useRouter>) => void) {
function Component() {
const router = useRouter();
useEffect(() => {
Expand Down Expand Up @@ -67,6 +68,25 @@ describe('unprefixed routing', () => {
callRouter((router) => router.push('about'));
expect(useNextRouter().push).toHaveBeenCalledWith('about');
});

it('can change the locale with `push`', () => {
callRouter((router) => router.push('/about', {locale: 'de'}));
expect(useNextRouter().push).toHaveBeenCalledWith('/de/about');
});

it('can change the locale with `replace`', () => {
callRouter((router) => router.replace('/about', {locale: 'es'}));
expect(useNextRouter().replace).toHaveBeenCalledWith('/es/about');
});

it('can prefetch a new locale', () => {
callRouter((router) =>
router.prefetch('/about', {locale: 'es', kind: PrefetchKind.AUTO})
);
expect(useNextRouter().prefetch).toHaveBeenCalledWith('/es/about', {
kind: PrefetchKind.AUTO
});
});
});

describe('prefixed routing', () => {
Expand Down