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

@@ -0,0 +1,84 @@
---
target: frontend/src (UI complète Wordly.art)
total_score: 20
max_score: 40
na_heuristics:
p0_count: 1
p1_count: 4
timestamp: 2026-08-30T19-00-41Z
slug: frontend-src
---
# Critique de design — Wordly.art (`frontend/src`)
Method: dual-agent (A: agent_0921d3fd-00f8-424c-a025-1e69fe2d49ff · B: agent_44a564e1-9137-48cf-a6b5-7d78e1042ead)
## Design Health Score
| # | Heuristique | Score | Problème clé |
|---|-------------|-------|--------------|
| 1 | Visibility of System Status | 2 | Stats fabriquées pendant le traitement (« 99.9% » `translate/page.tsx:404`, « Intégrité Layout » = `40 + progress*0.55` `translate/page.tsx:695`) ; `estimatedRemaining_seconds` récupéré mais jamais affiché (`useTranslationSubmit.ts:78`) ; échec de téléchargement **silencieux** (`translate/page.tsx:152`) ; notifications sans `role="status"` (`notification.tsx:118`). |
| 2 | Match System / Real World | 2 | Produit annoncé 12 locales, mais « Source/Cible/Moteur » codés en dur FR (`translate/page.tsx:671-728`), page reviews 100% FR (`reviews/[jobId]/page.tsx`), erreurs upload FR (`useFileUpload.ts:8-9`), erreurs polling EN (`useTranslationSubmit.ts:62,102`), « Loading… » EN (`DashboardLayoutClient.tsx:29`). |
| 3 | User Control and Freedom | 2 | « Annuler le processus » (`translate/page.tsx:700-705`) ne fait qu'un reset local — le job continue côté serveur, crédits consommés ; aucun historique : rafraîchir pendant une traduction perd tout (`useTranslationSubmit.ts` sans persistance) ; page review inatteignable (unique lien dans un composant mort, `TranslationComplete.tsx:168`). |
| 4 | Consistency and Standards | 1 | Trois systèmes visuels (éditorial / shadcn auth / admin violet `admin/login/page.tsx:52`) ; pricing landing 3 plans 7/15/39€ (`landing-page.tsx:330-332`) vs /pricing 5 plans (`pricing/page.tsx:56-192`) ; deux systèmes de toasts (`useNotification` + `useToast`) ; 5 composants translate/ orphelins jamais importés ; `tailwind.config.js:28-35` décrit un thème dark/bleu qui n'existe plus. |
| 5 | Error Prevention | 2 | Boutons Word/Excel/Slides/PDF du dropzone injectent un **faux fichier** de 100 octets à taille truquée (`useFileUpload.ts:83-90`) ; « Effacer le cache » sans confirmation (`settings/page.tsx:13-28`) ; gating incohérent : glossaires réservés à `tier === 'pro'` strict (`glossaries/page.tsx:56`) alors que la sidebar traite business/enterprise comme Pro (`DashboardSidebar.tsx:20`) — clients Business privés de la feature. |
| 6 | Recognition Rather Than Recall | 3 | Nav icône+texte (`DashboardSidebar.tsx:42-54`), résumé de config visible pendant le processing (`translate/page.tsx:647-684`). Perd : pages settings/services/reviews orphelines hors nav (`constants.ts:10-17`), badge tier `text-[7px]` illisible (`DashboardSidebar.tsx:113`). |
| 7 | Flexibility and Efficiency | 1 | Aucun raccourci clavier dans le code vivant (le seul handler Enter/Espace est dans le composant mort `FileDropZone.tsx:35`) ; un fichier à la fois, aucun batch ; pas d'historique ni rejeu de config. |
| 8 | Aesthetic and Minimalist Design | 2 | Micro-typographie : `text-[7.5px]` (`translate/page.tsx:342`), `text-[8px]` (`translate/page.tsx:263,336`), texte à `/30` opacité ; tout en `uppercase tracking-widest` jusqu'au CTA primaire (`translate/page.tsx:322`) ; deux blocs promo Memento concurrents sur l'écran de travail. |
| 9 | Error Recovery | 3 | `humanFriendlyError()` mappe les erreurs brutes vers des messages clairs localisés (`translate/page.tsx:90-111`) ; état failed conserve le fichier + Réessayer (`translate/page.tsx:452-489`). Gaps : téléchargement silencieux, messages backend bruts sur register (`RegisterForm.tsx:221`). |
| 10 | Help and Documentation | 2 | Aide contextuelle bonne sur moteurs/glossaires (`ProviderSelector.tsx:276-306`, `GlossarySelector.tsx:332-334`) mais aucune doc vivante : liens footer morts `href="#"` (`landing-page.tsx:436-445`), contact Enterprise = `mailto:contact@votre-domaine.com` (`pricing/page.tsx:362`) — un placeholder de domaine. |
| **Total** | | **20/40** | **Acceptable — améliorations significatives nécessaires** |
## Verdict de spécificité design
**LLM assessment** : Landing — spécifique, parfois remarquable (copy métier : « CTA restera "Air Handling Unit", jamais "Call To Action" » `landing.json:35` ; identité crème/Playfair/or `globals.css:14-17,112`) mais le hero illustre le produit avec une photo Unsplash générique (`landing-page.tsx:139`) au lieu de montrer l'argument n°1 — la mise en page préservée. Dashboard — l'ossature du flux traduction est écrite pour CE produit, mais rendue interchangeable par la bannière publicitaire tierce « Memento » (`translate/page.tsx:748-781`, `DashboardSidebar.tsx:73-93`) et des stats décoratives fausses. Auth — interchangeable (Register `variant="premium"` `RegisterForm.tsx:337` vs Login default `LoginForm.tsx:179`). Admin — littéralement un autre produit (sidebar compacte EN codée en dur, accent violet, `AdminSidebar.tsx:64-84`).
**Deterministic scan** : 4 findings (exit 2). Deux `overused-font` sur Inter (`globals.css:141,147`) — le détecteur confirme le diagnostic de la revue par l'autre bout : l'app est portée par Inter, la police par défaut de l'ère IA, tandis que l'identité réelle du produit (Playfair + crème + or) ne vit que sur la landing. Un `side-tab` (`border-l-4` `dashboard/profile/page.tsx:469`, visible pour les comptes payants) que la revue n'avait pas isolé. Un `border-accent-on-rounded` (`globals.css:215`) : **faux positif** — c'est le pattern canonique d'un spinner de chargement, pas une carte.
**Visual overlays** : aucun overlay disponible — l'injection mutable s'est heurtée à « Browser is not available in subagent » après deux tentatives réelles. Signal de fallback : scan CLI complet + vérification HTTP statique (/, /login, /pricing → 200, avec réserve : un port-forward de l'éditeurCursor sur 127.0.0.1:3000 a probablement répondu à la place du serveur local).
## Impression générale
La fondation est saine — machine à états propre, gestion d'erreurs humaine, copy métier différenciante — mais l'exécution sabote la confiance au moment exact où un professionnel en a besoin : publicité tierce dans l'écran de travail, statistiques fabriquées, parcours primaire inaccessible au clavier, i18n de façade. La plus grande opportunité : transformer la fin de job — déjà le meilleur moment émotionnel du produit — en preuve produit (lien vers la review segment par segment qui existe déjà), et évacuer le bruit qui la noie.
## Ce qui fonctionne
1. **La machine à états du flux de traduction** (`translate/page.tsx:172-181`) : upload → config → processing → complete/failed se relaient proprement dans un seul écran, une intention à la fois, zéro navigation. C'est la bonne architecture pour ce produit.
2. **`humanFriendlyError()`** (`translate/page.tsx:90-111`) : pattern-matching des erreurs brutes (« quota », « 429 », « timed out ») vers des messages clairs localisés, avec état failed qui conserve le fichier et propose Réessayer. La majorité des SaaS affichent le message brut — ici c'est pensé.
3. **La copy landing FR comme différenciateur** (« Les autres traducteurs détruisent votre mise en page. Pas nous. » `landing.json:43`) : c'est du domaine, pas du template.
## Problèmes prioritaires
1. **[P0] Le dépôt de fichier est impossible au clavier — et le composant accessible est du code mort.** Le dropzone vivant est un `div` avec `onClick` sans `tabIndex`, `role` ni handler clavier (`translate/page.tsx:256-261`) ; l'utilisateur clavier seul ne peut pas faire la tâche primaire. Le handler Enter/Espace correct existe dans `FileDropZone.tsx:35`, jamais importé. Le combobox de langue custom n'a ni `role="combobox"` ni flèches ni Échap (`LanguageSelector.tsx:66-113`) ; le switch glossaire n'a pas de `role="switch"` (`GlossarySelector.tsx:286-312`) ; contrastes massivement sous 4.5:1 (`text-brand-dark/30` sur crème, `translate/page.tsx:263,336,342`) ; actions révélées au survol uniquement (`GlossaryCard.tsx:65,75`). → Fix : brancher `FileDropZone.tsx` ou rendre le dropzone focusable, `Select` shadcn pour les langues, `role="switch"`/`role="status"` manquants. **Commandes : `$impeccable audit` (quantifier a11y) puis `$impeccable harden` (corriger).**
2. **[P1] Publicité tierce « Memento » greffée dans l'outil de travail.** Deux blocs promo vers un produit externe (sidebar `DashboardSidebar.tsx:73-93` + bannière pleine largeur à double CTA `translate/page.tsx:748-781`) sur l'écran du cœur de métier, pour un SaaS qui vend la confidentialité documentaire — le message « Rétention zéro » en 7.5px juste au-dessus (`translate/page.tsx:342-349`) est contredit par la promo en dessous. Pill « Ecosystème Wordly » codé en dur non traduit (`translate/page.tsx:764`). → Fix : sortir la promo du flux de traduction ; au maximum un emplacement unique post-tâche, discret, désactivable, traduit. **Commande : `$impeccable distill`.**
3. **[P1] Les boutons « Word / Excel / Slides / PDF » injectent de faux fichiers.** 4 boutons ressemblant à des filtres de format chargent un blob de 100 octets avec taille falsifiée (`useFileUpload.ts:83-90`, noms factices `useFileUpload.ts:74-77`). L'utilisateur croit sélectionner un type ; il obtient un fichier qui échouera à la soumission — piège à erreurs garanti. → Fix : supprimer, ou transformer en démo explicite (« Essayer avec un exemple ») sur un flux séparé. **Commandes : `$impeccable harden` + `$impeccable clarify`.**
4. **[P1] L'i18n est une façade : la moitié des strings visibles sont codées en dur, et le pattern de fallback est mort.** Un utilisateur EN voit « Source/Cible » (`translate/page.tsx:671-681`), des pages entières FR (reviews, teams, glossaries), des erreurs FR (« Erreur serveur » `pricing/page.tsx:390,434`) et EN (« Loading... »). Pire : `t()` retourne la clé brute si absente (`i18n.tsx:161`), donc tous les `t('key') || 'fallback'` (~30 occurrences, ex. `GlossarySelector.tsx:277`) n'appliquent **jamais** leur fallback — clé manquante = `translate.glossary.title` affiché à l'écran. Les découpes `lastIndexOf(' ')` pour l'italique du dernier mot (`translate/page.tsx:206-208`) casseront en zh/ja. → Fix : `t()` retourne une chaîne vide sur clé manquante, auditer les `||`, extraire les strings codées en dur. **Commande : `$impeccable harden`.**
5. **[P1] Features fantômes et perte de contrôle : review inatteignable, pages orphelines, annulation factice, pas d'historique.** La page de review segment par segment (`reviews/[jobId]/page.tsx`) n'est liée nulle part dans l'UI vivante (unique lien dans `TranslationComplete.tsx:168`, composant jamais importé) ; settings et services absents de la nav (`constants.ts:10-17`) ; « Annuler » ne fait qu'un reset local pendant que crédits partent en consommation ; rafraîchir pendant un traitement détruit tout état (aucun jobId persisté) ; clients Business/Enterprise privés de glossaires (`glossaries/page.tsx:56`). → Fix : persister le jobId avec reprise du polling, historique des jobs récents, lien complet → `/dashboard/reviews/[jobId]`, vraie API d'annulation ou renommage honnête, unifier `isPro`. **Commande : `$impeccable shape`.**
## Red flags personas
**Alex (utilisateur expert pressé)** : aucun raccourci clavier vivant ; un fichier à la fois, aucun batch (rédhibitoire pour 20 docs/semaine) ; aucun historique/rejeu — chaque session repart de zéro ; les stats « 99.9% précision / Turbo » (`translate/page.tsx:404-405`) détruisent sa confiance ; « Annuler » qui n'annule pas — il le découvrira à la facture.
**Sam (accessibilité)** : bloqué dès l'étape 1 — dropzone inaccessible clavier (`translate/page.tsx:256-261`) ; contraste ~1.8:1 sur des dizaines de libellés (`text-brand-dark/30`) ; tailles 7.5-9px `uppercase tracking-widest` illisibles ; notifications muettes pour lecteur d'écran (`notification.tsx:118`) ; échec de téléchargement sans aucun rendu (`translate/page.tsx:152`) ; actions invisibles hors hover (`GlossaryCard.tsx:65,75`, `notification.tsx:181`) ; RTL : `dir` bien posé mais barres/badges en `left-0`/`right-0` physiques (`translate/page.tsx:387`).
**Jordan (premier usage)** : premier écran correct (dropzone central « Glissez-déposez ici ») puis 4 boutons formats qui ne font pas ce qu'ils annoncent ; jargon non défini (« Standard » vs « IA contextuelle », « Intégrité Layout 100% SÉCURISÉ » `translate.json:85-86`) ; aucun doc vivante (footer « Documentation » mort `landing-page.tsx:436`) ; le premier paiement peut aboutir sur `mailto:contact@votre-domaine.com` (`pricing/page.tsx:362`).
## Observations mineures
- Titre dupliqué pill + H1 sur settings et services (`settings/page.tsx:40-42`, `services/page.tsx:56-58`).
- Dates glossaires `fr-FR` en dur (`glossaries/page.tsx:412`) contre `formatDate()` locale existante (`i18n.tsx:108`).
- Prix IA en `$` sur une page facturée en `€` (`pricing/page.tsx:870,885`).
- `site-header.tsx`/`site-footer.tsx` importés nulle part ; le header fantôme pointe « Connexion » vers `/dashboard` (`site-header.tsx:31`).
- Mention CGU non cliquable (`RegisterForm.tsx:366-370`).
- Drapeaux pays pour des langues (ar→🇸🇦, zh→🇨🇳 `language-switcher.tsx:24-25`) — discutable pour un outil pro multilingue ; 13 locales à plat dans le switcher.
- Bon réflexe rare : `prefers-reduced-motion` et `prefers-contrast` gérés (`globals.css:237-254`) ; skeleton pricing anti-flash (`pricing/page.tsx:221-259`).
- Le switch « Traduire les images » désactivé rend toute la carte grisée sans explication avant interaction (`translate/page.tsx:556-585`).
## Questions à considérer
1. Si un prospect demande « prouvez-moi que ma mise en page survit », quelle partie de l'écran répond ? Aucune aujourd'hui — le hero montre une photo Unsplash de bureau (`landing-page.tsx:139`) alors que le moteur produit déjà un avant/après à chaque job. Un seul DOCX tri-langue côte à côte vaudrait plus que les 6 cartes de LayoutFeatures.
2. Pourquoi l'état « traduction terminée » — le meilleur moment émotionnel — se termine-t-il sur un bouton fantôme de 10px alors que la page de review segment par segment existe déjà ?
3. Qu'est-ce qui coûte le plus : les revenus Memento, ou la crédibilité « documents confidentiels » que la promo détruit à chaque écran ?