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.
58 lines
2 KiB
TypeScript
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>
|
|
);
|
|
}
|