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
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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue