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