From 604d56c50cbfa8b7f75048d5b70643badd1568e7 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 20 Sep 2024 15:01:44 +0200 Subject: [PATCH] domain progress with hydrating link --- .../src/navigation/createNavigation.test.tsx | 36 +++++++++++++++- .../src/navigation/shared/BaseLink.tsx | 41 +++++++++++++++---- .../shared/createSharedNavigationFns.tsx | 41 ++++++++++++++++--- .../next-intl/src/navigation/shared/utils.tsx | 6 ++- 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/packages/next-intl/src/navigation/createNavigation.test.tsx b/packages/next-intl/src/navigation/createNavigation.test.tsx index cc49ed007..32733ba2e 100644 --- a/packages/next-intl/src/navigation/createNavigation.test.tsx +++ b/packages/next-intl/src/navigation/createNavigation.test.tsx @@ -40,8 +40,15 @@ function mockCurrentLocale(locale: string) { })); } +function mockLocation(location: Partial) { + 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; @@ -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( { + mockLocation({host: 'example.com'}); + render(About); + 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(About); + 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( + + About + + ); + expect(markup).toContain('href="/de/about"'); + }); }); describe('getPathname', () => { diff --git a/packages/next-intl/src/navigation/shared/BaseLink.tsx b/packages/next-intl/src/navigation/shared/BaseLink.tsx index 221264111..d415e7d15 100644 --- a/packages/next-intl/src/navigation/shared/BaseLink.tsx +++ b/packages/next-intl/src/navigation/shared/BaseLink.tsx @@ -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, 'locale'> & { locale?: string; nodeRef?: ComponentProps['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 | null; - const curLocale = useLocale(); - const isChangingLocale = locale !== curLocale; - function onLinkClick(event: MouseEvent) { syncLocaleCookie(pathname, curLocale, locale); if (onClick) onClick(event); @@ -36,7 +55,7 @@ function BaseLink({href, locale, nodeRef, onClick, prefetch, ...rest}: Props) { return ( (); + + useEffect(() => { + setHost(window.location.host); + }, []); + + return host; +} diff --git a/packages/next-intl/src/navigation/shared/createSharedNavigationFns.tsx b/packages/next-intl/src/navigation/shared/createSharedNavigationFns.tsx index 66bb99202..a6661edec 100644 --- a/packages/next-intl/src/navigation/shared/createSharedNavigationFns.tsx +++ b/packages/next-intl/src/navigation/shared/createSharedNavigationFns.tsx @@ -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'; @@ -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 = Omit< ComponentProps, @@ -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; @@ -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, + domain: DomainConfig + ) => { + // @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} /> ); diff --git a/packages/next-intl/src/navigation/shared/utils.tsx b/packages/next-intl/src/navigation/shared/utils.tsx index ae61bcda8..190054e20 100644 --- a/packages/next-intl/src/navigation/shared/utils.tsx +++ b/packages/next-intl/src/navigation/shared/utils.tsx @@ -228,8 +228,10 @@ export function applyPathnamePrefix( 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;