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
+83
View File
@@ -0,0 +1,83 @@
import { useEffect, useRef } from 'react';
interface UseFocusTrapOptions {
isActive: boolean;
onEscape?: () => void;
restoreFocus?: boolean;
}
export function useFocusTrap({
isActive,
onEscape,
restoreFocus = true,
}: UseFocusTrapOptions) {
const containerRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!isActive || !containerRef.current) return;
// Store the element that had focus before modal opened
previousActiveElement.current = document.activeElement as HTMLElement;
const container = containerRef.current;
// Get all focusable elements
const getFocusableElements = () => {
return container.querySelectorAll<HTMLElement>(
'button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"]):not(:disabled)'
);
};
// Focus first element
const focusableElements = getFocusableElements();
const firstElement = focusableElements[0];
if (firstElement) {
firstElement.focus();
}
// Handle Tab key to trap focus
const handleKeyDown = (e: KeyboardEvent) => {
// Handle Escape
if (e.key === 'Escape' && onEscape) {
onEscape();
return;
}
// Handle Tab
if (e.key === 'Tab') {
const focusableElements = getFocusableElements();
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
// Shift+Tab
if (document.activeElement === firstElement) {
lastElement?.focus();
e.preventDefault();
}
} else {
// Tab
if (document.activeElement === lastElement) {
firstElement?.focus();
e.preventDefault();
}
}
}
};
container.addEventListener('keydown', handleKeyDown);
// Cleanup
return () => {
container.removeEventListener('keydown', handleKeyDown);
// Restore focus to previous element
if (restoreFocus && previousActiveElement.current) {
previousActiveElement.current.focus();
}
};
}, [isActive, onEscape, restoreFocus]);
return containerRef;
}