feat: expand internationalization and add identity management

This release significantly expands internationalization support and adds comprehensive identity management features.

Internationalization (i18n):
- Add 5 new languages: Spanish, Italian, German, Dutch, Portuguese
- Expand from 3 to 8 total supported languages
- Redesign language switcher for better scalability (dropdown UI)
- Complete translations for all features across all languages

Identity Management:
- Multiple sender identities with per-identity signatures
- Sub-addressing support (user+tag@domain.com)
- Context-aware tag suggestions for sub-addresses
- Identity badges in email viewer and list
- Full CRUD operations for managing identities

Newsletter Management:
- RFC 2369 List-Unsubscribe support (one-click unsubscribe)
- HTTP and mailto unsubscribe methods
- Security validation prevents XSS attacks
- Two-step confirmation with persistent dismissal

Security & Accessibility:
- Dark mode email readability (intelligent color transformation)
- WCAG 2.0 Level AA color contrast compliance
- Comprehensive XSS prevention with validation utilities
- Unit test coverage for security-critical code (57 validation tests)

Testing:
- Add unit tests for validation utilities
- Add unit tests for email sanitization
- Add unit tests for color transformation
- Full test coverage for XSS attack vectors
This commit is contained in:
Matthieu MALVACHE
2026-01-08 22:15:32 +01:00
committed by Matthieu MALVACHE
parent 0d68851b63
commit 5d273e2109
49 changed files with 11018 additions and 432 deletions
+7 -3
View File
@@ -4,6 +4,7 @@ import { Menu, ArrowLeft, Plus, Search, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useUIStore } from "@/stores/ui-store";
import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl";
interface MobileHeaderProps {
title: string;
@@ -22,6 +23,7 @@ export function MobileHeader({
onSearch,
className,
}: MobileHeaderProps) {
const t = useTranslations('sidebar');
const { toggleSidebar, goBack, sidebarOpen } = useUIStore();
const handleLeftAction = () => {
@@ -72,7 +74,7 @@ export function MobileHeader({
size="icon"
onClick={onSearch}
className="h-10 w-10"
aria-label="Search"
aria-label={t('mobile.search')}
>
<Search className="h-5 w-5" />
</Button>
@@ -83,7 +85,7 @@ export function MobileHeader({
size="icon"
onClick={onCompose}
className="h-10 w-10 text-primary"
aria-label="Compose"
aria-label={t('mobile.compose')}
>
<Plus className="h-5 w-5" />
</Button>
@@ -111,6 +113,8 @@ export function MobileViewerHeader({
onArchive: _onArchive,
className,
}: MobileViewerHeaderProps) {
const t = useTranslations('sidebar');
return (
<header
className={cn(
@@ -124,7 +128,7 @@ export function MobileViewerHeader({
size="icon"
onClick={onBack}
className="h-10 w-10"
aria-label="Go back"
aria-label={t('mobile.go_back')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
+19 -1
View File
@@ -30,6 +30,7 @@ import { cn, buildMailboxTree, MailboxNode, formatFileSize } from "@/lib/utils";
import { Mailbox } from "@/lib/jmap/types";
import { useDragDropContext } from "@/contexts/drag-drop-context";
import { useMailboxDrop } from "@/hooks/use-mailbox-drop";
import { toast } from "@/stores/toast-store";
interface SidebarProps {
mailboxes: Mailbox[];
@@ -96,6 +97,7 @@ function MailboxTreeItem({
isCollapsed: boolean;
}) {
const t = useTranslations('sidebar');
const tNotifications = useTranslations('notifications');
const hasChildren = node.children.length > 0;
const isExpanded = expandedFolders.has(node.id);
const Icon = getIconForMailbox(node.role, node.name, hasChildren, isExpanded, node.isShared, node.id);
@@ -106,6 +108,22 @@ function MailboxTreeItem({
const { isDragging: globalDragging } = useDragDropContext();
const { dropHandlers, isValidDropTarget, isInvalidDropTarget } = useMailboxDrop({
mailbox: node,
onSuccess: (count, mailboxName) => {
if (count === 1) {
toast.success(
tNotifications('email_moved'),
tNotifications('moved_to_mailbox', { mailbox: mailboxName })
);
} else {
toast.success(
tNotifications('emails_moved', { count }),
tNotifications('moved_to_mailbox', { mailbox: mailboxName })
);
}
},
onError: () => {
toast.error(tNotifications('move_failed'), tNotifications('move_error'));
},
});
return (
@@ -371,7 +389,7 @@ export function Sidebar({
onClearSearch?.();
}}
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 rounded-full hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
aria-label="Clear search"
aria-label={t('clear_search')}
>
<X className="w-4 h-4" />
</button>