From eb2d4e16e61c5822111fa50d8bc27db86cd5e9c4 Mon Sep 17 00:00:00 2001 From: Aleksey Gurtovoy Date: Tue, 23 Sep 2025 16:30:57 -0500 Subject: [PATCH] fix: locale switcher correctly works w/ localized Makeswift paths --- .changeset/twenty-ants-sip.md | 5 ++ .../navigation/_actions/localized-pathname.ts | 55 +++++++++++++++++++ .../soul/primitives/navigation/index.tsx | 34 +++++++++--- 3 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 .changeset/twenty-ants-sip.md create mode 100644 core/vibes/soul/primitives/navigation/_actions/localized-pathname.ts diff --git a/.changeset/twenty-ants-sip.md b/.changeset/twenty-ants-sip.md new file mode 100644 index 0000000000..8b3118bc19 --- /dev/null +++ b/.changeset/twenty-ants-sip.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-makeswift": patch +--- + +Fix locale switcher on localized Makeswift pages with different paths diff --git a/core/vibes/soul/primitives/navigation/_actions/localized-pathname.ts b/core/vibes/soul/primitives/navigation/_actions/localized-pathname.ts new file mode 100644 index 0000000000..0d74d3c1fe --- /dev/null +++ b/core/vibes/soul/primitives/navigation/_actions/localized-pathname.ts @@ -0,0 +1,55 @@ +'use server'; + +import { getSiteVersion } from '@makeswift/runtime/next/server'; + +import { defaultLocale } from '~/i18n/locales'; +import { client as makeswiftClient } from '~/lib/makeswift/client'; + +const getPageInfo = async ({ + pathname, + locale, +}: { + pathname: string; + locale: string | undefined; +}) => + makeswiftClient + .getPages({ + locale: locale === defaultLocale ? undefined : locale, + pathPrefix: pathname, + siteVersion: await getSiteVersion(), + }) + .filter((page) => page.path === pathname) + .toArray() + .then((pages) => (pages.length === 0 ? null : pages[0])); + +const getPathname = (variants: Array<{ locale: string; path: string }>, locale: string) => + variants.find((v) => v.locale === locale)?.path; + +export async function getLocalizedPathname({ + pathname, + activeLocale, + targetLocale, +}: { + pathname: string; + activeLocale: string | undefined; + targetLocale: string; +}) { + // fallback to page info for default locale if there is no page info for active locale + const fallbackPageInfo = + activeLocale === defaultLocale + ? Promise.resolve(null) + : getPageInfo({ pathname, locale: undefined }); + + const localizedPageInfo = await getPageInfo({ pathname, locale: activeLocale }); + const pageInfo = localizedPageInfo ?? (await fallbackPageInfo); + + if (pageInfo == null) { + return pathname; + } + + return ( + getPathname(pageInfo.localizedVariants, targetLocale) ?? + getPathname(pageInfo.localizedVariants, defaultLocale) ?? + pathname + ); +} diff --git a/core/vibes/soul/primitives/navigation/index.tsx b/core/vibes/soul/primitives/navigation/index.tsx index 26f52f3256..b3ac21c826 100644 --- a/core/vibes/soul/primitives/navigation/index.tsx +++ b/core/vibes/soul/primitives/navigation/index.tsx @@ -30,6 +30,8 @@ import { Link } from '~/components/link'; import { usePathname, useRouter } from '~/i18n/routing'; import { useSearch } from '~/lib/search'; +import { getLocalizedPathname } from './_actions/localized-pathname'; + interface Link { label: string; href: string; @@ -862,22 +864,36 @@ function SearchResults({ ); } -const useSwitchLocale = () => { +const useSwitchLocale = ({ activeLocale }: { activeLocale: Locale | undefined }) => { const pathname = usePathname(); const router = useRouter(); const params = useParams(); const searchParams = useSearchParams(); return useCallback( - (locale: string) => + async (locale: string) => { + const localizedPathname = await getLocalizedPathname({ + pathname, + activeLocale: activeLocale?.id, + targetLocale: locale, + }); + + // the Next.js App Router guarantees a `startTransition` call on `router.push`, + // so we don’t need to wrap it in an explicit nested call as set out in + // https://react.dev/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition router.push( - // @ts-expect-error -- TypeScript will validate that only known `params` - // are used in combination with a given `pathname`. Since the two will - // always match for the current route, we can skip runtime checks. - { pathname, params, query: Object.fromEntries(searchParams.entries()) }, + { + pathname: localizedPathname, + // @ts-expect-error -- TypeScript will validate that only known `params` + // are used in combination with a given `pathname`. Since the two will + // always match for the current route, we can skip runtime checks. + params, + query: Object.fromEntries(searchParams.entries()), + }, { locale }, - ), - [pathname, params, router, searchParams], + ); + }, + [pathname, activeLocale?.id, params, router, searchParams], ); }; @@ -892,7 +908,7 @@ function LocaleSwitcher({ }) { const activeLocale = locales.find((locale) => locale.id === activeLocaleId); const [isPending, startTransition] = useTransition(); - const switchLocale = useSwitchLocale(); + const switchLocale = useSwitchLocale({ activeLocale }); return (