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/fix-html-lang-locale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

Restore locale-aware `lang` attribute on the root `<html>` tag. The previous root layout hardcoded `lang="en"` for all locales; ownership of `<html>`/`<body>` now lives in `app/[locale]/layout.tsx` so `lang={locale}` reflects the active locale. The root `app/layout.tsx` is now a passthrough, and `app/not-found.tsx` is self-sufficient (renders its own `<html>`/`<body>`) to preserve the branded 404 for non-localized requests.
58 changes: 32 additions & 26 deletions core/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { clsx } from 'clsx';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import { cache, PropsWithChildren } from 'react';

import '../../globals.css';

import { fonts } from '~/app/fonts';
import { CookieNotifications } from '~/app/notifications';
import { Providers } from '~/app/providers';
import { client } from '~/client';
Expand Down Expand Up @@ -136,32 +140,34 @@ export default async function RootLayout({ params, children }: Props) {
const privacyPolicyUrl = rootData.data.site.settings?.privacy?.privacyPolicyUrl;

return (
<>
<NextIntlClientProvider>
<ConsentManager
isCookieConsentEnabled={isCookieConsentEnabled}
privacyPolicyUrl={privacyPolicyUrl}
scripts={scripts}
>
<NuqsAdapter>
<AnalyticsProvider
channelId={rootData.data.channel.entityId}
isCookieConsentEnabled={isCookieConsentEnabled}
settings={rootData.data.site.settings}
>
<Providers>
{toastNotificationCookieData && (
<CookieNotifications {...toastNotificationCookieData} />
)}
{children}
</Providers>
</AnalyticsProvider>
</NuqsAdapter>
</ConsentManager>
</NextIntlClientProvider>
<VercelComponents />
<ContainerQueryPolyfill />
</>
<html className={clsx(fonts.map((f) => f.variable))} lang={locale}>
<body className="flex min-h-screen flex-col">
<NextIntlClientProvider>
<ConsentManager
isCookieConsentEnabled={isCookieConsentEnabled}
privacyPolicyUrl={privacyPolicyUrl}
scripts={scripts}
>
<NuqsAdapter>
<AnalyticsProvider
channelId={rootData.data.channel.entityId}
isCookieConsentEnabled={isCookieConsentEnabled}
settings={rootData.data.site.settings}
>
<Providers>
{toastNotificationCookieData && (
<CookieNotifications {...toastNotificationCookieData} />
)}
{children}
</Providers>
</AnalyticsProvider>
</NuqsAdapter>
</ConsentManager>
</NextIntlClientProvider>
<VercelComponents />
<ContainerQueryPolyfill />
</body>
</html>
);
}

Expand Down
17 changes: 7 additions & 10 deletions core/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { clsx } from 'clsx';
import { PropsWithChildren } from 'react';

import '../globals.css';

import { fonts } from '~/app/fonts';

// Since we have a `not-found.tsx` at the root, a layout file is required even if
// it just passes children through. Ownership of <html>/<body> lives in
// app/[locale]/layout.tsx (to set lang={locale}) and app/not-found.tsx (for
// non-localized 404s). See: https://next-intl.dev/docs/environments/error-files
// TODO: Move <html>/<body> back here and set lang via Next.js `rootParams`
// once it is available on Native Hosting (Next.js 16.2+).
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html className={clsx(fonts.map((f) => f.variable))} lang="en">
<body className="flex min-h-screen flex-col">{children}</body>
</html>
);
return children;
}
49 changes: 31 additions & 18 deletions core/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { clsx } from 'clsx';

import '../globals.css';

import { fonts } from '~/app/fonts';

// Renders for non-localized requests that don't match any route (e.g. /unknown.txt)
// or when app/[locale]/layout.tsx calls notFound() for an invalid locale. Since
// app/layout.tsx is a passthrough, this page must own its own <html>/<body>.
export default function RootNotFound() {
return (
<main className="flex flex-1 items-center justify-center font-[family-name:var(--not-found-font-family,var(--font-family-body))]">
<div className="mx-auto w-full max-w-screen-2xl px-3 py-10 @container @xl:px-6 @4xl:px-20">
<header className="text-center">
<h1 className="mb-3 font-[family-name:var(--not-found-title-font-family,var(--font-family-heading))] text-3xl font-medium leading-none text-[var(--not-found-title,hsl(var(--foreground)))] @xl:text-4xl @4xl:text-5xl">
Not found
</h1>
<p className="mb-4 text-lg text-[var(--not-found-subtitle,hsl(var(--contrast-500)))]">
The page you are looking for could not be found.
</p>
<a
className="relative z-0 inline-flex min-h-14 select-none items-center justify-center gap-x-3 overflow-hidden rounded-full border border-[var(--button-primary-border,hsl(var(--primary)))] bg-[var(--button-primary-background,hsl(var(--primary)))] px-6 py-4 text-center font-[family-name:var(--button-font-family)] text-base font-semibold leading-normal text-[var(--button-primary-text)] after:absolute after:inset-0 after:-z-10 after:-translate-x-[105%] after:rounded-full after:bg-[var(--button-primary-background-hover,color-mix(in_oklab,hsl(var(--primary)),white_75%))] after:transition-[opacity,transform] after:duration-300 after:[animation-timing-function:cubic-bezier(0,0.25,0,1)] hover:after:translate-x-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--button-focus,hsl(var(--primary)))] focus-visible:ring-offset-2"
href="/"
>
<span>Go to homepage</span>
</a>
</header>
</div>
</main>
<html className={clsx(fonts.map((f) => f.variable))} lang="en">
<body className="flex min-h-screen flex-col">
<main className="flex flex-1 items-center justify-center font-[family-name:var(--not-found-font-family,var(--font-family-body))]">
<div className="mx-auto w-full max-w-screen-2xl px-3 py-10 @container @xl:px-6 @4xl:px-20">
<header className="text-center">
<h1 className="mb-3 font-[family-name:var(--not-found-title-font-family,var(--font-family-heading))] text-3xl font-medium leading-none text-[var(--not-found-title,hsl(var(--foreground)))] @xl:text-4xl @4xl:text-5xl">
Not found
</h1>
<p className="mb-4 text-lg text-[var(--not-found-subtitle,hsl(var(--contrast-500)))]">
The page you are looking for could not be found.
</p>
<a
className="relative z-0 inline-flex min-h-14 select-none items-center justify-center gap-x-3 overflow-hidden rounded-full border border-[var(--button-primary-border,hsl(var(--primary)))] bg-[var(--button-primary-background,hsl(var(--primary)))] px-6 py-4 text-center font-[family-name:var(--button-font-family)] text-base font-semibold leading-normal text-[var(--button-primary-text)] after:absolute after:inset-0 after:-z-10 after:-translate-x-[105%] after:rounded-full after:bg-[var(--button-primary-background-hover,color-mix(in_oklab,hsl(var(--primary)),white_75%))] after:transition-[opacity,transform] after:duration-300 after:[animation-timing-function:cubic-bezier(0,0.25,0,1)] hover:after:translate-x-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--button-focus,hsl(var(--primary)))] focus-visible:ring-offset-2"
href="/"
>
<span>Go to homepage</span>
</a>
</header>
</div>
</main>
</body>
</html>
);
}
Loading