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
+15 -36
View File
@@ -1,52 +1,31 @@
"use client";
import { useLocale, useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import { useLocale } from 'next-intl';
import { useLocaleStore } from '@/stores/locale-store';
import { Select } from '@/components/settings/settings-section';
export function LanguageSwitcher({ className }: { className?: string }) {
const currentLocale = useLocale();
const t = useTranslations('language');
const setLocale = useLocaleStore((state) => state.setLocale);
const handleLanguageChange = (newLocale: string) => {
if (newLocale === currentLocale) return;
// Update locale in store (persisted to localStorage via Zustand)
// IntlProvider handles the translation switch
setLocale(newLocale);
};
const languages = [
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' }
{ value: 'fr', label: 'Français' },
{ value: 'ja', label: '日本語' },
{ value: 'es', label: 'Español' },
{ value: 'it', label: 'Italiano' },
{ value: 'de', label: 'Deutsch' },
{ value: 'nl', label: 'Nederlands' },
{ value: 'pt', label: 'Português' }
];
return (
<div
className={cn("flex gap-2", className)}
role="radiogroup"
aria-label={t('select_language')}
>
{languages.map((lang) => (
<button
key={lang.value}
type="button"
role="radio"
aria-checked={currentLocale === lang.value}
aria-label={t(lang.value === 'en' ? 'switch_to_english' : 'switch_to_french')}
onClick={() => handleLanguageChange(lang.value)}
className={cn(
"px-3 py-1.5 text-xs rounded transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
currentLocale === lang.value
? "bg-primary text-primary-foreground"
: "bg-muted hover:bg-accent text-foreground"
)}
>
{lang.label}
</button>
))}
<div className={className}>
<Select
value={currentLocale}
onChange={setLocale}
options={languages}
/>
</div>
);
}