feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup

- Restructured docker-compose for Nginx Proxy Manager (no custom nginx)
- Added domain wordly.art configuration
- Added Prometheus + Grafana monitoring stack with pre-configured dashboards
- Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation)
- Added alert rules for backend, system, and Docker metrics
- Updated deployment guide for NPM + IONOS DNS homelab setup
- Added marketing plan document
- PDF translator and watermark support
- Enhanced middleware, routes, and translator modules

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:43:28 +02:00
parent 16ac7ca2b9
commit ce8e150a61
110 changed files with 6935 additions and 4301 deletions

View File

@@ -15,6 +15,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { API_BASE } from "@/lib/config";
import { ANNUAL_DISCOUNT_PERCENT } from "@/lib/pricing";
import { useI18n } from "@/lib/i18n";
/* ─────────────────────────────────────────────
Types
@@ -50,11 +51,13 @@ interface CreditPackage {
/* ─────────────────────────────────────────────
Static plan data (fallback + SSR-friendly)
Names, descriptions, features, badges use i18n keys
resolved via t() in the render layer.
───────────────────────────────────────────── */
const STATIC_PLANS: Plan[] = [
{
id: "free",
name: "Gratuit",
name: "pricing.plans.free.name",
price_monthly: 0,
price_yearly: 0,
docs_per_month: 5,
@@ -63,21 +66,21 @@ const STATIC_PLANS: Plan[] = [
max_chars_per_month: 50_000,
providers: ["google"],
features: [
"5 documents / mois",
"Jusqu'à 15 pages par document",
"Google Traduction inclus",
"Toutes les langues (130+)",
"Support communautaire",
"pricing.plans.free.feat1",
"pricing.plans.free.feat2",
"pricing.plans.free.feat3",
"pricing.plans.free.feat4",
"pricing.plans.free.feat5",
],
ai_translation: false,
api_access: false,
priority_processing: false,
popular: false,
description: "Parfait pour découvrir l'application",
description: "pricing.plans.free.description",
},
{
id: "starter",
name: "Starter",
name: "pricing.plans.starter.name",
price_monthly: 9.00,
price_yearly: 86.40,
docs_per_month: 50,
@@ -86,22 +89,22 @@ const STATIC_PLANS: Plan[] = [
max_chars_per_month: 500_000,
providers: ["google", "deepl"],
features: [
"50 documents / mois",
"Jusqu'à 50 pages par document",
"Google Traduction + DeepL",
"Fichiers jusqu'à 10 Mo",
"Support par e-mail",
"Historique 30 jours",
"pricing.plans.starter.feat1",
"pricing.plans.starter.feat2",
"pricing.plans.starter.feat3",
"pricing.plans.starter.feat4",
"pricing.plans.starter.feat5",
"pricing.plans.starter.feat6",
],
ai_translation: false,
api_access: false,
priority_processing: false,
popular: false,
description: "Pour les particuliers et petits projets",
description: "pricing.plans.starter.description",
},
{
id: "pro",
name: "Pro",
name: "pricing.plans.pro.name",
price_monthly: 19.00,
price_yearly: 182.40,
docs_per_month: 200,
@@ -110,27 +113,27 @@ const STATIC_PLANS: Plan[] = [
max_chars_per_month: 2_000_000,
providers: ["google", "deepl", "openrouter"],
features: [
"200 documents / mois",
"Jusqu'à 200 pages par document",
"Traduction IA Essentielle (DeepSeek V3.2)",
"Google Traduction + DeepL",
"Fichiers jusqu'à 25 Mo",
"Glossaires personnalisés",
"Support prioritaire",
"Historique 90 jours",
"pricing.plans.pro.feat1",
"pricing.plans.pro.feat2",
"pricing.plans.pro.feat3",
"pricing.plans.pro.feat4",
"pricing.plans.pro.feat5",
"pricing.plans.pro.feat6",
"pricing.plans.pro.feat7",
"pricing.plans.pro.feat8",
],
ai_translation: true,
ai_tier: "essential",
api_access: false,
priority_processing: true,
popular: true,
highlight: "Le plus populaire",
description: "Pour les professionnels et équipes en croissance",
badge: "POPULAIRE",
highlight: "pricing.plans.pro.highlight",
description: "pricing.plans.pro.description",
badge: "pricing.plans.pro.badge",
},
{
id: "business",
name: "Business",
name: "pricing.plans.business.name",
price_monthly: 49.00,
price_yearly: 470.40,
docs_per_month: 1000,
@@ -139,16 +142,16 @@ const STATIC_PLANS: Plan[] = [
max_chars_per_month: 10_000_000,
providers: ["google", "deepl", "openrouter", "openrouter_premium", "openai"],
features: [
"1 000 documents / mois",
"Jusqu'à 500 pages par document",
"IA Essentielle + Premium (Claude Haiku)",
"Tous les fournisseurs de traduction",
"Fichiers jusqu'à 50 Mo",
"Accès API (10 000 appels/mois)",
"Webhooks de notification",
"Support dédié",
"Historique 1 an",
"Analytiques avancées",
"pricing.plans.business.feat1",
"pricing.plans.business.feat2",
"pricing.plans.business.feat3",
"pricing.plans.business.feat4",
"pricing.plans.business.feat5",
"pricing.plans.business.feat6",
"pricing.plans.business.feat7",
"pricing.plans.business.feat8",
"pricing.plans.business.feat9",
"pricing.plans.business.feat10",
],
ai_translation: true,
ai_tier: "premium",
@@ -156,11 +159,11 @@ const STATIC_PLANS: Plan[] = [
priority_processing: true,
team_seats: 5,
popular: false,
description: "Pour les équipes et organisations",
description: "pricing.plans.business.description",
},
{
id: "enterprise",
name: "Entreprise",
name: "pricing.plans.enterprise.name",
price_monthly: -1,
price_yearly: -1,
docs_per_month: -1,
@@ -169,14 +172,14 @@ const STATIC_PLANS: Plan[] = [
max_chars_per_month: -1,
providers: ["all"],
features: [
"Documents illimités",
"Tous les modèles IA (GPT-5, Claude Opus 4.6…)",
"Déploiement on-premise ou cloud dédié",
"SLA 99,9 % garanti",
"Support 24/7 dédié",
"Marque blanche (white-label)",
"Équipes illimitées",
"Intégrations sur mesure",
"pricing.plans.enterprise.feat1",
"pricing.plans.enterprise.feat2",
"pricing.plans.enterprise.feat3",
"pricing.plans.enterprise.feat4",
"pricing.plans.enterprise.feat5",
"pricing.plans.enterprise.feat6",
"pricing.plans.enterprise.feat7",
"pricing.plans.enterprise.feat8",
],
ai_translation: true,
ai_tier: "custom",
@@ -184,8 +187,8 @@ const STATIC_PLANS: Plan[] = [
priority_processing: true,
team_seats: -1,
popular: false,
description: "Solutions sur mesure pour grandes organisations",
badge: "SUR DEVIS",
description: "pricing.plans.enterprise.description",
badge: "pricing.plans.enterprise.badge",
},
];
@@ -215,7 +218,7 @@ const PLAN_COLORS: Record<string, { gradient: string; border: string; badge: str
enterprise: { gradient: "from-amber-600 to-amber-700", border: "border-amber-700/50", badge: "bg-amber-600", button: "bg-amber-600 hover:bg-amber-500" },
};
/** Évite le flash des tarifs statiques (STATIC_PLANS) avant la réponse API au refresh. */
/** Avoids flash of static prices before the API responds on refresh. */
function PricingDataSkeleton() {
return (
<>
@@ -257,40 +260,20 @@ function PricingDataSkeleton() {
}
const FAQS = [
{
q: "Puis-je changer de forfait à tout moment ?",
a: "Oui. Le passage à un forfait supérieur est immédiat et proratisé. La rétrogradation prend effet à la fin de la période en cours.",
},
{
q: "Qu'est-ce que la « Traduction IA Essentielle » ?",
a: "C'est notre moteur IA basé sur DeepSeek V3.2 via OpenRouter. Il comprend le contexte de vos documents, préserve la mise en page et gère les termes techniques bien mieux qu'une traduction classique.",
},
{
q: "Quelle est la différence entre IA Essentielle et IA Premium ?",
a: "L'IA Essentielle utilise DeepSeek V3.2 (excellent rapport qualité/prix). L'IA Premium utilise Claude 3.5 Haiku d'Anthropic, plus précis sur les documents juridiques, médicaux et techniques complexes.",
},
{
q: "Mes documents sont-ils conservés après traduction ?",
a: "Les fichiers traduits sont disponibles selon votre forfait (30 jours Starter, 90 jours Pro, 1 an Business). Ils sont chiffrés au repos et en transit.",
},
{
q: "Que se passe-t-il si je dépasse mon quota mensuel ?",
a: "Vous pouvez acheter des crédits supplémentaires à l'unité, ou upgrader votre forfait. Vous êtes notifié à 80 % d'utilisation.",
},
{
q: "Y a-t-il une version d'essai gratuite des forfaits payants ?",
a: "Le forfait Gratuit est permanent et sans carte bancaire. Pour les forfaits Pro et Business, contactez-nous pour un accès d'essai de 14 jours.",
},
{
q: "Quels formats de fichiers sont supportés ?",
a: "Word (.docx), Excel (.xlsx/.xls), PowerPoint (.pptx), et bientôt PDF. Tous les plans supportent les mêmes formats.",
},
{ q: "pricing.faq.q1", a: "pricing.faq.a1" },
{ q: "pricing.faq.q2", a: "pricing.faq.a2" },
{ q: "pricing.faq.q3", a: "pricing.faq.a3" },
{ q: "pricing.faq.q4", a: "pricing.faq.a4" },
{ q: "pricing.faq.q5", a: "pricing.faq.a5" },
{ q: "pricing.faq.q6", a: "pricing.faq.a6" },
{ q: "pricing.faq.q7", a: "pricing.faq.a7" },
];
/* ─────────────────────────────────────────────
Main component
───────────────────────────────────────────── */
export default function PricingPage() {
const { t } = useI18n();
const router = useRouter();
const searchParams = useSearchParams();
const planFromUrl = searchParams.get("plan");
@@ -303,11 +286,11 @@ export default function PricingPage() {
const [loadingPlanId, setLoadingPlanId] = useState<string | null>(null);
const [toastMsg, setToastMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [annualDiscountPercent, setAnnualDiscountPercent] = useState(ANNUAL_DISCOUNT_PERCENT);
/** Tant que false : on naffiche pas STATIC_PLANS (évite le flash de faux prix au refresh). */
/** Until false: don't show STATIC_PLANS (avoids flash of stale prices on refresh). */
const [pricingLoaded, setPricingLoaded] = useState(false);
useEffect(() => {
// Fetch live plans — backend returns prices set by admin (pas de cache navigateur)
// Fetch live plans — backend returns prices set by admin (no browser cache)
const plansUrl = `${API_BASE}/api/v1/auth/plans`;
fetch(plansUrl, { cache: "no-store" })
.then(async (r) => {
@@ -327,7 +310,7 @@ export default function PricingPage() {
})
.catch((err) => {
if (process.env.NODE_ENV === "development") {
console.warn("[pricing] Impossible de charger /api/v1/auth/plans — affichage des tarifs par défaut.", err);
console.warn("[pricing] Cannot load /api/v1/auth/plans — showing default prices.", err);
}
})
.finally(() => {
@@ -354,7 +337,7 @@ export default function PricingPage() {
if (planFromUrl && isLoggedIn && plans.length > 0 && currentPlan !== null) {
const targetPlan = plans.find(p => p.id === planFromUrl);
if (targetPlan && targetPlan.price_monthly > 0 && currentPlan !== planFromUrl) {
// Directly initiate checkout — no more redirect to /settings/subscription
// Directly initiate checkout
handleSubscribe(planFromUrl);
}
}
@@ -399,7 +382,7 @@ export default function PricingPage() {
const backendMessage = data.message ?? data.error ?? data.data?.error;
if (!res.ok) {
throw new Error(backendMessage || "Erreur lors de la création du paiement.");
throw new Error(backendMessage || t('pricing.toast.paymentError'));
}
if (url) {
@@ -408,11 +391,11 @@ export default function PricingPage() {
// Demo mode: Stripe not yet configured
setToastMsg({
type: 'ok',
text: `Mode démo — Stripe n'est pas encore configuré. En production, vous seriez redirigé vers le paiement pour activer le forfait ${planId}.`,
text: t('pricing.toast.demo', { planId }),
});
}
} catch (error) {
const message = error instanceof Error ? error.message : "Erreur réseau. Veuillez réessayer.";
const message = error instanceof Error ? error.message : t('pricing.toast.networkError');
setToastMsg({ type: 'err', text: message });
} finally {
setLoadingPlanId(null);
@@ -429,21 +412,21 @@ export default function PricingPage() {
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Retour
{t('pricing.nav.back')}
</button>
<div className="flex items-center gap-2">
<Link
href={isLoggedIn ? "/dashboard" : "/"}
className="px-3 py-1.5 rounded-lg text-sm bg-secondary hover:bg-secondary/80 text-secondary-foreground transition-colors"
>
{isLoggedIn ? "Dashboard" : "Accueil"}
{isLoggedIn ? "Dashboard" : t('pricing.nav.home')}
</Link>
{isLoggedIn && (
<Link
href="/settings/subscription"
href="/dashboard/profile"
className="px-3 py-1.5 rounded-lg text-sm bg-accent/80 hover:bg-accent text-accent-foreground transition-colors"
>
Mon abonnement
{t('pricing.nav.mySubscription')}
</Link>
)}
</div>
@@ -468,14 +451,13 @@ export default function PricingPage() {
<div className="max-w-7xl mx-auto px-4 pt-20 pb-12 text-center">
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-accent/10 border border-accent/20 text-accent text-sm font-medium mb-6">
<Cpu className="w-4 h-4" />
Modèles IA mis à jour Mars 2026
{t('pricing.header.badge')}
</div>
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-4 bg-gradient-to-r from-foreground via-accent/80 to-accent bg-clip-text text-transparent">
Un forfait pour chaque besoin
{t('pricing.header.title')}
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto mb-8">
Traduisez vos documents Word, Excel et PowerPoint en conservant
la mise en page originale. Sans jamais saisir de clé API.
{t('pricing.header.subtitle')}
</p>
{/* Monthly / Yearly toggle */}
@@ -487,7 +469,7 @@ export default function PricingPage() {
!isYearly ? "bg-foreground text-background shadow" : "text-muted-foreground hover:text-foreground"
)}
>
Mensuel
{t('pricing.billing.monthly')}
</button>
<button
onClick={() => setIsYearly(true)}
@@ -496,7 +478,7 @@ export default function PricingPage() {
isYearly ? "bg-foreground text-background shadow" : "text-muted-foreground hover:text-foreground"
)}
>
Annuel
{t('pricing.billing.yearly')}
<span className="bg-success text-success-foreground text-xs px-1.5 py-0.5 rounded-full">
{annualDiscountPercent} %
</span>
@@ -504,7 +486,7 @@ export default function PricingPage() {
</div>
</div>
{/* ── Plan cards (+ comparaison + crédits) : squelette jusquà lAPI pour éviter le flash des tarifs statiques */}
{/* ── Plan cards (+ comparison + credits): skeleton until API responds to avoid stale price flash ── */}
<div className="max-w-7xl mx-auto px-4 pb-20">
{!pricingLoaded ? (
<PricingDataSkeleton />
@@ -534,13 +516,13 @@ export default function PricingPage() {
"absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full text-xs font-bold text-white",
colors.badge
)}>
{plan.badge}
{t(plan.badge)}
</div>
)}
{isCurrent && (
<div className="absolute -top-3 right-4 px-3 py-1 rounded-full text-xs font-bold bg-emerald-600 text-white flex items-center gap-1">
<BadgeCheck className="w-3 h-3" /> Mon forfait
<BadgeCheck className="w-3 h-3" /> {t('pricing.card.myPlan')}
</div>
)}
@@ -550,27 +532,27 @@ export default function PricingPage() {
<div className="p-1.5 bg-white/10 rounded-lg">
<Icon className="w-5 h-5 text-white" />
</div>
<span className="font-bold text-white">{plan.name}</span>
<span className="font-bold text-white">{t(plan.name)}</span>
</div>
{isEnterprise ? (
<div className="text-3xl font-bold text-white">Sur devis</div>
<div className="text-3xl font-bold text-white">{t('pricing.card.onRequest')}</div>
) : price === 0 ? (
<div className="text-3xl font-bold text-white">Gratuit</div>
<div className="text-3xl font-bold text-white">{t('pricing.card.free')}</div>
) : (
<div className="flex items-end gap-1">
<span className="text-3xl font-bold text-white">{price} </span>
<span className="text-white/70 text-sm pb-1">/mois</span>
<span className="text-white/70 text-sm pb-1">{t('pricing.card.perMonth')}</span>
</div>
)}
{isYearly && plan.price_yearly > 0 && (
<div className="text-white/70 text-xs mt-1">
Facturé {plan.price_yearly.toFixed(2)} / an
{t('pricing.card.billedYearly', { price: plan.price_yearly.toFixed(2) })}
</div>
)}
<p className="text-white/60 text-xs mt-2">{plan.description}</p>
<p className="text-white/60 text-xs mt-2">{t(plan.description || '')}</p>
</div>
{/* Features */}
@@ -579,21 +561,21 @@ export default function PricingPage() {
<div className="grid grid-cols-1 gap-2 mb-3">
<Stat
icon={<FileText className="w-3.5 h-3.5" />}
label="Documents"
value={plan.docs_per_month === -1 ? "Illimité" : `${plan.docs_per_month} / mois`}
label={t('pricing.card.documents')}
value={plan.docs_per_month === -1 ? t('pricing.card.unlimited') : `${plan.docs_per_month} ${t('pricing.card.perMonthStat')}`}
/>
<Stat
icon={<Layers className="w-3.5 h-3.5" />}
label="Pages max"
value={plan.max_pages_per_doc === -1 ? "Illimité" : `${plan.max_pages_per_doc} p / doc`}
label={t('pricing.card.pagesMax')}
value={plan.max_pages_per_doc === -1 ? t('pricing.card.unlimited') : `${plan.max_pages_per_doc} ${t('pricing.card.perDoc')}`}
/>
{plan.ai_translation && (
<Stat
icon={<Brain className="w-3.5 h-3.5 text-violet-400" />}
label="Traduction IA"
label={t('pricing.card.aiTranslation')}
value={
plan.ai_tier === "essential" ? "Essentielle" :
plan.ai_tier === "premium" ? "Essentielle + Premium" : "Sur mesure"
plan.ai_tier === "essential" ? t('pricing.card.aiEssential') :
plan.ai_tier === "premium" ? t('pricing.card.aiEssentialPremium') : t('pricing.card.aiCustom')
}
highlight
/>
@@ -604,7 +586,7 @@ export default function PricingPage() {
{plan.features.map((feat, i) => (
<div key={i} className="flex items-start gap-2">
<Check className="w-4 h-4 text-emerald-500 flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground text-xs leading-snug">{feat}</span>
<span className="text-muted-foreground text-xs leading-snug">{t(feat)}</span>
</div>
))}
</div>
@@ -616,9 +598,9 @@ export default function PricingPage() {
<Button
variant="outline"
className="w-full border-success/50 text-success hover:bg-success/10"
onClick={() => router.push("/settings/subscription")}
onClick={() => router.push("/dashboard/profile")}
>
<BadgeCheck className="w-4 h-4 mr-1" /> Gérer mon forfait
<BadgeCheck className="w-4 h-4 me-1" /> {t('pricing.card.managePlan')}
</Button>
) : plan.id === "free" && !currentPlan ? (
<Button
@@ -626,7 +608,7 @@ export default function PricingPage() {
className="w-full border-border text-muted-foreground hover:bg-muted/30"
onClick={() => router.push("/auth/register")}
>
Commencer gratuitement
{t('pricing.card.startFree')}
</Button>
) : (
<button
@@ -644,11 +626,11 @@ export default function PricingPage() {
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Traitement
{t('pricing.card.processing')}
</>
) : (
<>
{isEnterprise ? "Nous contacter" : "Choisir ce forfait"}
{isEnterprise ? t('pricing.card.contactUs') : t('pricing.card.choosePlan')}
<ArrowRight className="w-4 h-4" />
</>
)}
@@ -662,33 +644,33 @@ export default function PricingPage() {
{/* ── Feature comparison table ── */}
<div className="mt-20">
<h2 className="text-3xl font-bold text-center mb-2">Comparaison détaillée</h2>
<p className="text-muted-foreground text-center mb-10">Tout ce qui est inclus dans chaque forfait</p>
<h2 className="text-3xl font-bold text-center mb-2">{t('pricing.comparison.title')}</h2>
<p className="text-muted-foreground text-center mb-10">{t('pricing.comparison.subtitle')}</p>
<div className="overflow-x-auto rounded-2xl border border-border/40">
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/60 border-b border-border/40">
<th className="text-left py-4 px-6 text-muted-foreground font-medium">Fonctionnalité</th>
<th className="text-start py-4 px-6 text-muted-foreground font-medium">{t('pricing.comparison.feature')}</th>
{plans.slice(0, 4).map((p) => (
<th key={p.id} className={cn("py-4 px-4 text-center font-medium", p.popular ? "text-accent" : "text-muted-foreground")}>
{p.name}
{t(p.name)}
</th>
))}
</tr>
</thead>
<tbody>
{[
{ label: "Documents / mois", vals: plans.slice(0,4).map(p => p.docs_per_month === -1 ? "∞" : String(p.docs_per_month)) },
{ label: "Pages max / document", vals: plans.slice(0,4).map(p => p.max_pages_per_doc === -1 ? "∞" : String(p.max_pages_per_doc)) },
{ label: "Taille max fichier", vals: plans.slice(0,4).map(p => p.max_file_size_mb === -1 ? "∞" : `${p.max_file_size_mb} Mo`) },
{ label: "Google Traduction", vals: plans.slice(0,4).map(() => true) },
{ label: "DeepL", vals: plans.slice(0,4).map(p => p.providers.includes("deepl") || p.providers.includes("all")) },
{ label: "Traduction IA Essentielle", vals: plans.slice(0,4).map(p => p.ai_translation && (p.ai_tier === "essential" || p.ai_tier === "premium" || p.ai_tier === "custom")) },
{ label: "Traduction IA Premium", vals: plans.slice(0,4).map(p => p.ai_translation && (p.ai_tier === "premium" || p.ai_tier === "custom")) },
{ label: "Accès API", vals: plans.slice(0,4).map(p => p.api_access) },
{ label: "Traitement prioritaire", vals: plans.slice(0,4).map(p => p.priority_processing) },
{ label: "Support", vals: ["Communauté", "E-mail", "Prioritaire", "Dédié"] },
{ label: t('pricing.comparison.docsPerMonth'), vals: plans.slice(0,4).map(p => p.docs_per_month === -1 ? "∞" : String(p.docs_per_month)) },
{ label: t('pricing.comparison.pagesMaxPerDoc'), vals: plans.slice(0,4).map(p => p.max_pages_per_doc === -1 ? "∞" : String(p.max_pages_per_doc)) },
{ label: t('pricing.comparison.maxFileSize'), vals: plans.slice(0,4).map(p => p.max_file_size_mb === -1 ? "∞" : `${p.max_file_size_mb} ${t('pricing.comparison.mb')}`) },
{ label: t('pricing.comparison.googleTranslation'), vals: plans.slice(0,4).map(() => true) },
{ label: t('pricing.comparison.deepl'), vals: plans.slice(0,4).map(p => p.providers.includes("deepl") || p.providers.includes("all")) },
{ label: t('pricing.comparison.aiEssential'), vals: plans.slice(0,4).map(p => p.ai_translation && (p.ai_tier === "essential" || p.ai_tier === "premium" || p.ai_tier === "custom")) },
{ label: t('pricing.comparison.aiPremium'), vals: plans.slice(0,4).map(p => p.ai_translation && (p.ai_tier === "premium" || p.ai_tier === "custom")) },
{ label: t('pricing.comparison.apiAccess'), vals: plans.slice(0,4).map(p => p.api_access) },
{ label: t('pricing.comparison.priorityProcessing'), vals: plans.slice(0,4).map(p => p.priority_processing) },
{ label: t('pricing.comparison.support'), vals: [t('pricing.comparison.support.community'), t('pricing.comparison.support.email'), t('pricing.comparison.support.priority'), t('pricing.comparison.support.dedicated')] },
].map((row, i) => (
<tr key={i} className={cn("border-b border-border/20", i % 2 === 0 ? "bg-muted/20" : "")}>
<td className="py-3 px-6 text-muted-foreground">{row.label}</td>
@@ -714,10 +696,10 @@ export default function PricingPage() {
{/* ── Credits ── */}
<div className="mt-20">
<h2 className="text-3xl font-bold text-center mb-2">Crédits supplémentaires</h2>
<h2 className="text-3xl font-bold text-center mb-2">{t('pricing.credits.title')}</h2>
<p className="text-muted-foreground text-center mb-8">
Besoin de plus ? Achetez des crédits à l'unité, sans abonnement.
<span className="text-muted-foreground/70"> 1 crédit = 1 page traduite.</span>
{t('pricing.credits.subtitle')}
<span className="text-muted-foreground/70"> {t('pricing.credits.perPage')}</span>
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-3xl mx-auto">
{credits.map((pkg, i) => (
@@ -732,15 +714,15 @@ export default function PricingPage() {
>
{pkg.popular && (
<div className="absolute -top-2.5 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-accent text-accent-foreground text-xs rounded-full font-bold">
Le meilleur rapport
{t('pricing.credits.bestValue')}
</div>
)}
<div className="text-2xl font-bold text-foreground">{pkg.credits}</div>
<div className="text-muted-foreground text-xs mb-3">crédits</div>
<div className="text-muted-foreground text-xs mb-3">{t('pricing.credits.unit')}</div>
<div className="text-xl font-bold text-foreground">{pkg.price} </div>
<div className="text-muted-foreground text-xs">{(pkg.price_per_credit * 100).toFixed(0)} cts / crédit</div>
<div className="text-muted-foreground text-xs">{(pkg.price_per_credit * 100).toFixed(0)} {t('pricing.credits.centsPerCredit')}</div>
<button className="mt-3 w-full py-1.5 rounded-lg bg-muted hover:bg-muted/80 text-foreground text-xs transition-all">
Acheter
{t('pricing.credits.buy')}
</button>
</div>
))}
@@ -752,15 +734,15 @@ export default function PricingPage() {
{/* ── Trust signals ── */}
<div className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-6">
{[
{ icon: <Shield className="w-6 h-6 text-emerald-500" />, title: "Chiffrement de bout en bout", sub: "TLS 1.3 + AES-256 au repos" },
{ icon: <Globe className="w-6 h-6 text-blue-500" />, title: "130+ langues", sub: "Dont arabe, persan, hébreu (RTL)" },
{ icon: <Gauge className="w-6 h-6 text-accent" />, title: "Traitement parallèle", sub: "IA multi-thread ultra-rapide" },
{ icon: <Clock className="w-6 h-6 text-amber-500" />, title: "Disponible 24/7", sub: "Uptime garanti 99,9 %" },
].map((t, i) => (
{ icon: <Shield className="w-6 h-6 text-emerald-500" />, title: t('pricing.trust.encryption.title'), sub: t('pricing.trust.encryption.sub') },
{ icon: <Globe className="w-6 h-6 text-blue-500" />, title: t('pricing.trust.languages.title'), sub: t('pricing.trust.languages.sub') },
{ icon: <Gauge className="w-6 h-6 text-accent" />, title: t('pricing.trust.parallel.title'), sub: t('pricing.trust.parallel.sub') },
{ icon: <Clock className="w-6 h-6 text-amber-500" />, title: t('pricing.trust.availability.title'), sub: t('pricing.trust.availability.sub') },
].map((signal, i) => (
<div key={i} className="flex flex-col items-center text-center p-6 rounded-2xl bg-card border border-border/30">
<div className="p-3 rounded-full bg-muted/50 mb-3">{t.icon}</div>
<div className="font-semibold text-foreground text-sm mb-1">{t.title}</div>
<div className="text-muted-foreground text-xs">{t.sub}</div>
<div className="p-3 rounded-full bg-muted/50 mb-3">{signal.icon}</div>
<div className="font-semibold text-foreground text-sm mb-1">{signal.title}</div>
<div className="text-muted-foreground text-xs">{signal.sub}</div>
</div>
))}
</div>
@@ -769,39 +751,37 @@ export default function PricingPage() {
<div className="mt-20 p-8 rounded-2xl bg-gradient-to-br from-accent/10 to-card border border-accent/20">
<div className="flex items-center gap-3 mb-6">
<Brain className="w-6 h-6 text-accent" />
<h2 className="text-2xl font-bold">Nos modèles IA — Mars 2026</h2>
<h2 className="text-2xl font-bold">{t('pricing.aiModels.title')}</h2>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="p-5 rounded-xl bg-card border border-border/40">
<div className="flex items-center gap-2 mb-2">
<Zap className="w-4 h-4 text-blue-500" />
<span className="font-semibold">Traduction IA Essentielle</span>
<Badge className="ml-auto text-xs bg-blue-500/10 text-blue-600 border-blue-500/30 dark:text-blue-300">Forfait Pro</Badge>
<span className="font-semibold">{t('pricing.aiModels.essential.title')}</span>
<Badge className="ml-auto text-xs bg-blue-500/10 text-blue-600 border-blue-500/30 dark:text-blue-300">{t('pricing.aiModels.essential.plan')}</Badge>
</div>
<div className="text-sm text-muted-foreground mb-3">
Basée sur <strong className="text-foreground">DeepSeek V3.2</strong> — le modèle IA le plus rentable de 2026.
Qualité comparable aux modèles frontier à 1/50ème du coût.
{t('pricing.aiModels.essential.descPrefix')} <strong className="text-foreground">DeepSeek V3.2</strong> {t('pricing.aiModels.essential.descSuffix')}
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="px-2 py-1 bg-muted rounded">163K tokens de contexte</span>
<span className="px-2 py-1 bg-muted rounded">{t('pricing.aiModels.essential.context')}</span>
<span className="px-2 py-1 bg-muted rounded">$0.25/$0.38 per 1M</span>
<span className="px-2 py-1 bg-success/10 text-success rounded">Excellent rapport qualité/prix</span>
<span className="px-2 py-1 bg-success/10 text-success rounded">{t('pricing.aiModels.essential.value')}</span>
</div>
</div>
<div className="p-5 rounded-xl bg-card border border-accent/30">
<div className="flex items-center gap-2 mb-2">
<Crown className="w-4 h-4 text-accent" />
<span className="font-semibold">Traduction IA Premium</span>
<Badge className="ml-auto text-xs bg-accent/10 text-accent border-accent/30">Forfait Business</Badge>
<span className="font-semibold">{t('pricing.aiModels.premium.title')}</span>
<Badge className="ml-auto text-xs bg-accent/10 text-accent border-accent/30">{t('pricing.aiModels.premium.plan')}</Badge>
</div>
<div className="text-sm text-muted-foreground mb-3">
Basée sur <strong className="text-foreground">Claude 3.5 Haiku</strong> d'Anthropic précis sur les documents
juridiques, médicaux et techniques complexes.
{t('pricing.aiModels.premium.descPrefix')} <strong className="text-foreground">Claude 3.5 Haiku</strong> {t('pricing.aiModels.premium.descSuffix')}
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="px-2 py-1 bg-muted rounded">200K tokens de contexte</span>
<span className="px-2 py-1 bg-muted rounded">{t('pricing.aiModels.premium.context')}</span>
<span className="px-2 py-1 bg-muted rounded">$0.25/$1.25 per 1M</span>
<span className="px-2 py-1 bg-accent/10 text-accent rounded">Meilleure précision</span>
<span className="px-2 py-1 bg-accent/10 text-accent rounded">{t('pricing.aiModels.premium.precision')}</span>
</div>
</div>
</div>
@@ -809,7 +789,7 @@ export default function PricingPage() {
{/* ── FAQ ── */}
<div className="mt-20 max-w-3xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-10">Questions fréquentes</h2>
<h2 className="text-3xl font-bold text-center mb-10">{t('pricing.faq.title')}</h2>
<div className="space-y-3">
{FAQS.map((faq, i) => (
<div
@@ -817,10 +797,10 @@ export default function PricingPage() {
className="rounded-xl border border-border/40 bg-card overflow-hidden"
>
<button
className="w-full flex items-center justify-between p-5 text-left hover:bg-muted/20 transition-colors"
className="w-full flex items-center justify-between p-5 text-start hover:bg-muted/20 transition-colors"
onClick={() => setOpenFAQ(openFAQ === i ? null : i)}
>
<span className="font-medium text-foreground">{faq.q}</span>
<span className="font-medium text-foreground">{t(faq.q)}</span>
{openFAQ === i
? <ChevronUp className="w-5 h-5 text-muted-foreground flex-shrink-0" />
: <ChevronDown className="w-5 h-5 text-muted-foreground flex-shrink-0" />
@@ -828,7 +808,7 @@ export default function PricingPage() {
</button>
{openFAQ === i && (
<div className="px-5 pb-5 text-muted-foreground text-sm leading-relaxed border-t border-border/30 pt-4">
{faq.a}
{t(faq.a)}
</div>
)}
</div>
@@ -838,20 +818,20 @@ export default function PricingPage() {
{/* ── CTA bottom ── */}
<div className="mt-20 text-center p-12 rounded-2xl bg-gradient-to-br from-accent/10 to-card border border-accent/20">
<h2 className="text-3xl font-bold mb-3">Prêt à commencer ?</h2>
<h2 className="text-3xl font-bold mb-3">{t('pricing.cta.title')}</h2>
<p className="text-muted-foreground mb-8 max-w-lg mx-auto">
Commencez gratuitement, sans carte bancaire. Passez à un forfait supérieur quand vous en avez besoin.
{t('pricing.cta.subtitle')}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/auth/register">
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground px-8 py-3 text-base font-semibold">
Créer un compte gratuit
<ArrowRight className="ml-2 w-5 h-5" />
{t('pricing.cta.createAccount')}
<ArrowRight className="ms-2 w-5 h-5" />
</Button>
</Link>
<Link href="/auth/login">
<Button variant="outline" className="px-8 py-3 text-base">
Se connecter
{t('pricing.cta.login')}
</Button>
</Link>
</div>