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

@@ -10,20 +10,21 @@ import {
} from "@/components/ui/select";
import { useI18n, type Locale } from "@/lib/i18n";
const languages: { value: Locale; label: string; flag: string }[] = [
{ value: "en", label: "English", flag: "🇬🇧" },
{ value: "fr", label: "Français", flag: "🇫🇷" },
{ value: "es", label: "Español", flag: "🇪🇸" },
{ value: "de", label: "Deutsch", flag: "🇩🇪" },
{ value: "pt", label: "Português", flag: "🇧🇷" },
{ value: "it", label: "Italiano", flag: "🇮🇹" },
{ value: "nl", label: "Nederlands", flag: "🇳🇱" },
{ value: "ru", label: "Русский", flag: "🇷🇺" },
{ value: "ja", label: "日本語", flag: "🇯🇵" },
{ value: "ko", label: "한국어", flag: "🇰🇷" },
{ value: "zh", label: "中文", flag: "🇨🇳" },
{ value: "ar", label: "العربية", flag: "🇸🇦" },
{ value: "fa", label: "فارسی", flag: "🇮🇷" },
// Language names in their own script — flags are country symbols, not languages.
const languages: { value: Locale; label: string }[] = [
{ value: "en", label: "English" },
{ value: "fr", label: "Français" },
{ value: "es", label: "Español" },
{ value: "de", label: "Deutsch" },
{ value: "pt", label: "Português" },
{ value: "it", label: "Italiano" },
{ value: "nl", label: "Nederlands" },
{ value: "ru", label: "Русский" },
{ value: "ja", label: "日本語" },
{ value: "ko", label: "한국어" },
{ value: "zh", label: "中文" },
{ value: "ar", label: "العربية" },
{ value: "fa", label: "فارسی" },
];
interface LanguageSwitcherProps {
@@ -31,16 +32,15 @@ interface LanguageSwitcherProps {
}
export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
const { locale, setLocale } = useI18n();
const { locale, setLocale, t } = useI18n();
const current = languages.find((l) => l.value === locale) ?? languages[0];
return (
<Select value={locale} onValueChange={(v) => setLocale(v as Locale)}>
<SelectTrigger className={className} aria-label="Change language">
<SelectTrigger className={className} aria-label={t('langSelector.changeLanguage')}>
<SelectValue>
<span className="flex items-center gap-2">
<Globe className="h-4 w-4" />
<span>{current.flag}</span>
<span>{current.label}</span>
</span>
</SelectValue>
@@ -49,7 +49,6 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
{languages.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
<span className="flex items-center gap-2">
<span>{lang.flag}</span>
<span>{lang.label}</span>
</span>
</SelectItem>

View File

@@ -117,13 +117,15 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
return (
<div
ref={ref}
role="status"
aria-live="polite"
className={cn(notificationVariants({ variant, size, position }), className)}
{...props}
>
{/* Progress Bar for Auto-close */}
{autoClose && !loading && (
<div className="absolute top-0 left-0 h-1 bg-white/20">
<div
<div
className="h-full bg-white/40 transition-all duration-100 ease-linear"
style={{ width: `${progress}%` }}
/>
@@ -178,7 +180,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
<button
type="button"
onClick={handleClose}
className="absolute right-2 top-2 flex-shrink-0 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
className="absolute right-2 top-2 flex-shrink-0 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label="Close notification"
>
<X className="h-4 w-4" />