Skip to content
Draft
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/trac-421-custom-locale-subfolders.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

Consume merchant-configured per-locale URL subfolders from the BigCommerce Storefront GraphQL API (`Locale.path`). When the Control Panel has any custom subfolder set, Catalyst trusts the CP mapping for every locale (including the default) and routes URLs using those prefixes. When no custom subfolders are configured, Catalyst keeps the legacy behavior: the default locale has no prefix and other locales use their locale code.
1 change: 1 addition & 0 deletions core/build-config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const buildConfigSchema = z.object({
z.object({
code: z.string(),
isDefault: z.boolean(),
path: z.string().nullable(),
}),
),
urls: z.object({
Expand Down
8 changes: 8 additions & 0 deletions core/i18n/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ const localeNodes = buildConfig.get('locales');

export const locales = localeNodes.map((locale) => locale.code);
export const defaultLocale = localeNodes.find((locale) => locale.isDefault)?.code ?? 'en';

export const prefixes = localeNodes.reduce<Record<string, `/${string}`>>((acc, locale) => {
if (locale.path) acc[locale.code] = `/${locale.path}`;

return acc;
}, {});

export const hasCustomSubfolders = Object.keys(prefixes).length > 0;
11 changes: 8 additions & 3 deletions core/i18n/routing.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { createNavigation } from 'next-intl/navigation';
import { defineRouting } from 'next-intl/routing';

import { defaultLocale, locales } from './locales';
import { defaultLocale, hasCustomSubfolders, locales, prefixes } from './locales';

enum LocalePrefixes {
ALWAYS = 'always',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just curious: why did we remove this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't think we needed the enum anymore as we are only using as-needed. I can add a comment like this if you prefer:

localePrefix: {
// 'as-needed' removes the prefix for the default locale.
// 'always' would prefix every locale including the default.
// 'never' has cache-related issues — avoid:
// amannn/next-intl#786
mode: 'as-needed',
prefixes,
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Enum change reverted to provide context for merchant configuration

// Don't use NEVER as there is a issue that causes cache problems and returns the wrong messages.
// Don't use NEVER as there is an issue that causes cache problems and returns the wrong messages.
// More info: https://github.com/amannn/next-intl/issues/786
// NEVER = 'never',
ASNEEDED = 'as-needed', // removes prefix on default locale
}

const localePrefix = LocalePrefixes.ASNEEDED;
// When the merchant has configured any locale subfolders in the BC control panel,
// trust the CP mapping for every locale (including the default). Otherwise fall back
// to the legacy behavior where the default locale has no prefix.
const localePrefix = hasCustomSubfolders
? { mode: LocalePrefixes.ALWAYS, prefixes }
: LocalePrefixes.ASNEEDED;

export const routing = defineRouting({
locales,
Expand Down
7 changes: 5 additions & 2 deletions core/lib/seo/canonical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { cache } from 'react';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { defaultLocale, locales } from '~/i18n/locales';
import { defaultLocale, hasCustomSubfolders, locales, prefixes } from '~/i18n/locales';

interface CanonicalUrlOptions {
/**
Expand Down Expand Up @@ -90,7 +90,10 @@ function buildLocalizedUrl(baseUrl: string, pathname: string, locale: string): s

const url = new URL(pathname, baseUrl);

url.pathname = locale === defaultLocale ? url.pathname : `/${locale}${url.pathname}`;
const prefix = prefixes[locale] ?? `/${locale}`;
const skipPrefix = !hasCustomSubfolders && locale === defaultLocale;

url.pathname = skipPrefix ? url.pathname : `${prefix}${url.pathname}`;

if (trailingSlash && !url.pathname.endsWith('/')) {
url.pathname += '/';
Expand Down
1 change: 1 addition & 0 deletions core/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const SettingsQuery = graphql(`
locales {
code
isDefault
path
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions core/proxies/with-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { auth } from '~/auth';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { prefixes } from '~/i18n/locales';
import { getVisitIdCookie, getVisitorIdCookie } from '~/lib/analytics/bigcommerce';
import { sendProductViewedEvent } from '~/lib/analytics/bigcommerce/data-events';
import { kvKey, STORE_STATUS_KEY } from '~/lib/kv/keys';
Expand Down Expand Up @@ -218,12 +219,14 @@ const updateStatusCache = async (
};

const clearLocaleFromPath = (path: string, locale: string) => {
if (path === `/${locale}` || path === `/${locale}/`) {
const prefix = prefixes[locale] ?? `/${locale}`;

if (path === prefix || path === `${prefix}/`) {
return '/';
}

if (path.startsWith(`/${locale}/`)) {
return path.replace(`/${locale}`, '');
if (path.startsWith(`${prefix}/`)) {
return path.replace(prefix, '');
}

return path;
Expand Down
Loading