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:
parent
efeea16467
commit
cf9da94bb5
15 changed files with 454 additions and 116 deletions
|
|
@ -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 (
|
||||
<html
|
||||
lang="fr"
|
||||
lang={locale}
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${cormorant.variable} h-full antialiased`}
|
||||
>
|
||||
<body
|
||||
|
|
@ -78,8 +82,10 @@ export default async function RootLayout({
|
|||
className="min-h-full flex flex-col font-sans"
|
||||
>
|
||||
<PluginProvider enabledKeys={enabledKeys}>
|
||||
<SeasonBanner />
|
||||
{children}
|
||||
<LocaleProvider locale={locale} messages={messages}>
|
||||
<SeasonBanner />
|
||||
{children}
|
||||
</LocaleProvider>
|
||||
</PluginProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
58
src/components/LocaleSwitcher.tsx
Normal file
58
src/components/LocaleSwitcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'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'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'ouvrent au public touriste le reste de
|
||||
l'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>
|
||||
|
|
|
|||
|
|
@ -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'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'envie, on choisit le carbet qui se rejoint en voiture pour un week-end facile,
|
||||
ou celui qu'on n'atteint qu'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'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'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'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>
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
|
|
|
|||
|
|
@ -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'Approuague ou de l'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>
|
||||
|
|
|
|||
|
|
@ -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'échapper.
|
||||
{await t("howItWorks.title")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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'ont dit comme ça.
|
||||
{await t("testimonials.title")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
36
src/lib/i18n/client.tsx
Normal file
36
src/lib/i18n/client.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import { createContext, useCallback, useContext, useMemo, type ReactNode } from "react";
|
||||
import type { Locale } from "./types";
|
||||
|
||||
type LocaleCtx = {
|
||||
locale: Locale;
|
||||
messages: Record<string, string>;
|
||||
};
|
||||
|
||||
const Ctx = createContext<LocaleCtx>({ locale: "fr", messages: {} });
|
||||
|
||||
export function LocaleProvider({
|
||||
locale,
|
||||
messages,
|
||||
children,
|
||||
}: {
|
||||
locale: Locale;
|
||||
messages: Record<string, string>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const value = useMemo(() => ({ locale, messages }), [locale, messages]);
|
||||
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
||||
}
|
||||
|
||||
export function useLocale(): Locale {
|
||||
return useContext(Ctx).locale;
|
||||
}
|
||||
|
||||
export function useT() {
|
||||
const { messages } = useContext(Ctx);
|
||||
return useCallback(
|
||||
(key: string) => messages[key] ?? key,
|
||||
[messages],
|
||||
);
|
||||
}
|
||||
70
src/lib/i18n/server.ts
Normal file
70
src/lib/i18n/server.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Plugin i18n-fr-en — résolution serveur de la locale courante.
|
||||
*
|
||||
* Ordre de priorité :
|
||||
* 1. cookie karbe-locale (posé par le LocaleSwitcher)
|
||||
* 2. en-tête Accept-Language
|
||||
* 3. DEFAULT_LOCALE (fr)
|
||||
*
|
||||
* Quand le plugin i18n est désactivé, le résolveur force FR.
|
||||
*/
|
||||
|
||||
import "server-only";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { DEFAULT_LOCALE, isLocale, LOCALE_COOKIE, type Locale } from "./types";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
|
||||
import frMessages from "@/messages/fr.json";
|
||||
import enMessages from "@/messages/en.json";
|
||||
|
||||
const DICTS: Record<Locale, Record<string, string>> = {
|
||||
fr: frMessages as Record<string, string>,
|
||||
en: enMessages as Record<string, string>,
|
||||
};
|
||||
|
||||
function parseAcceptLanguage(header: string | null): Locale | null {
|
||||
if (!header) return null;
|
||||
const items = header
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
for (const item of items) {
|
||||
const code = item.split(";")[0].slice(0, 2);
|
||||
if (code === "fr") return "fr";
|
||||
if (code === "en") return "en";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getLocale(): Promise<Locale> {
|
||||
// Plugin éteint → toujours FR (comportement legacy).
|
||||
if (!(await isPluginEnabled("i18n-fr-en"))) return "fr";
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const cookieValue = cookieStore.get(LOCALE_COOKIE)?.value;
|
||||
if (isLocale(cookieValue)) return cookieValue;
|
||||
|
||||
const h = await headers();
|
||||
const fromHeader = parseAcceptLanguage(h.get("accept-language"));
|
||||
return fromHeader ?? DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* t(key, locale?) : récupère un message du dico. Sans locale, on lit la locale
|
||||
* courante. Fallback : retourne la valeur FR puis la clé brute si rien.
|
||||
*/
|
||||
export async function t(key: string, locale?: Locale): Promise<string> {
|
||||
const lang = locale ?? (await getLocale());
|
||||
const dict = DICTS[lang];
|
||||
const fallback = DICTS.fr;
|
||||
return dict[key] ?? fallback[key] ?? key;
|
||||
}
|
||||
|
||||
/**
|
||||
* dict(locale) : retourne le dico complet pour passer en prop client-side.
|
||||
* Garde le bundle léger (le client ne reçoit qu'une langue à la fois).
|
||||
*/
|
||||
export async function dict(locale?: Locale): Promise<Record<string, string>> {
|
||||
const lang = locale ?? (await getLocale());
|
||||
return DICTS[lang];
|
||||
}
|
||||
13
src/lib/i18n/types.ts
Normal file
13
src/lib/i18n/types.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Plugin i18n-fr-en — types et constantes partagés.
|
||||
*/
|
||||
|
||||
export const LOCALES = ["fr", "en"] as const;
|
||||
export type Locale = (typeof LOCALES)[number];
|
||||
|
||||
export const DEFAULT_LOCALE: Locale = "fr";
|
||||
export const LOCALE_COOKIE = "karbe-locale";
|
||||
|
||||
export function isLocale(value: unknown): value is Locale {
|
||||
return value === "fr" || value === "en";
|
||||
}
|
||||
71
src/messages/en.json
Normal file
71
src/messages/en.json
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"site.tagline": "Karbé — riverside carbets of French Guiana",
|
||||
"site.description": "The not-for-profit marketplace to rent a carbet along the rivers of French Guiana.",
|
||||
|
||||
"hero.eyebrow": "Solidarity marketplace — zero commission on stays",
|
||||
"hero.titleLine1": "The sleeping karbé",
|
||||
"hero.titleAccent": "is waiting for you",
|
||||
"hero.subtitle": "Rent a carbet along the Maroni, Approuague or Oyapock. The hammock is up, the pirogue glides, the silence is real. For a few nights, the river is yours.",
|
||||
"hero.ctaDiscover": "Find a carbet",
|
||||
"hero.ctaPropose": "List mine",
|
||||
|
||||
"experiences.eyebrow": "Two ways to live Karbé",
|
||||
"experiences.title": "From riverbank to pirogue expedition.",
|
||||
"experiences.subtitle": "Pick the carbet you reach by car for an easy weekend — or the one you can only reach by pirogue, hours from the last village.",
|
||||
"experiences.roadFluve.tag": "🛣️ Road + river",
|
||||
"experiences.roadFluve.title": "The weekend carbet",
|
||||
"experiences.roadFluve.body": "Accessible by track from Kourou, Saint-Laurent or Régina. Park the car, grab your gear, you're there. For families, couples who want quiet without logistics, social committees booking short stays.",
|
||||
"experiences.roadFluve.b1": "1 to 3 nights typical",
|
||||
"experiences.roadFluve.b2": "Car or 4WD depending on the track",
|
||||
"experiences.roadFluve.b3": "Equipped carbets, swimming possible",
|
||||
"experiences.riverOnly.tag": "🛶 River expedition",
|
||||
"experiences.riverOnly.title": "The carbet you earn",
|
||||
"experiences.riverOnly.body": "No road. Board a pirogue at a 'dégrad', sometimes two or three hours upriver. For those who really want to sleep far — howler monkeys, sky without halo, the river five meters from your hammock.",
|
||||
"experiences.riverOnly.b1": "2 nights minimum recommended",
|
||||
"experiences.riverOnly.b2": "Pirogue with skipper (host or partner)",
|
||||
"experiences.riverOnly.b3": "Dry season recommended (July-November)",
|
||||
|
||||
"howItWorks.eyebrow": "How it works",
|
||||
"howItWorks.title": "Three steps to disappear.",
|
||||
"howItWorks.step1.title": "Choose the river",
|
||||
"howItWorks.step1.body": "Maroni, Approuague, Comté, Oyapock — each river has its mood, its embarkation point, its carbets. Filter by your level of adventure.",
|
||||
"howItWorks.step2.title": "Book the carbet",
|
||||
"howItWorks.step2.body": "Dates, party size, pirogue duration if needed. Secure Stripe payment, paid in full to the host — zero commission on the stay.",
|
||||
"howItWorks.step3.title": "Sleep for real",
|
||||
"howItWorks.step3.body": "The host (or their partner) picks you up at the dégrad if needed. You get the keys to the karbé, hang the hammock, listen. Nothing left to do.",
|
||||
|
||||
"ce.eyebrow": "For social committees",
|
||||
"ce.title": "Carbets sleep when you're not there.",
|
||||
"ce.titleAccent": "Let's share them.",
|
||||
"ce.body": "Karbé is built so that social committees who already own a carbet can reserve it for their members on selected weekends, then open it to public travellers the rest of the year. Zero commission on stays: the payment goes straight back to the committee.",
|
||||
"ce.kpi.commission": "commission on stays",
|
||||
"ce.kpi.ceFirst": "your members book first",
|
||||
"ce.kpi.publicOpen": "remaining dates feed the pot",
|
||||
"ce.kpi.noPaperwork": "Stripe charges and remits directly",
|
||||
"ce.cta": "Learn more for your committee",
|
||||
|
||||
"testimonials.eyebrow": "No marketing",
|
||||
"testimonials.title": "They told us, just like that.",
|
||||
|
||||
"footer.tagline": "Marketplace for riverside carbets of French Guiana. Solidarity with local social committees. Zero commission on stays.",
|
||||
"footer.col.discover": "Discover",
|
||||
"footer.col.propose": "Host",
|
||||
"footer.col.legal": "Legal",
|
||||
"footer.copyright": "non-profit digital project in French Guiana.",
|
||||
|
||||
"season.dry": "Dry season",
|
||||
"season.lowWater": "Low water",
|
||||
"season.wet": "Rainy season",
|
||||
"season.dry.message": "Optimal conditions: rivers navigable, roads in good shape, sunrise over burning water.",
|
||||
"season.lowWater.message": "Low water period: river-only carbets may not be reachable. Filter available on the search page.",
|
||||
"season.wet.message": "Frequent rain: the jungle is dense and alive, plan an adapted vehicle for road+river carbets.",
|
||||
|
||||
"access.roadAndRiver": "🛣️ Road + river",
|
||||
"access.riverOnly": "🛶 River expedition",
|
||||
"access.roadAndRiver.title": "Accessible by road and by river.",
|
||||
"access.riverOnly.title": "Reachable only by pirogue from a 'dégrad'.",
|
||||
|
||||
"language.switch": "Language",
|
||||
"language.fr": "Français",
|
||||
"language.en": "English"
|
||||
}
|
||||
71
src/messages/fr.json
Normal file
71
src/messages/fr.json
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"site.tagline": "Karbé — carbets fluviaux de Guyane",
|
||||
"site.description": "La marketplace solidaire pour louer un carbet le long des fleuves de Guyane.",
|
||||
|
||||
"hero.eyebrow": "Marketplace solidaire — sans commission sur le séjour",
|
||||
"hero.titleLine1": "Le karbé qui dort",
|
||||
"hero.titleAccent": "vous attend",
|
||||
"hero.subtitle": "Louez un carbet le long du Maroni, de l'Approuague ou de l'Oyapock. Le hamac est tendu, la pirogue glisse, le silence est vrai. Pour quelques nuits, le fleuve vous appartient.",
|
||||
"hero.ctaDiscover": "Découvrir un carbet",
|
||||
"hero.ctaPropose": "Proposer le mien",
|
||||
|
||||
"experiences.eyebrow": "Deux façons de vivre Karbé",
|
||||
"experiences.title": "Du bord du fleuve à l'expédition pirogue.",
|
||||
"experiences.subtitle": "Selon l'envie, on choisit le carbet qui se rejoint en voiture pour un week-end facile, ou celui qu'on n'atteint qu'en pirogue, à plusieurs heures du dernier village.",
|
||||
"experiences.roadFluve.tag": "🛣️ Route + fleuve",
|
||||
"experiences.roadFluve.title": "Le carbet du week-end",
|
||||
"experiences.roadFluve.body": "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.",
|
||||
"experiences.roadFluve.b1": "1 à 3 nuits typiques",
|
||||
"experiences.roadFluve.b2": "Voiture ou 4×4 selon la piste",
|
||||
"experiences.roadFluve.b3": "Carbets équipés, baignade possible",
|
||||
"experiences.riverOnly.tag": "🛶 Expédition fleuve",
|
||||
"experiences.riverOnly.title": "Le carbet qu'on mérite",
|
||||
"experiences.riverOnly.body": "Aucune route n'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'eau du fleuve à 5 mètres du hamac.",
|
||||
"experiences.riverOnly.b1": "2 nuits minimum recommandées",
|
||||
"experiences.riverOnly.b2": "Pirogue avec passeur (loueur ou partenaire)",
|
||||
"experiences.riverOnly.b3": "Saison sèche conseillée (juillet-novembre)",
|
||||
|
||||
"howItWorks.eyebrow": "Comment ça marche",
|
||||
"howItWorks.title": "Trois étapes pour s'échapper.",
|
||||
"howItWorks.step1.title": "Choisissez le fleuve",
|
||||
"howItWorks.step1.body": "Maroni, Approuague, Comté, Oyapock — chaque fleuve a son ambiance, son embarquement, ses carbets. Filtrez selon votre niveau d'aventure.",
|
||||
"howItWorks.step2.title": "Réservez le carbet",
|
||||
"howItWorks.step2.body": "Dates, capacité, durée de pirogue le cas échéant. Paiement sécurisé Stripe, reversé au loueur sans commission sur le séjour.",
|
||||
"howItWorks.step3.title": "Dormez vrai",
|
||||
"howItWorks.step3.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.",
|
||||
|
||||
"ce.eyebrow": "Pour comités d'entreprise",
|
||||
"ce.title": "Les carbets dorment quand vous n'y êtes pas.",
|
||||
"ce.titleAccent": "Partageons-les.",
|
||||
"ce.body": "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'ouvrent au public touriste le reste de l'année. Sans commission sur le séjour : le paiement revient intégralement au CE.",
|
||||
"ce.kpi.commission": "de commission sur le séjour",
|
||||
"ce.kpi.ceFirst": "vos membres réservent en priorité",
|
||||
"ce.kpi.publicOpen": "le reste des dates rentre dans le pot",
|
||||
"ce.kpi.noPaperwork": "Stripe encaisse et reverse direct",
|
||||
"ce.cta": "En savoir plus pour votre CE",
|
||||
|
||||
"testimonials.eyebrow": "Pas de marketing",
|
||||
"testimonials.title": "Ils nous l'ont dit comme ça.",
|
||||
|
||||
"footer.tagline": "Marketplace des carbets fluviaux de Guyane. Solidaire avec les CE locaux. Sans commission sur le séjour.",
|
||||
"footer.col.discover": "Découvrir",
|
||||
"footer.col.propose": "Proposer",
|
||||
"footer.col.legal": "Légal",
|
||||
"footer.copyright": "projet associatif numérique en Guyane.",
|
||||
|
||||
"season.dry": "Saison sèche",
|
||||
"season.lowWater": "Étiage",
|
||||
"season.wet": "Saison des pluies",
|
||||
"season.dry.message": "Conditions optimales : fleuves navigables, pistes route en bon état, lever de soleil sur l'eau brûlante.",
|
||||
"season.lowWater.message": "Étiage en cours : les carbets fleuve uniquement peuvent ne pas être accessibles. Filtre dispo en page recherche.",
|
||||
"season.wet.message": "Pluies fréquentes : la jungle est dense et vivante, prévoir un véhicule adapté pour les carbets route+fleuve.",
|
||||
|
||||
"access.roadAndRiver": "🛣️ Route + fleuve",
|
||||
"access.riverOnly": "🛶 Expédition fleuve",
|
||||
"access.roadAndRiver.title": "Accessible par la route et par le fleuve.",
|
||||
"access.riverOnly.title": "Accessible uniquement par pirogue depuis un dégrad.",
|
||||
|
||||
"language.switch": "Langue",
|
||||
"language.fr": "Français",
|
||||
"language.en": "English"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue