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
+170 -5
View File
@@ -52,7 +52,13 @@
"expand": "Développer",
"collapse": "Réduire",
"expand_tooltip": "Développer",
"collapse_tooltip": "Réduire"
"collapse_tooltip": "Réduire",
"mobile": {
"search": "Rechercher",
"compose": "Composer",
"go_back": "Retour"
},
"clear_search": "Effacer la recherche"
},
"email_list": {
"no_emails": "Aucun email",
@@ -63,7 +69,19 @@
"to_recipients": "À {{count}} destinataires",
"and_others": "et {{count}} autres",
"draft": "Brouillon",
"starred": "Favori"
"starred": "Favori",
"conversations_count": "{count} sur {total} conversations",
"conversations_count_plus": "{count}+ conversations",
"conversations_count_simple": "{count} conversations",
"no_conversations": "Aucune conversation",
"loading_more": "Chargement d'autres emails...",
"no_more_emails": "Plus d'emails à charger",
"batch_actions": {
"mark_read": "Marquer comme lu",
"mark_unread": "Marquer comme non lu",
"delete": "Supprimer",
"clear_selection": "Effacer la sélection"
}
},
"email_viewer": {
"no_email_selected": "Aucun email sélectionné",
@@ -129,6 +147,9 @@
"minutes": "minutes"
},
"unknown_sender": "Inconnu",
"recipient_me": "moi",
"recipient_and_others": "{name} et {count} autres",
"recipient_to_prefix": "À :",
"authentication": {
"title": "Authentification",
"status": {
@@ -167,7 +188,35 @@
"green": "Vert",
"blue": "Bleu",
"purple": "Violet",
"pink": "Rose",
"none": "Aucune"
},
"tooltips": {
"reply": "Répondre",
"archive": "Archiver",
"delete": "Supprimer"
},
"spam": {
"button_title": "Signaler comme spam",
"not_spam_title": "Marquer comme légitime",
"toast_success": "Déplacé vers Indésirables",
"toast_batch": "{count} e-mails déplacés vers Indésirables",
"toast_undo": "Annuler",
"toast_not_spam_success": "Déplacé vers Boîte de réception",
"toast_not_spam_batch": "{count} e-mails déplacés vers Boîte de réception",
"error": "Échec du signalement spam",
"error_not_spam": "Échec de la restauration"
},
"unsubscribe_banner": {
"label": "Newsletter",
"button": "Se désabonner",
"confirm_title": "Se désabonner de cet expéditeur ?",
"confirm_button": "Confirmer",
"cancel": "Annuler",
"success_http": "Page de désabonnement ouverte dans un nouvel onglet",
"success_mailto": "Demande de désabonnement envoyée à votre client mail",
"error": "Impossible de se désabonner",
"dismiss": "Ignorer"
}
},
"email_composer": {
@@ -200,6 +249,12 @@
"bcc_label": "Cci :",
"subject_label": "Objet :",
"file_size_kb": "Ko",
"prefix": {
"forward": "Tr:",
"reply": "Re:"
},
"no_subject": "(Sans objet)",
"unknown_sender": "Inconnu",
"quote": {
"reply_header": "Le {{date}}, {{sender}} a écrit :",
"forward_header": "---------- Message transféré ----------",
@@ -207,7 +262,8 @@
"date": "Date : {{date}}",
"subject": "Objet : {{subject}}",
"to": "À : {{recipients}}"
}
},
"remove_sub_address": "Retirer le sous-adressage"
},
"common": {
"loading": "Chargement...",
@@ -243,7 +299,20 @@
"error_loading": "Échec du chargement des emails",
"new_email": "Nouvel email",
"new_email_from": "De {sender}",
"click_to_view": "Cliquer pour voir"
"click_to_view": "Cliquer pour voir",
"email_moved": "Email déplacé",
"emails_moved": "{count} emails déplacés",
"moved_to_mailbox": "Déplacé vers {mailbox}",
"move_failed": "Échec du déplacement",
"move_error": "Impossible de déplacer les emails vers le dossier sélectionné",
"identity_created": "Identité créée avec succès",
"identity_updated": "Identité mise à jour avec succès",
"identity_deleted": "Identité supprimée",
"identity_create_failed": "Échec de la création de l'identité: {{error}}",
"identity_update_failed": "Échec de la mise à jour de l'identité: {{error}}",
"identity_delete_failed": "Échec de la suppression de l'identité: {{error}}",
"identity_unauthorized": "Vous n'êtes pas autorisé à envoyer depuis cette adresse email",
"identity_not_found": "Identité introuvable"
},
"date": {
"today": "Aujourd'hui",
@@ -264,9 +333,21 @@
"title": "Langue",
"english": "English",
"french": "Français",
"japanese": "日本語",
"spanish": "Español",
"italian": "Italiano",
"german": "Deutsch",
"dutch": "Nederlands",
"portuguese": "Português",
"select_language": "Sélectionner la langue",
"switch_to_english": "Passer à l'anglais",
"switch_to_french": "Passer au français",
"switch_to_japanese": "Passer au japonais",
"switch_to_spanish": "Passer à l'espagnol",
"switch_to_italian": "Passer à l'italien",
"switch_to_german": "Passer à l'allemand",
"switch_to_dutch": "Passer au néerlandais",
"switch_to_portuguese": "Passer au portugais",
"switching": "Changement de langue..."
},
"settings": {
@@ -283,6 +364,7 @@
"composer": "Compositeur",
"privacy": "Confidentialité et sécurité",
"account": "Compte",
"identities": "Identités",
"advanced": "Avancé"
},
"appearance": {
@@ -468,6 +550,23 @@
"value": "{{time}}"
}
},
"identities": {
"title": "Identités d'envoi",
"description": "Gérer les adresses email depuis lesquelles vous pouvez envoyer",
"identities_count": {
"label": "Vos identités",
"description": "Adresses email configurées pour l'envoi",
"count_zero": "Aucune identité",
"count_one": "1 identité",
"count_other": "{{count}} identités"
},
"manage": "Gérer les identités",
"sub_addressing": {
"label": "Sous-adressage",
"description": "Utilisez des tags comme utilisateur+tag@domaine.com pour organiser votre courrier",
"learn_more": "En savoir plus"
}
},
"advanced": {
"title": "Avancé",
"description": "Options avancées et paramètres développeur",
@@ -525,6 +624,8 @@
"move_to": "Déplacer vers...",
"archive": "Archiver",
"delete": "Supprimer",
"mark_as_spam": "Signaler comme spam",
"not_spam": "Pas un spam",
"color_tag": "Étiquette de couleur",
"remove_color": "Supprimer la couleur",
"items_selected": "{{count}} emails sélectionnés"
@@ -552,7 +653,8 @@
"archive": "Archiver",
"delete": "Supprimer",
"mark_unread": "Marquer comme non lu",
"mark_read": "Marquer comme lu"
"mark_read": "Marquer comme lu",
"toggle_spam": "Signaler / Pas un spam"
},
"global": {
"compose": "Composer un email",
@@ -577,5 +679,68 @@
"delete": "Supprimer la conversation",
"star": "Marquer la conversation comme favorite",
"unstar": "Retirer des favoris"
},
"identities": {
"modal_title": "Gérer les identités d'envoi",
"create_new": "Créer une nouvelle identité",
"edit_identity": "Modifier l'identité",
"delete_confirm": "Supprimer cette identité ? Cette action est irréversible.",
"cannot_delete": "Cette identité ne peut pas être supprimée",
"primary_identity": "Principale",
"no_identities": "Aucune identité trouvée",
"display": {
"reply_to": "Répondre à :",
"bcc": "CCI :",
"signature": "Signature :",
"preview": "Aperçu :"
},
"validation_errors": {
"invalid_emails": "Emails invalides : {emails}",
"unknown_error": "Erreur inconnue"
},
"form": {
"name_label": "Nom d'affichage",
"name_placeholder": "ex: Email professionnel, Personnel",
"name_required": "Le nom est requis",
"email_label": "Adresse email",
"email_placeholder": "votre.email@exemple.com",
"email_required": "L'email est requis",
"email_invalid": "Veuillez entrer une adresse email valide",
"email_immutable": "L'adresse email ne peut pas être modifiée après création",
"reply_to_label": "Répondre à (optionnel)",
"reply_to_placeholder": "autre@email.com",
"bcc_label": "Copie cachée automatique (optionnel)",
"bcc_placeholder": "archive@email.com",
"text_signature_label": "Signature texte",
"html_signature_label": "Signature HTML",
"save": "Enregistrer l'identité",
"cancel": "Annuler",
"creating": "Création...",
"updating": "Mise à jour..."
},
"sub_address": {
"button_tooltip": "Utiliser le sous-adressage",
"popover_title": "Ajouter un tag de sous-adresse",
"tag_input_placeholder": "Entrez un tag (ex: shopping)",
"preview_label": "Aperçu:",
"recent_tags": "Tags récents",
"suggested_tags": "Suggérés",
"use_address": "Utiliser cette adresse",
"invalid_tag": "Le tag doit contenir uniquement des lettres, chiffres et tirets",
"tag_too_long": "Le tag doit faire 30 caractères ou moins",
"help_text": "Les emails envoyés à utilisateur+tag@domaine.com arriveront dans votre boîte de réception",
"validation": {
"empty": "Le tag ne peut pas être vide",
"too_long": "Le tag doit faire {max} caractères ou moins",
"invalid_chars": "Le tag doit contenir uniquement des lettres, chiffres et tirets"
}
},
"badge": {
"sent_via": "via",
"sub_address_tag": "Envoyé avec le sous-adresse: {tag}",
"identity_name": "Envoyé avec l'identité: {name}",
"identity_short": "via {name}",
"subaddress_tag": "+{tag}"
}
}
}