Skip to content

Commit 15e365a

Browse files
authored
feat(core): TRAC-421 consume merchant-configured locale subfolders from BC backend (bigcommerce#3015)
1 parent 8bc379d commit 15e365a

7 files changed

Lines changed: 57 additions & 10 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bigcommerce/catalyst-core": minor
3+
---
4+
5+
Consume merchant-configured per-locale URL subfolders from the BigCommerce Storefront GraphQL API (`Locale.path`). The locale that sits at the bare root URL (`/`) is derived from the CP configuration: if the default locale has no path, it sits at root; otherwise, if exactly one non-default locale has no path, that one sits at root; otherwise every locale gets a prefix. Locales with a path use it; locales without a path fall back to their locale code.

core/build-config/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const buildConfigSchema = z.object({
55
z.object({
66
code: z.string(),
77
isDefault: z.boolean(),
8+
path: z.string().nullable(),
89
}),
910
),
1011
urls: z.object({

core/i18n/locales.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
import { buildConfig } from '~/build-config/reader';
22

33
const localeNodes = buildConfig.get('locales');
4+
const defaultLocaleNode = localeNodes.find((locale) => locale.isDefault);
45

56
export const locales = localeNodes.map((locale) => locale.code);
6-
export const defaultLocale = localeNodes.find((locale) => locale.isDefault)?.code ?? 'en';
7+
export const defaultLocale = defaultLocaleNode?.code ?? 'en';
8+
9+
export const prefixes = localeNodes.reduce<Record<string, `/${string}`>>((acc, locale) => {
10+
if (locale.path) acc[locale.code] = `/${locale.path}`;
11+
12+
return acc;
13+
}, {});
14+
15+
// The locale that should occupy the bare root URL ("/").
16+
//
17+
// Rule:
18+
// 1. If the default locale has no CP path, default lives at "/".
19+
// 2. Else, if exactly one non-default locale has no CP path, that one lives at "/".
20+
// 3. Otherwise, no locale lives at "/" and every locale gets a prefix.
21+
function computeRootLocale(): string | null {
22+
if (!defaultLocaleNode) return defaultLocale;
23+
if (!defaultLocaleNode.path) return defaultLocaleNode.code;
24+
25+
const blankNonDefaults = localeNodes.filter((locale) => !locale.isDefault && !locale.path);
26+
const [onlyBlank, ...rest] = blankNonDefaults;
27+
28+
if (onlyBlank && rest.length === 0) return onlyBlank.code;
29+
30+
return null;
31+
}
32+
33+
export const rootLocale = computeRootLocale();

core/i18n/routing.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
import { createNavigation } from 'next-intl/navigation';
22
import { defineRouting } from 'next-intl/routing';
33

4-
import { defaultLocale, locales } from './locales';
4+
import { defaultLocale, locales, prefixes, rootLocale } from './locales';
55

66
enum LocalePrefixes {
77
ALWAYS = 'always',
8-
// Don't use NEVER as there is a issue that causes cache problems and returns the wrong messages.
8+
// Don't use NEVER as there is an issue that causes cache problems and returns the wrong messages.
99
// More info: https://github.com/amannn/next-intl/issues/786
1010
// NEVER = 'never',
1111
ASNEEDED = 'as-needed', // removes prefix on default locale
1212
}
1313

14-
const localePrefix = LocalePrefixes.ASNEEDED;
14+
// `rootLocale` is the locale (if any) that occupies the bare "/" URL. We hand it
15+
// to next-intl as the routing default so URLs like "/" resolve correctly. When no
16+
// locale lives at "/", every locale needs a prefix, so we use `always` mode and
17+
// fall back to the BC default for next-intl's defaultLocale.
18+
const localePrefix =
19+
rootLocale === null
20+
? { mode: LocalePrefixes.ALWAYS, prefixes }
21+
: { mode: LocalePrefixes.ASNEEDED, prefixes };
1522

1623
export const routing = defineRouting({
1724
locales,
18-
defaultLocale,
25+
defaultLocale: rootLocale ?? defaultLocale,
1926
localePrefix,
2027
});
2128

core/lib/seo/canonical.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { cache } from 'react';
33
import { client } from '~/client';
44
import { graphql } from '~/client/graphql';
55
import { revalidate } from '~/client/revalidate-target';
6-
import { defaultLocale, locales } from '~/i18n/locales';
6+
import { defaultLocale, locales, prefixes, rootLocale } from '~/i18n/locales';
77

88
interface CanonicalUrlOptions {
99
/**
@@ -90,7 +90,10 @@ function buildLocalizedUrl(baseUrl: string, pathname: string, locale: string): s
9090

9191
const url = new URL(pathname, baseUrl);
9292

93-
url.pathname = locale === defaultLocale ? url.pathname : `/${locale}${url.pathname}`;
93+
const prefix = prefixes[locale] ?? `/${locale}`;
94+
const skipPrefix = locale === rootLocale;
95+
96+
url.pathname = skipPrefix ? url.pathname : `${prefix}${url.pathname}`;
9497

9598
if (trailingSlash && !url.pathname.endsWith('/')) {
9699
url.pathname += '/';

core/next.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const SettingsQuery = graphql(`
2525
locales {
2626
code
2727
isDefault
28+
path
2829
}
2930
}
3031
}

core/proxies/with-routes.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { auth } from '~/auth';
55
import { client } from '~/client';
66
import { graphql } from '~/client/graphql';
77
import { revalidate } from '~/client/revalidate-target';
8+
import { prefixes } from '~/i18n/locales';
89
import { getVisitIdCookie, getVisitorIdCookie } from '~/lib/analytics/bigcommerce';
910
import { sendProductViewedEvent } from '~/lib/analytics/bigcommerce/data-events';
1011
import { kvKey, STORE_STATUS_KEY } from '~/lib/kv/keys';
@@ -218,12 +219,14 @@ const updateStatusCache = async (
218219
};
219220

220221
const clearLocaleFromPath = (path: string, locale: string) => {
221-
if (path === `/${locale}` || path === `/${locale}/`) {
222+
const prefix = prefixes[locale] ?? `/${locale}`;
223+
224+
if (path === prefix || path === `${prefix}/`) {
222225
return '/';
223226
}
224227

225-
if (path.startsWith(`/${locale}/`)) {
226-
return path.replace(`/${locale}`, '');
228+
if (path.startsWith(`${prefix}/`)) {
229+
return path.replace(prefix, '');
227230
}
228231

229232
return path;

0 commit comments

Comments
 (0)