Skip to content

Commit

Permalink
domain progress with hydrating link
Browse files Browse the repository at this point in the history
  • Loading branch information
amannn committed Sep 20, 2024
1 parent ba7acf4 commit 604d56c
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 16 deletions.
36 changes: 35 additions & 1 deletion packages/next-intl/src/navigation/createNavigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,15 @@ function mockCurrentLocale(locale: string) {
}));
}

function mockLocation(location: Partial<typeof window.location>) {
delete (global.window as any).location;
global.window ??= Object.create(window);
(global.window as any).location = location;
}

beforeEach(() => {
mockCurrentLocale('en');
mockLocation({host: 'localhost:3000'});
});

const locales = ['en', 'de', 'ja'] as const;
Expand Down Expand Up @@ -133,7 +140,7 @@ describe.each([
expect(markup).toContain('hrefLang="de"');
});

it('renders an object href', () => {
it('renders an object href with an external host', () => {
render(
<Link
href={{
Expand Down Expand Up @@ -699,6 +706,33 @@ describe.each([
expect(markup).toContain('href="/en/about"');
});

it('does not render a prefix eventually on the client side for the default locale of the given domain', () => {
mockLocation({host: 'example.com'});
render(<Link href="/about">About</Link>);
expect(
screen.getByRole('link', {name: 'About'}).getAttribute('href')
).toBe('/about');
});

it('renders a prefix when currently on a secondary locale', () => {
mockLocation({host: 'example.de'});
mockCurrentLocale('en');
render(<Link href="/about">About</Link>);
expect(
screen.getByRole('link', {name: 'About'}).getAttribute('href')
).toBe('/en/about');
});

it('renders a prefix when currently on a secondary locale and linking to the default locale', () => {
mockLocation({host: 'example.de'});
mockCurrentLocale('en');
const markup = renderToString(
<Link href="/about" locale="de">
About
</Link>
);
expect(markup).toContain('href="/de/about"');
});
});

describe('getPathname', () => {
Expand Down
41 changes: 34 additions & 7 deletions packages/next-intl/src/navigation/shared/BaseLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,42 @@

import NextLink from 'next/link';
import {usePathname} from 'next/navigation';
import React, {ComponentProps, MouseEvent} from 'react';
import React, {ComponentProps, MouseEvent, useEffect, useState} from 'react';
import useLocale from '../../react-client/useLocale';
import syncLocaleCookie from './syncLocaleCookie';

type Props = Omit<ComponentProps<typeof NextLink>, 'locale'> & {
locale?: string;
nodeRef?: ComponentProps<typeof NextLink>['ref'];
unprefixConfig?: {
domains: {[defaultLocale: string]: string};
pathname: string;
};
};

function BaseLink({href, locale, nodeRef, onClick, prefetch, ...rest}: Props) {
export default function BaseLink({
href,
locale,
nodeRef,
onClick,
prefetch,
unprefixConfig,
...rest
}: Props) {
const curLocale = useLocale();
const isChangingLocale = locale !== curLocale;
const linkLocale = locale || curLocale;

const host = useHost();
const finalHref =
unprefixConfig && unprefixConfig.domains[linkLocale] === host
? unprefixConfig.pathname
: href;

// 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 curLocale = useLocale();
const isChangingLocale = locale !== curLocale;

function onLinkClick(event: MouseEvent<HTMLAnchorElement>) {
syncLocaleCookie(pathname, curLocale, locale);
if (onClick) onClick(event);
Expand All @@ -36,7 +55,7 @@ function BaseLink({href, locale, nodeRef, onClick, prefetch, ...rest}: Props) {
return (
<NextLink
ref={nodeRef}
href={href}
href={finalHref}
hrefLang={isChangingLocale ? locale : undefined}
onClick={onLinkClick}
prefetch={prefetch}
Expand All @@ -45,4 +64,12 @@ function BaseLink({href, locale, nodeRef, onClick, prefetch, ...rest}: Props) {
);
}

export default BaseLink;
function useHost() {
const [host, setHost] = useState<string>();

useEffect(() => {
setHost(window.location.host);
}, []);

return host;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
RoutingConfigLocalizedNavigation,
RoutingConfigSharedNavigation
} from '../../routing/config';
import {Locales, Pathnames} from '../../routing/types';
import {DomainConfig, Locales, Pathnames} from '../../routing/types';
import {ParametersExceptFirst} from '../../shared/types';
import {isLocalizableHref} from '../../shared/utils';
import BaseLink from './BaseLink';
Expand Down Expand Up @@ -60,7 +60,8 @@ export default function createSharedNavigationFns<
// that the user might get redirected again if the middleware detects that the
// prefix is not needed.
const forcePrefixSsr =
config.localePrefix.mode === 'as-needed' && 'domains' in config;
(config.localePrefix.mode === 'as-needed' && 'domains' in config) ||
undefined;

type LinkProps<Pathname extends keyof AppPathnames = never> = Omit<
ComponentProps<typeof BaseLink>,
Expand All @@ -85,17 +86,18 @@ export default function createSharedNavigationFns<
pathname = href;
}

const curLocale = getLocale();

// @ts-expect-error -- This is ok
const finalPathname = isLocalizableHref(href)
const isLocalizable = isLocalizableHref(href);

const curLocale = getLocale();
const finalPathname = isLocalizable
? getPathname(
{
locale: locale || curLocale,
// @ts-expect-error -- This is ok
href: pathnames == null ? pathname : {pathname, params}
},
locale != null || forcePrefixSsr
locale != null || forcePrefixSsr || undefined
)
: pathname;

Expand All @@ -107,6 +109,33 @@ export default function createSharedNavigationFns<
pathname: finalPathname
}}
locale={locale}
// Provide the minimal relevant information to the client side in order
// to potentially remove the prefix in case of the `forcePrefixSsr` case
unprefixConfig={
forcePrefixSsr && isLocalizable
? {
domains: (config as any).domains.reduce(
(
acc: Record<Locale, string>,
domain: DomainConfig<AppLocales>
) => {
// @ts-expect-error -- This is ok
acc[domain.defaultLocale] = domain.domain;
return acc;
},
{}
),
pathname: getPathname(
{
locale: curLocale,
// @ts-expect-error -- This is ok
href: pathnames == null ? pathname : {pathname, params}
},
false
)
}
: undefined
}
{...rest}
/>
);
Expand Down
6 changes: 4 additions & 2 deletions packages/next-intl/src/navigation/shared/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,10 @@ export function applyPathnamePrefix<AppLocales extends Locales>(
const {mode} = routing.localePrefix;

let shouldPrefix;
if (isLocalizableHref(pathname)) {
if (force || mode === 'always') {
if (force !== undefined) {
shouldPrefix = force;
} else if (isLocalizableHref(pathname)) {
if (mode === 'always') {
shouldPrefix = true;
} else if (mode === 'as-needed') {
let {defaultLocale} = routing;
Expand Down

0 comments on commit 604d56c

Please sign in to comment.