fix(ui): design critique overhaul — a11y, honest metrics, i18n repair (critique 2026-08-30)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s

P0 a11y: keyboard-accessible dropzone (role/tabIndex/Enter-Space), ARIA
combobox + listbox pattern for language selector, role=switch on glossary
toggle, role=status live region on notifications, aria-labels on password
toggles, visible-on-focus close buttons, RTL logical positioning (start-*).

Trust: third-party Memento promo removed from translate page, sidebar and
all 13 locales; fabricated stats (99.9%, Turbo, computed layout-integrity
bars) replaced with real measurements incl. API estimated remaining time;
silent download failure now surfaces an error notification.

Honesty: fake 100-byte file injections removed (format chips are now
informational); cancel-that-doesn't renamed 'Back to start' with hint;
Enterprise contact placeholder replaced with contact@wordly.art.

i18n: t() no longer returns raw keys (empty string + defaultValue support,
~30 dead || fallbacks now work); ~170 new keys EN+FR across new reviews/
teams namespaces, glossaries context tab, translate monitor, settings,
services, pricing, landing, fileUploader; split-key italic titles replace
lastIndexOf() surgery (zh/ja-safe); key-audit script added 0 missing.

Flow: active job persisted across refresh with polling resume (24h TTL);
client-side recent-jobs history with review links; review page linked from
complete state; settings/services added to dashboard nav; Business/
Enterprise regain glossary access (tier gate unified).

Typeset (sober-tool direction): 7.5-9px labels raised to 10-12px, /30
opacity to /45-/55, uppercase tracking reduced, trust footer legible,
country flags removed from language switcher, localized dates.

Cleanup: 5 orphaned translate components, dead site header/footer,
fossil tailwind.config.js, PipelineStepper, duplicate pill+H1 titles,
two-step confirm for cache clear, dead landing footer links.

Verified: next build exit 0, vitest 9/9, eslint 64 errors = HEAD
(no regression, -3 warnings), detector 4 -> 3 findings.
This commit is contained in:
2026-08-30 21:44:53 +02:00
parent 1a67241ad5
commit 50047ea8a2
79 changed files with 878 additions and 1590 deletions

View File

@@ -16,6 +16,9 @@ import { API_BASE } from "@/lib/config";
import { ANNUAL_DISCOUNT_PERCENT } from "@/lib/pricing";
import { useI18n } from "@/lib/i18n";
// Enterprise contact — single place to update the sales address.
const SUPPORT_EMAIL = "contact@wordly.art";
/* ─────────────────────────────────────────────
Types
───────────────────────────────────────────── */
@@ -359,7 +362,7 @@ export default function PricingPage() {
return;
}
if (planId === "enterprise") {
window.location.href = "mailto:contact@votre-domaine.com?subject=Offre Enterprise";
window.location.href = `mailto:${SUPPORT_EMAIL}?subject=${encodeURIComponent(t('pricing.enterprise.subject'))}`;
return;
}
if (planId === currentPlan) return;
@@ -387,7 +390,7 @@ export default function PricingPage() {
const backendMessage = data.message ?? data.error ?? data.data?.error;
if (!res.ok) {
throw new Error(backendMessage || `Erreur serveur ${res.status}`);
throw new Error(backendMessage || t('pricing.error.server', { status: res.status }));
}
if (url) {
@@ -431,7 +434,7 @@ export default function PricingPage() {
const url = data.data?.url ?? data.url;
if (!res.ok) {
throw new Error(data.message ?? data.error ?? `Erreur serveur ${res.status}`);
throw new Error(data.message ?? data.error ?? t('pricing.error.server', { status: res.status }));
}
if (url) {
window.location.replace(url);
@@ -488,7 +491,7 @@ export default function PricingPage() {
)}>
<span className="text-lg">{toastMsg.type === 'ok' ? t('pricing.okSymbol') : t('pricing.errSymbol')}</span>
<p className="text-sm flex-1">{toastMsg.text}</p>
<button onClick={() => setToastMsg(null)} className="text-muted-foreground hover:text-foreground text-lg leading-none">&times;</button>
<button onClick={() => setToastMsg(null)} aria-label={t('pricing.toast.close')} className="text-muted-foreground hover:text-foreground text-lg leading-none">&times;</button>
</div>
)}
@@ -499,9 +502,9 @@ export default function PricingPage() {
<span className="text-[10px] font-black tracking-[0.3em] opacity-40">{t('pricing.header.badge')}</span>
</span>
<h1 className="text-5xl md:text-7xl font-black uppercase tracking-tighter mb-6 leading-none">
{t('pricing.header.title').split(' ').slice(0, -2).join(' ')}{" "}
{t('pricing.header.titleBase')}{" "}
<span className="text-accent">
{t('pricing.header.title').split(' ').slice(-2).join(' ')}
{t('pricing.header.titleAccent')}
</span>
</h1>
<p className="text-foreground/40 font-medium text-xl max-w-2xl mx-auto leading-relaxed">