diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8b05af1..54c6919 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,8 @@ 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 @@ -67,10 +69,12 @@ export default async function RootLayout({ } const themeGuyane = enabledKeys.includes("theme-guyane"); + const locale = await getLocale(); + const messages = await dict(locale); return ( - - {children} + + + {children} + diff --git a/src/components/AccessTypeBadge.tsx b/src/components/AccessTypeBadge.tsx index 27b8d84..758486a 100644 --- a/src/components/AccessTypeBadge.tsx +++ b/src/components/AccessTypeBadge.tsx @@ -1,12 +1,12 @@ "use client"; import { useIsPluginEnabled } from "@/lib/plugins/client"; +import { useT } from "@/lib/i18n/client"; import type { AccessType } from "@/generated/prisma/enums"; /** * Badge route+fleuve vs fleuve only. Gated par le plugin `access-type`. - * Si le plugin est désactivé, rien n'est rendu — la fiche tombe sur le - * comportement legacy (pirogue toujours mentionnée). + * Si le plugin est désactivé, rien n'est rendu. Label i18n via useT(). */ export function AccessTypeBadge({ accessType, @@ -16,10 +16,11 @@ export function AccessTypeBadge({ size?: "sm" | "md"; }) { const enabled = useIsPluginEnabled("access-type"); + const t = useT(); if (!enabled) return null; const isExpedition = accessType === "RIVER_ONLY"; - const label = isExpedition ? "🛶 Expédition fleuve" : "🛣️ Route + fleuve"; + const label = isExpedition ? t("access.riverOnly") : t("access.roadAndRiver"); const styles = isExpedition ? "bg-[var(--color-karbe-laterite-300)]/25 text-[var(--color-karbe-laterite-700)] ring-[var(--color-karbe-laterite-500)]/30" : "bg-[var(--color-karbe-canopy-50)] text-[var(--color-karbe-canopy-700)] ring-[var(--color-karbe-canopy-500)]/30"; @@ -31,11 +32,7 @@ export function AccessTypeBadge({ return ( {label} diff --git a/src/components/LocaleSwitcher.tsx b/src/components/LocaleSwitcher.tsx new file mode 100644 index 0000000..7472539 --- /dev/null +++ b/src/components/LocaleSwitcher.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { useLocale, useT } from "@/lib/i18n/client"; +import { LOCALE_COOKIE, type Locale } from "@/lib/i18n/types"; + +/** + * Switcher de langue (FR / EN). Pose le cookie karbe-locale et refresh la page + * pour que le server re-render avec la nouvelle locale. + */ +export function LocaleSwitcher() { + const router = useRouter(); + const current = useLocale(); + const t = useT(); + const [pending, startTransition] = useTransition(); + + function setLocale(next: Locale) { + if (next === current) return; + // 1 an, scope au site entier + document.cookie = `${LOCALE_COOKIE}=${next}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`; + startTransition(() => { + router.refresh(); + }); + } + + return ( +
+ {t("language.switch")} + + +
+ ); +} diff --git a/src/components/SeasonBanner.tsx b/src/components/SeasonBanner.tsx index d489cd3..2d1a6ab 100644 --- a/src/components/SeasonBanner.tsx +++ b/src/components/SeasonBanner.tsx @@ -1,38 +1,40 @@ import { isPluginEnabled } from "@/lib/plugins/server"; import { currentSeason, SEASON_META } from "@/lib/seasonality"; +import { t } from "@/lib/i18n/server"; + +const TONES = { + ok: "bg-[var(--color-karbe-canopy-700)] text-[var(--color-karbe-bone)]", + warn: "bg-[var(--color-karbe-laterite-500)] text-[var(--color-karbe-bone)]", + info: "bg-[var(--color-karbe-maroni-700)] text-[var(--color-karbe-bone)]", +} as const; + +const SEASON_KEYS = { + DRY: { label: "season.dry", message: "season.dry.message" }, + LOW_WATER: { label: "season.lowWater", message: "season.lowWater.message" }, + WET: { label: "season.wet", message: "season.wet.message" }, +} as const; /** * Bandeau saison — affiché en haut de la home et de /carbets si le plugin * `seasonality` est activé. Server component pur, pas de fetch DB. + * Texte i18n via t() server-side. */ export async function SeasonBanner() { if (!(await isPluginEnabled("seasonality"))) return null; const season = currentSeason(); const meta = SEASON_META[season]; - - const messages: Record = { - DRY: - "Conditions optimales : fleuves navigables, pistes route en bon état, lever de soleil sur l'eau brûlante.", - LOW_WATER: - "Étiage en cours : les carbets fleuve uniquement peuvent ne pas être accessibles. Filtre dispo en page recherche.", - WET: - "Pluies fréquentes : la jungle est dense et vivante, prévoir un véhicule adapté pour les carbets route+fleuve.", - }; - - const tones = { - ok: "bg-[var(--color-karbe-canopy-700)] text-[var(--color-karbe-bone)]", - warn: "bg-[var(--color-karbe-laterite-500)] text-[var(--color-karbe-bone)]", - info: "bg-[var(--color-karbe-maroni-700)] text-[var(--color-karbe-bone)]", - } as const; + const keys = SEASON_KEYS[season]; + const label = await t(keys.label); + const message = await t(keys.message); return ( -