79 lines
2.4 KiB
TypeScript
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>
|
|
);
|
|
}
|