Files
web_sport/apps/storefront/src/app/[locale]/layout.tsx
T

79 lines
2.4 KiB
TypeScript

import type { Metadata, Viewport } from 'next';
import { notFound } from 'next/navigation';
import { hasLocale, NextIntlClientProvider } from 'next-intl';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { LOCALE_TAGS, type Locale } from '@sport/types';
import { CartProvider } from '@/features/cart/cart-provider';
import { routing } from '@/i18n/routing';
import '@/styles/globals.css';
/**
* This is the root layout — there is no `app/layout.tsx`. Every route lives
* under `[locale]`, so the `<html lang>` attribute can be set from the resolved
* locale rather than hard-coded, which screen readers and translation tools
* both depend on.
*/
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'meta' });
return {
title: { default: t('defaultTitle'), template: `%s | ${t('siteName')}` },
description: t('defaultDescription'),
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'),
// Tells search engines these are translations of one another rather than
// duplicate content — the single most important i18n SEO signal.
alternates: {
canonical: '/',
languages: { vi: '/', en: '/en' },
},
};
}
export const viewport: Viewport = {
themeColor: '#111111',
width: 'device-width',
initialScale: 1,
};
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
// Required for static rendering: without it every page under this layout
// opts into dynamic rendering the moment it reads a translation.
setRequestLocale(locale);
return (
<html lang={LOCALE_TAGS[locale as Locale]} suppressHydrationWarning>
<body className="min-h-screen antialiased">
<NextIntlClientProvider>
{/* Above both route groups: the bag survives moving from browsing
into checkout, which is the one transition it must not lose. */}
<CartProvider locale={locale as Locale}>{children}</CartProvider>
</NextIntlClientProvider>
</body>
</html>
);
}