Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/twenty-ants-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-makeswift": patch
---

Fix locale switcher on localized Makeswift pages with different paths
Original file line number Diff line number Diff line change
@@ -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
);
}
34 changes: 25 additions & 9 deletions core/vibes/soul/primitives/navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 },
Copy link
Contributor Author

@agurtovoy agurtovoy Sep 23, 2025

Choose a reason for hiding this comment

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

The canonical way to handle this in next-intl is to use the pathnames configuration, but it's too inflexible for our purposes here:

  • If we kept it as a build-time configuration, it would require a site redeploy after each Makeswift publish that updates page paths.
  • A build-time configuration would also be incorrect for previews.
  • If we converted it into a run-time configuration, we’d have to rework our code to retrieve the derived navigation components from a context and then cache and revalidate them at runtime.

The approach in this PR is more localized and arguably more performant, as we don't have to retrieve the whole sitemap.

),
[pathname, params, router, searchParams],
);
},
[pathname, activeLocale?.id, params, router, searchParams],
);
};

Expand All @@ -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 (
<div className={className}>
Expand Down
Loading