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.
This commit is contained in:
Claude Integration 2026-05-31 11:38:39 +00:00
parent efeea16467
commit cf9da94bb5
15 changed files with 454 additions and 116 deletions

View file

@ -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 (
<span
className={`inline-flex items-center gap-1 rounded-full font-medium ring-1 ${styles} ${sizing}`}
title={
isExpedition
? "Accessible uniquement par pirogue depuis un dégrad."
: "Accessible par la route et par le fleuve."
}
title={isExpedition ? t("access.riverOnly.title") : t("access.roadAndRiver.title")}
>
{label}
</span>

View file

@ -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 (
<div className="inline-flex items-center gap-1 rounded-full border border-[var(--color-karbe-bone)]/30 bg-[var(--color-karbe-bone)]/10 p-0.5 text-[11px] font-semibold backdrop-blur-sm">
<span className="sr-only">{t("language.switch")}</span>
<button
type="button"
onClick={() => setLocale("fr")}
disabled={pending}
aria-pressed={current === "fr"}
className={`rounded-full px-2.5 py-0.5 uppercase tracking-wider transition ${
current === "fr"
? "bg-[var(--color-karbe-bone)] text-[var(--color-karbe-canopy-900)]"
: "text-[var(--color-karbe-bone)] hover:bg-[var(--color-karbe-bone)]/15"
} disabled:opacity-50`}
>
FR
</button>
<button
type="button"
onClick={() => setLocale("en")}
disabled={pending}
aria-pressed={current === "en"}
className={`rounded-full px-2.5 py-0.5 uppercase tracking-wider transition ${
current === "en"
? "bg-[var(--color-karbe-bone)] text-[var(--color-karbe-canopy-900)]"
: "text-[var(--color-karbe-bone)] hover:bg-[var(--color-karbe-bone)]/15"
} disabled:opacity-50`}
>
EN
</button>
</div>
);
}

View file

@ -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<typeof season, string> = {
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 (
<aside className={`${tones[meta.tone]} text-xs sm:text-sm`}>
<aside className={`${TONES[meta.tone]} text-xs sm:text-sm`}>
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-6 py-2 sm:px-8 lg:px-12">
<span className="flex items-center gap-2">
<span aria-hidden>{meta.emoji}</span>
<span className="font-semibold uppercase tracking-wider">{meta.label}</span>
<span className="font-semibold uppercase tracking-wider">{label}</span>
<span className="hidden text-[var(--color-karbe-bone)]/85 sm:inline">
· {messages[season]}
· {message}
</span>
</span>
</div>

View file

@ -1,36 +1,36 @@
import Link from "next/link";
import { t } from "@/lib/i18n/server";
/**
* Section dédiée aux Comités d'Entreprise (CE). Registre coop/solidaire,
* voix différente du reste de la home (qui parle au touriste aventurier).
* Section dédiée aux Comités d'Entreprise (CE). Registre coop/solidaire, i18n.
*/
export function CESection() {
export async function CESection() {
const kpis = [
{ k: "0 %", v: await t("ce.kpi.commission") },
{ k: "CE first", v: await t("ce.kpi.ceFirst") },
{ k: "Public open", v: await t("ce.kpi.publicOpen") },
{ k: "Stripe", v: await t("ce.kpi.noPaperwork") },
];
return (
<section className="bg-[var(--color-karbe-canopy-700)] text-[var(--color-karbe-bone)]">
<div className="mx-auto grid max-w-6xl gap-8 px-6 py-20 sm:px-8 sm:py-24 lg:grid-cols-2 lg:gap-16 lg:px-12">
<div>
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-[var(--color-karbe-laterite-300)]">
Pour comités d&apos;entreprise
{await t("ce.eyebrow")}
</span>
<h2 className="mt-3 font-serif text-3xl font-medium leading-tight tracking-tight sm:text-4xl md:text-5xl">
Les carbets dorment quand vous n&apos;y êtes pas.
{await t("ce.title")}
<br />
<span className="italic text-[var(--color-karbe-bone)]/80">Partageons-les.</span>
<span className="italic text-[var(--color-karbe-bone)]/80">{await t("ce.titleAccent")}</span>
</h2>
<p className="mt-5 text-base leading-relaxed text-[var(--color-karbe-bone)]/85 sm:text-lg">
Karbé est conçu pour que les comités sociaux possédant déjà un carbet le réservent à
leurs membres certains week-ends, et l&apos;ouvrent au public touriste le reste de
l&apos;année. Sans commission sur le séjour : le paiement revient intégralement au CE.
{await t("ce.body")}
</p>
</div>
<ul className="grid grid-cols-1 gap-4 self-center sm:grid-cols-2">
{[
{ k: "0 %", v: "de commission sur le séjour" },
{ k: "CE first", v: "vos membres réservent en priorité" },
{ k: "Public ouvert", v: "le reste des dates rentre dans le pot" },
{ k: "Sans paperasse", v: "Stripe encaisse et reverse direct" },
].map(({ k, v }) => (
{kpis.map(({ k, v }) => (
<li
key={k}
className="rounded-2xl border border-[var(--color-karbe-bone)]/15 bg-[var(--color-karbe-bone)]/5 p-5 backdrop-blur-sm"
@ -44,7 +44,7 @@ export function CESection() {
href="/pour-comites-entreprise"
className="inline-flex items-center gap-2 rounded-full bg-[var(--color-karbe-laterite-500)] px-5 py-2.5 text-sm font-semibold text-[var(--color-karbe-bone)] transition hover:bg-[var(--color-karbe-laterite-700)]"
>
En savoir plus pour votre CE
{await t("ce.cta")}
<span aria-hidden></span>
</Link>
</li>

View file

@ -1,24 +1,23 @@
import { RoadIcon, PirogueIcon } from "@/components/illustrations/Icons";
import { t } from "@/lib/i18n/server";
/**
* Section « 2 expériences » route+fleuve vs expédition fleuve.
* Reflète la distinction métier qu'on appliquera côté schema dans le plugin
* `access-type`. Ici on ne fait que l'éditorialiser pour la home.
* Texte i18n via t() server-side.
*/
export function ExperiencesSection() {
export async function ExperiencesSection() {
return (
<section className="relative bg-[var(--color-karbe-bone)] py-20 sm:py-24">
<div className="mx-auto max-w-6xl px-6 sm:px-8 lg:px-12">
<div className="max-w-2xl">
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-[var(--color-karbe-canopy-700)]">
Deux façons de vivre Karbé
{await t("experiences.eyebrow")}
</span>
<h2 className="mt-3 font-serif text-3xl font-medium tracking-tight text-[var(--color-karbe-ink)] sm:text-4xl md:text-5xl">
Du bord du fleuve à l&apos;expédition pirogue.
{await t("experiences.title")}
</h2>
<p className="mt-4 max-w-xl text-base leading-relaxed text-[var(--color-karbe-ink)]/75 sm:text-lg">
Selon l&apos;envie, on choisit le carbet qui se rejoint en voiture pour un week-end facile,
ou celui qu&apos;on n&apos;atteint qu&apos;en pirogue, à plusieurs heures du dernier village.
{await t("experiences.subtitle")}
</p>
</div>
@ -29,20 +28,18 @@ export function ExperiencesSection() {
<RoadIcon className="h-full w-full" />
</div>
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-canopy-500)]">
🛣 Route + fleuve
{await t("experiences.roadFluve.tag")}
</p>
<h3 className="mt-3 font-serif text-2xl font-medium text-[var(--color-karbe-ink)]">
Le carbet du week-end
{await t("experiences.roadFluve.title")}
</h3>
<p className="mt-3 text-sm leading-relaxed text-[var(--color-karbe-ink)]/75">
Accessible par la piste depuis Kourou, Saint-Laurent ou Régina. Garez la voiture,
prenez vos affaires et vous y êtes. Pour les familles, les couples qui veulent du calme
sans logistique, les CE qui réservent des séjours courts.
{await t("experiences.roadFluve.body")}
</p>
<ul className="mt-5 space-y-1.5 text-sm text-[var(--color-karbe-ink)]/70">
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> 1 à 3 nuits typiques</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> Voiture ou 4×4 selon la piste</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> Carbets équipés, baignade possible</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> {await t("experiences.roadFluve.b1")}</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> {await t("experiences.roadFluve.b2")}</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> {await t("experiences.roadFluve.b3")}</li>
</ul>
</article>
@ -52,20 +49,18 @@ export function ExperiencesSection() {
<PirogueIcon className="h-full w-full" />
</div>
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-700)]">
🛶 Expédition fleuve
{await t("experiences.riverOnly.tag")}
</p>
<h3 className="mt-3 font-serif text-2xl font-medium text-[var(--color-karbe-ink)]">
Le carbet qu&apos;on mérite
{await t("experiences.riverOnly.title")}
</h3>
<p className="mt-3 text-sm leading-relaxed text-[var(--color-karbe-ink)]/75">
Aucune route n&apos;y mène. On embarque en pirogue depuis un dégrad, parfois deux ou
trois heures de remontée. Pour ceux qui veulent vraiment dormir loin singes hurleurs,
ciel sans halo, l&apos;eau du fleuve à 5 mètres du hamac.
{await t("experiences.riverOnly.body")}
</p>
<ul className="mt-5 space-y-1.5 text-sm text-[var(--color-karbe-ink)]/70">
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> 2 nuits minimum recommandées</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> Pirogue avec passeur (loueur ou partenaire)</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> Saison sèche conseillée (juillet-novembre)</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> {await t("experiences.riverOnly.b1")}</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> {await t("experiences.riverOnly.b2")}</li>
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> {await t("experiences.riverOnly.b3")}</li>
</ul>
</article>
</div>

View file

@ -1,7 +1,8 @@
import Link from "next/link";
import { PalmIcon, WaveIcon } from "@/components/illustrations/Icons";
import { t } from "@/lib/i18n/server";
export function LandingFooter() {
export async function LandingFooter() {
const year = new Date().getFullYear();
return (
<footer className="relative bg-[var(--color-karbe-canopy-900)] text-[var(--color-karbe-bone)]/85">
@ -14,42 +15,47 @@ export function LandingFooter() {
<span className="font-serif text-2xl font-medium tracking-tight text-[var(--color-karbe-bone)]">Karbé</span>
</div>
<p className="mt-3 text-sm leading-relaxed text-[var(--color-karbe-bone)]/65">
Marketplace des carbets fluviaux de Guyane. Solidaire avec les CE locaux. Sans
commission sur le séjour.
{await t("footer.tagline")}
</p>
</div>
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">Découvrir</h4>
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">
{await t("footer.col.discover")}
</h4>
<ul className="mt-4 space-y-2 text-sm">
<li><Link href="/carbets" className="hover:text-[var(--color-karbe-bone)]">Tous les carbets</Link></li>
<li><Link href="/comment-ca-marche" className="hover:text-[var(--color-karbe-bone)]">Comment ça marche</Link></li>
<li><Link href="/a-propos" className="hover:text-[var(--color-karbe-bone)]">À propos de Karbé</Link></li>
<li><Link href="/carbets" className="hover:text-[var(--color-karbe-bone)]">{await t("hero.ctaDiscover")}</Link></li>
<li><Link href="/comment-ca-marche" className="hover:text-[var(--color-karbe-bone)]">{await t("howItWorks.eyebrow")}</Link></li>
<li><Link href="/a-propos" className="hover:text-[var(--color-karbe-bone)]">Karbé</Link></li>
</ul>
</div>
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">Proposer</h4>
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">
{await t("footer.col.propose")}
</h4>
<ul className="mt-4 space-y-2 text-sm">
<li><Link href="/espace-hote" className="hover:text-[var(--color-karbe-bone)]">Devenir loueur</Link></li>
<li><Link href="/pour-comites-entreprise" className="hover:text-[var(--color-karbe-bone)]">Pour comités d&apos;entreprise</Link></li>
<li><Link href="/connexion" className="hover:text-[var(--color-karbe-bone)]">Espace membre</Link></li>
<li><Link href="/devenir-loueur" className="hover:text-[var(--color-karbe-bone)]">{await t("hero.ctaPropose")}</Link></li>
<li><Link href="/pour-comites-entreprise" className="hover:text-[var(--color-karbe-bone)]">{await t("ce.eyebrow")}</Link></li>
<li><Link href="/connexion" className="hover:text-[var(--color-karbe-bone)]">Stripe</Link></li>
</ul>
</div>
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">Légal</h4>
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">
{await t("footer.col.legal")}
</h4>
<ul className="mt-4 space-y-2 text-sm">
<li><Link href="/cgv" className="hover:text-[var(--color-karbe-bone)]">CGV</Link></li>
<li><Link href="/mentions-legales" className="hover:text-[var(--color-karbe-bone)]">Mentions légales</Link></li>
<li><Link href="/politique-de-confidentialite" className="hover:text-[var(--color-karbe-bone)]">Confidentialité</Link></li>
<li><Link href="/mentions-legales" className="hover:text-[var(--color-karbe-bone)]">Mentions</Link></li>
<li><Link href="/politique-de-confidentialite" className="hover:text-[var(--color-karbe-bone)]">RGPD</Link></li>
</ul>
</div>
</div>
<div className="border-t border-[var(--color-karbe-bone)]/10">
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-2 px-6 py-5 text-xs text-[var(--color-karbe-bone)]/55 sm:flex-row sm:px-8 lg:px-12">
<span>© {year} Karbé projet associatif numérique en Guyane.</span>
<span>© {year} Karbé {await t("footer.copyright")}</span>
<span className="font-mono text-[var(--color-karbe-bone)]/40">karbe.cosmolan.fr</span>
</div>
</div>

View file

@ -1,36 +1,50 @@
import Link from "next/link";
import { CarbetRiver } from "@/components/illustrations/CarbetRiver";
import { LocaleSwitcher } from "@/components/LocaleSwitcher";
import { isPluginEnabled } from "@/lib/plugins/server";
import { t } from "@/lib/i18n/server";
/**
* Hero plein écran. Plugin `landing-hero`.
* Pas de dépendance image externe illustration vectorielle inline.
* Hero plein écran. Plugin `landing-hero`. Texte i18n via t() server.
* Affiche le LocaleSwitcher en haut à droite si le plugin i18n est activé.
*/
export function HeroSection() {
export async function HeroSection() {
const i18nOn = await isPluginEnabled("i18n-fr-en");
const eyebrow = await t("hero.eyebrow");
const titleLine1 = await t("hero.titleLine1");
const titleAccent = await t("hero.titleAccent");
const subtitle = await t("hero.subtitle");
const ctaDiscover = await t("hero.ctaDiscover");
const ctaPropose = await t("hero.ctaPropose");
return (
<section className="relative isolate overflow-hidden bg-[var(--color-karbe-canopy-900)] text-[var(--color-karbe-bone)]">
{/* fond illustration */}
<div className="absolute inset-0 -z-10">
<CarbetRiver className="h-full w-full object-cover opacity-90 [object-position:center_60%]" />
{/* voile sombre pour lisibilité texte */}
<div className="absolute inset-0 bg-gradient-to-t from-[var(--color-karbe-canopy-900)] via-[var(--color-karbe-canopy-900)]/45 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-r from-[var(--color-karbe-canopy-900)]/70 via-transparent to-transparent" />
</div>
{i18nOn ? (
<div className="absolute right-6 top-6 z-10 sm:right-8 lg:right-12">
<LocaleSwitcher />
</div>
) : null}
<div className="mx-auto flex min-h-[78vh] max-w-6xl flex-col items-start justify-end gap-6 px-6 pb-16 pt-32 sm:px-8 sm:pb-20 sm:pt-40 lg:px-12">
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--color-karbe-bone)]/30 bg-[var(--color-karbe-bone)]/10 px-3 py-1 text-xs font-medium uppercase tracking-wider backdrop-blur-sm">
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-karbe-laterite-300)]" />
Marketplace solidaire sans commission sur le séjour
{eyebrow}
</span>
<h1 className="max-w-3xl font-serif text-4xl font-medium leading-[1.05] tracking-tight sm:text-5xl md:text-6xl lg:text-7xl">
Le karbé qui dort
{titleLine1}
<br />
<span className="text-[var(--color-karbe-laterite-300)] italic">vous attend</span>.
<span className="text-[var(--color-karbe-laterite-300)] italic">{titleAccent}</span>.
</h1>
<p className="max-w-xl text-base leading-relaxed text-[var(--color-karbe-bone)]/85 sm:text-lg">
Louez un carbet le long du Maroni, de l&apos;Approuague ou de l&apos;Oyapock. Le hamac est tendu,
la pirogue glisse, le silence est vrai. Pour quelques nuits, le fleuve vous appartient.
{subtitle}
</p>
<div className="mt-2 flex flex-wrap items-center gap-3">
@ -38,13 +52,13 @@ export function HeroSection() {
href="/carbets"
className="rounded-full bg-[var(--color-karbe-laterite-500)] px-6 py-3 text-sm font-semibold text-[var(--color-karbe-bone)] shadow-lg shadow-black/30 transition hover:bg-[var(--color-karbe-laterite-700)]"
>
Découvrir un carbet
{ctaDiscover}
</Link>
<Link
href="/espace-hote"
className="rounded-full border border-[var(--color-karbe-bone)]/40 bg-[var(--color-karbe-bone)]/5 px-6 py-3 text-sm font-medium text-[var(--color-karbe-bone)] backdrop-blur-sm transition hover:bg-[var(--color-karbe-bone)]/15"
>
Proposer le mien
{ctaPropose}
</Link>
</div>
</div>

View file

@ -1,42 +1,40 @@
import { CompassIcon, HammockIcon, HeartHandIcon } from "@/components/illustrations/Icons";
import { t } from "@/lib/i18n/server";
/**
* Section « Comment ça marche » 3 étapes côté voyageur.
* Section « Comment ça marche » 3 étapes côté voyageur, i18n via t() server.
*/
export function HowItWorksSection() {
const steps = [
export async function HowItWorksSection() {
const steps = await Promise.all([
{
icon: CompassIcon,
step: "01",
title: "Choisissez le fleuve",
body:
"Maroni, Approuague, Comté, Oyapock — chaque fleuve a son ambiance, son embarquement, ses carbets. Filtrez selon votre niveau d'aventure.",
title: await t("howItWorks.step1.title"),
body: await t("howItWorks.step1.body"),
},
{
icon: HammockIcon,
step: "02",
title: "Réservez le carbet",
body:
"Dates, capacité, durée de pirogue le cas échéant. Paiement sécurisé Stripe, reversé au loueur sans commission sur le séjour.",
title: await t("howItWorks.step2.title"),
body: await t("howItWorks.step2.body"),
},
{
icon: HeartHandIcon,
step: "03",
title: "Dormez vrai",
body:
"Le loueur (ou son partenaire) vous récupère au dégrad si besoin. Vous récupérez les clés du karbé, tendez le hamac, écoutez. Plus rien à faire.",
title: await t("howItWorks.step3.title"),
body: await t("howItWorks.step3.body"),
},
];
]);
return (
<section className="bg-gradient-to-b from-[var(--color-karbe-bone)] to-[var(--color-karbe-canopy-50)] py-20 sm:py-24">
<div className="mx-auto max-w-6xl px-6 sm:px-8 lg:px-12">
<div className="max-w-2xl">
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-[var(--color-karbe-canopy-700)]">
Comment ça marche
{await t("howItWorks.eyebrow")}
</span>
<h2 className="mt-3 font-serif text-3xl font-medium tracking-tight text-[var(--color-karbe-ink)] sm:text-4xl">
Trois étapes pour s&apos;échapper.
{await t("howItWorks.title")}
</h2>
</div>

View file

@ -1,9 +1,10 @@
import { t } from "@/lib/i18n/server";
/**
* Section témoignages 3 stubs avec noms/contextes plausibles Guyane.
* Les contenus sont éditorialisés par défaut, remplaçables via le plugin
* `content-pages` (Phase 4) qui fournira un store éditable depuis l'admin.
* Section témoignages i18n pour les headers, contenu des citations conservé
* tel quel (les vraies paroles restent en VO).
*/
export function TestimonialsSection() {
export async function TestimonialsSection() {
const items = [
{
name: "Émilie · CE Hôpital Cayenne",
@ -30,10 +31,10 @@ export function TestimonialsSection() {
<div className="mx-auto max-w-6xl px-6 sm:px-8 lg:px-12">
<div className="max-w-2xl">
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-[var(--color-karbe-canopy-700)]">
Pas de marketing
{await t("testimonials.eyebrow")}
</span>
<h2 className="mt-3 font-serif text-3xl font-medium tracking-tight text-[var(--color-karbe-ink)] sm:text-4xl">
Ils nous l&apos;ont dit comme ça.
{await t("testimonials.title")}
</h2>
</div>