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 `` 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 { 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 ( {/* Above both route groups: the bag survives moving from browsing into checkout, which is the one transition it must not lose. */} {children} ); }