Skip to content

Commit

Permalink
fix: Allow usage of next-intl/link and usePathname outside of Nex…
Browse files Browse the repository at this point in the history
…t.js (#338)

Fixes #337
  • Loading branch information
amannn committed Jun 21, 2023
1 parent 6737aff commit 6e1a56c
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 19 deletions.
7 changes: 5 additions & 2 deletions packages/next-intl/src/client/useClientLocale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import {LOCALE_SEGMENT_NAME} from '../shared/constants';
export default function useClientLocale(): string {
let locale;

const params = useParams();
if (params[LOCALE_SEGMENT_NAME]) {
// The types aren't entirely correct here. Outside of Next.js
// `useParams` can be called, but the return type is `null`.
const params = useParams() as ReturnType<typeof useParams> | null;

if (params?.[LOCALE_SEGMENT_NAME]) {
locale = params[LOCALE_SEGMENT_NAME];
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks -- Reading from context conditionally is fine
Expand Down
9 changes: 8 additions & 1 deletion packages/next-intl/src/client/usePathname.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ import useClientLocale from './useClientLocale';
* ```
*/
export default function usePathname(): string {
const pathname = useNextPathname();
// The types aren't entirely correct here. Outside of Next.js
// `useParams` can be called, but the return type is `null`.
const pathname = useNextPathname() as ReturnType<
typeof useNextPathname
> | null;

const locale = useClientLocale();

return useMemo(() => {
if (!pathname) return pathname as ReturnType<typeof useNextPathname>;

const isPathnamePrefixed = hasPathnamePrefixed(locale, pathname);
const unlocalizedPathname = isPathnamePrefixed
? unlocalizePathname(pathname, locale)
Expand Down
7 changes: 6 additions & 1 deletion packages/next-intl/src/shared/BaseLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ type Props = Omit<ComponentProps<typeof NextLink>, 'locale'> & {
};

function BaseLink({href, locale, prefetch, ...rest}: Props, ref: Props['ref']) {
const pathname = usePathname();
// The types aren't entirely correct here. Outside of Next.js
// `useParams` can be called, but the return type is `null`.
const pathname = usePathname() as ReturnType<typeof usePathname> | null;

const defaultLocale = useClientLocale();
const isChangingLocale = locale !== defaultLocale;

Expand All @@ -30,6 +33,8 @@ function BaseLink({href, locale, prefetch, ...rest}: Props, ref: Props['ref']) {
);

useEffect(() => {
if (!pathname) return;

setLocalizedHref(
localizeHref(href, locale, defaultLocale, pathname ?? undefined)
);
Expand Down
39 changes: 29 additions & 10 deletions packages/next-intl/test/client/usePathname.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {render, screen} from '@testing-library/react';
import {usePathname as useNextPathname, useParams} from 'next/navigation';
import React from 'react';
import {NextIntlClientProvider} from '../../src';
import {usePathname} from '../../src/client';

jest.mock('next/navigation');
Expand All @@ -10,38 +11,56 @@ function mockPathname(pathname: string) {
jest.mocked(useParams).mockImplementation(() => ({locale: 'en'}));
}

function renderComponent() {
function Component() {
return <>{usePathname()}</>;
}

render(<Component />);
function Component() {
return <>{usePathname()}</>;
}

describe('unprefixed routing', () => {
it('returns an unprefixed pathname', () => {
mockPathname('/');
renderComponent();
render(<Component />);
screen.getByText('/');
});

it('returns an unprefixed pathname at sub paths', () => {
mockPathname('/about');
renderComponent();
render(<Component />);
screen.getByText('/about');
});
});

describe('prefixed routing', () => {
it('returns an unprefixed pathname', () => {
mockPathname('/en');
renderComponent();
render(<Component />);
screen.getByText('/');
});

it('returns an unprefixed pathname at sub paths', () => {
mockPathname('/en/about');
renderComponent();
render(<Component />);
screen.getByText('/about');
});
});

describe('usage outside of Next.js', () => {
beforeEach(() => {
jest.mocked(useNextPathname).mockImplementation((() => null) as any);
jest.mocked(useParams).mockImplementation((() => null) as any);
});

it('returns `null` when used within a provider', () => {
const {container} = render(
<NextIntlClientProvider locale="en">
<Component />
</NextIntlClientProvider>
);
expect(container.innerHTML).toBe('');
});

it('throws without a provider', () => {
expect(() => render(<Component />)).toThrow(
'No intl context found. Have you configured the provider?'
);
});
});
33 changes: 28 additions & 5 deletions packages/next-intl/test/link/Link.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import {render, screen} from '@testing-library/react';
import {usePathname} from 'next/navigation';
import {usePathname, useParams} from 'next/navigation';
import React from 'react';
import {NextIntlClientProvider} from '../../src';
import Link from '../../src/link';

jest.mock('next/navigation', () => ({
useParams: jest.fn(() => ({locale: 'en'})),
usePathname: jest.fn(() => '/')
}));
jest.mock('next/navigation');

describe('unprefixed routing', () => {
beforeEach(() => {
jest.mocked(usePathname).mockImplementation(() => '/');
jest.mocked(useParams).mockImplementation(() => ({locale: 'en'}));
});

it('renders an href without a locale if the locale matches', () => {
Expand Down Expand Up @@ -94,6 +93,7 @@ describe('unprefixed routing', () => {
describe('prefixed routing', () => {
beforeEach(() => {
jest.mocked(usePathname).mockImplementation(() => '/en');
jest.mocked(useParams).mockImplementation(() => ({locale: 'en'}));
});

it('renders an href with a locale if the locale matches', () => {
Expand Down Expand Up @@ -156,3 +156,26 @@ describe('prefixed routing', () => {
);
});
});

describe('usage outside of Next.js', () => {
beforeEach(() => {
jest.mocked(useParams).mockImplementation((() => null) as any);
});

it('works with a provider', () => {
render(
<NextIntlClientProvider locale="en">
<Link href="/test">Test</Link>
</NextIntlClientProvider>
);
expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe(
'/en/test'
);
});

it('throws without a provider', () => {
expect(() => render(<Link href="/test">Test</Link>)).toThrow(
'No intl context found. Have you configured the provider?'
);
});
});

1 comment on commit 6e1a56c

@vercel
Copy link

@vercel vercel bot commented on 6e1a56c Jun 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-intl-docs – ./docs

next-intl-docs.vercel.app
next-intl-docs-next-intl.vercel.app
next-intl-docs-git-main-next-intl.vercel.app

Please sign in to comment.