ordinarthur 4f3417fcef feat(landing): support i18n EN avec routing /en/* (Astro i18n natif)
Active Astro i18n avec `defaultLocale: fr` et `prefixDefaultLocale: false`
— les URLs FR restent canoniques à la racine, l'EN vit sous `/en/*` pour
ne pas casser le SEO existant.

Architecture :
- `src/i18n/{types,fr,en,index}.ts` — dico FR fait foi (Dict inféré),
  EN doit matcher la shape ; helpers `getTranslations(locale)` et
  `getAlternateUrl()` pour le language switcher.
- `Layout.astro` lit `Astro.currentLocale`, propage `locale` aux
  composants React, set `<html lang>`, og:locale + alt, hreflang.
- `SiteHeader` expose un lien switcher FR↔EN qui préserve la page.
- Toutes les sections (Hero, Stats, Promise, HowItWorks, Gamification,
  AutoBanking, Legal, Pricing, FAQ, FinalCTA, Footnotes, SiteFooter)
  acceptent une prop `locale` et tirent leurs chaînes du dico.

Pages EN créées :
- `/en/` — home complète
- `/en/blog`, `/en/changelog` — chrome traduit, contenu reste dans la
  langue de rédaction (les .md changelog + posts API sont FR)
- `/en/cgv`, `/en/mentions-legales`, `/en/confidentialite` — résumés
  courts ; la version juridiquement contraignante reste la FR (droit
  français, conformité GDPR/LCEN/LME).

Sitemap mis à jour avec entrées FR/EN + `xhtml:link rel="alternate"`.

Pas de détection auto via Accept-Language pour l'instant — le switcher
header suffit en V1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 13:29:57 +02:00

171 lines
6.8 KiB
TypeScript

import { Brand, Button, Eyebrow, Gem, cn } from "@rubis/ui";
import { Check } from "lucide-react";
import { getTranslations, type Locale } from "../../i18n";
const APP_URL = "https://app.rubis.pro";
type HeroProps = {
locale?: Locale;
};
export function Hero({ locale = "fr" }: HeroProps) {
const t = getTranslations(locale).hero;
const pricingHref = locale === "fr" ? "#pricing" : "/en/#pricing";
return (
<section className="relative overflow-hidden">
<div
aria-hidden
className="pointer-events-none absolute -top-40 -right-40 size-[480px] rounded-full"
style={{
background:
"radial-gradient(circle, var(--color-rubis-glow) 0%, transparent 65%)",
}}
/>
<div className="max-w-[1180px] mx-auto px-5 sm:px-8 pt-16 pb-20 lg:pt-24 lg:pb-28 grid lg:grid-cols-[1.1fr_1fr] gap-12 lg:gap-16 items-center relative">
<div>
<Eyebrow>{t.eyebrow}</Eyebrow>
<h1 className="mt-5 font-display font-extrabold text-ink leading-[1.05] tracking-[-0.03em] text-[44px] sm:text-[56px] lg:text-[64px] max-w-[680px]">
{t.title_a}
<em>{t.title_b}</em>
{t.title_c}
</h1>
<p className="mt-6 text-[18px] sm:text-[19px] leading-relaxed text-ink-2 max-w-[580px] md:text-justify hyphens-auto">
{t.body_a}
<b className="text-ink">{t.body_bold}</b>
{t.body_c}
</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Button asChild size="lg">
<a href={APP_URL}>{t.ctaPrimary}</a>
</Button>
<Button asChild variant="secondary" size="lg">
<a href={pricingHref}>{t.ctaSecondary}</a>
</Button>
</div>
<div className="mt-7 flex flex-wrap items-center gap-x-3 gap-y-2 text-[13px] text-ink-3">
<span className="inline-flex items-center gap-1.5">
<Check size={14} className="text-rubis" aria-hidden />
{t.feature1}
</span>
<span className="inline-flex items-center gap-3">
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" />
{t.feature2}
</span>
<span className="inline-flex items-center gap-3">
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" />
{t.feature3}
</span>
</div>
</div>
<div className="relative w-full max-w-[480px] mx-auto lg:ml-auto lg:mr-0">
<div
className={cn(
"bg-white border border-line rounded-card shadow-card overflow-hidden",
)}
>
<div className="flex items-center justify-between px-6 sm:px-7 lg:px-8 py-3.5 border-b border-line bg-cream/40">
<Brand withSuffix gemSize={18} />
<span className="text-[11px] uppercase tracking-[0.08em] font-semibold text-ink-3">
{t.mockDashboard}
</span>
</div>
<div className="p-6 sm:p-7 lg:p-8">
<div className="flex items-center gap-4 pb-5 border-b border-line">
<Gem size={56} glow />
<div>
<div className="font-display font-bold text-[32px] tracking-[-0.022em] leading-none text-ink">
124 {t.mockRubis}
</div>
<div className="mt-1.5 text-[14px] text-ink-2">
{t.mockHoursPrefix}
<b className="text-ink">{t.mockHoursValue}</b>
{t.mockHoursSuffix}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-5 mt-5">
<div>
<div className="text-[10.5px] uppercase tracking-[0.06em] font-semibold text-ink-3">
{t.mockKpiCollected}
</div>
<div className="mt-1.5 font-display font-bold text-[22px] tracking-[-0.015em] text-ink tabular-nums">
{locale === "fr" ? "14 320 €" : "€14,320"}
</div>
<div className="mt-1 text-[11.5px] text-rubis font-medium">
{t.mockKpiCollectedDelta}
</div>
</div>
<div>
<div className="text-[10.5px] uppercase tracking-[0.06em] font-semibold text-ink-3">
{t.mockKpiDso}
</div>
<div className="mt-1.5 font-display font-bold text-[22px] tracking-[-0.015em] text-ink tabular-nums">
{locale === "fr" ? "38 j" : "38 d"}
</div>
<div className="mt-1 text-[11.5px] text-rubis font-medium">
{t.mockKpiDsoDelta}
</div>
</div>
</div>
<div className="mt-5 pt-4 border-t border-dashed border-line">
<div className="text-[11px] uppercase tracking-[0.08em] font-semibold text-ink-3 mb-2.5">
{t.mockToday}
</div>
<ul className="space-y-2 text-[13px]">
<li className="flex items-baseline justify-between gap-3">
<span className="text-ink-2">
{t.mockActivity1Pre}
<b className="text-ink">{t.mockActivity1Name}</b>
</span>
<time className="text-ink-3 tabular-nums text-[11.5px]">11:14</time>
</li>
<li className="flex items-baseline justify-between gap-3">
<span className="text-ink-2">
{t.mockActivity2Pre}
<b className="text-ink">{t.mockActivity2Name}</b>
{t.mockActivity2Post}
</span>
<time className="text-ink-3 tabular-nums text-[11.5px]">10:02</time>
</li>
<li className="flex items-baseline justify-between gap-3">
<span className="text-ink-2">{t.mockActivity3}</span>
<time className="text-ink-3 tabular-nums text-[11.5px]">09:48</time>
</li>
</ul>
</div>
</div>
</div>
<div className="absolute -bottom-3 left-4 sm:left-6 bg-ink text-cream rounded-full px-4 py-2 text-[12.5px] font-semibold flex items-center gap-1.5 shadow-card">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
{t.mockBadge}
</div>
</div>
</div>
</section>
);
}