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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user