karbe/src/app/layout.tsx
Claude Integration cf9da94bb5 feat(plugin): i18n FR + EN (Phase 4.2)
Infrastructure i18n légère, sans deps externe :

- lib/i18n/types.ts : LOCALES, DEFAULT_LOCALE, cookie name
- lib/i18n/server.ts : getLocale (cookie > Accept-Language > FR),
  t(key) async server-side, dict(locale)
- lib/i18n/client.tsx : LocaleProvider + useLocale + useT
- messages/fr.json + messages/en.json : ~50 clés pour landing + header + footer
- LocaleSwitcher component (cookie + router.refresh)

Plugin gated :
- Quand i18n-fr-en désactivé, getLocale() force FR. Le switcher ne s'affiche
  pas dans le hero. Pas d'impact sur le rendu existant.
- Quand activé, switcher visible coin haut-droit du hero. Les composants
  landing/header/footer rendent en FR ou EN selon le cookie utilisateur.

Composants i18n-isés :
- HeroSection (eyebrow, titre, CTA)
- ExperiencesSection (route/fleuve vs expédition, tous les bullets)
- HowItWorksSection (3 étapes)
- CESection (KPIs + body + CTA)
- TestimonialsSection (eyebrow + titre, citations restent en VO)
- Footer (taglines, colonnes)
- SeasonBanner (3 saisons + messages)
- AccessTypeBadge (labels + tooltips)

Pour les ContentPage, le champ lang existait déjà. Une suite (PR ultérieure)
ajoutera le filtre lang dans getContentPage + seed pages EN.
2026-05-31 11:38:39 +00:00

93 lines
2.9 KiB
TypeScript

import type { Metadata } from "next";
import { Geist, Geist_Mono, Cormorant_Garamond } from "next/font/google";
import "./globals.css";
import { PluginProvider } from "@/lib/plugins/client";
import { getEnabledPluginKeys, syncPluginsFromRegistry } from "@/lib/plugins/server";
import { SeasonBanner } from "@/components/SeasonBanner";
import { LocaleProvider } from "@/lib/i18n/client";
import { dict, getLocale } from "@/lib/i18n/server";
// Le layout interroge la DB Plugin à chaque request → rendu dynamique forcé.
// Sans ça, le layout (et donc data-theme + enabledKeys passés au client) est
// statiquement rendu au build et ne reflète plus l'état actuel des toggles.
export const dynamic = "force-dynamic";
export const revalidate = 0;
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
// Cormorant Garamond : typographie display pour le theme Guyane (gated par
// le plugin `theme-guyane`). Sert pour `font-serif`.
const cormorant = Cormorant_Garamond({
variable: "--font-serif",
subsets: ["latin"],
weight: ["400", "500", "600"],
display: "swap",
});
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: {
default: "Karbé — carbets fluviaux de Guyane",
template: "%s | Karbé",
},
description:
"Karbé, la marketplace de location de carbets fluviaux de Guyane.",
openGraph: {
type: "website",
siteName: "Karbé",
locale: "fr_FR",
title: "Karbé — carbets fluviaux de Guyane",
description:
"La marketplace pour louer des carbets fluviaux le long des fleuves de Guyane.",
},
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// Plugins Karbé : sync registry → DB puis charge la liste activée pour le client.
// En cas d'erreur DB (build statique sans DB par ex.), on retombe en mode "aucun
// plugin activé" pour ne pas casser le rendu.
let enabledKeys: string[] = [];
try {
await syncPluginsFromRegistry();
enabledKeys = await getEnabledPluginKeys();
} catch {
enabledKeys = [];
}
const themeGuyane = enabledKeys.includes("theme-guyane");
const locale = await getLocale();
const messages = await dict(locale);
return (
<html
lang={locale}
className={`${geistSans.variable} ${geistMono.variable} ${cormorant.variable} h-full antialiased`}
>
<body
data-theme={themeGuyane ? "guyane" : undefined}
className="min-h-full flex flex-col font-sans"
>
<PluginProvider enabledKeys={enabledKeys}>
<LocaleProvider locale={locale} messages={messages}>
<SeasonBanner />
{children}
</LocaleProvider>
</PluginProvider>
</body>
</html>
);
}