karbe/src/components/LocaleSwitcher.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

58 lines
2 KiB
TypeScript

"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>
);
}