diff --git a/pyproject.toml b/pyproject.toml index 4433a39..3754f27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,10 @@ backup = "./src/manage.py export_backup" restore = "./src/manage.py import_backup" auto-backup = "./src/manage.py auto_backup" +# Translations +makemessages = "./src/manage.py makemessages -l fr --ignore=theme/* --ignore=staticfiles/* --ignore=venv/*" +compilemessages = "./src/manage.py compilemessages" + # Tests test = "pytest ./src --cov=src --cov-report=term-missing" test-fast = "pytest ./src -x -q" diff --git a/src/accounts/urls.py b/src/accounts/urls.py index 9335bc2..2fe029f 100644 --- a/src/accounts/urls.py +++ b/src/accounts/urls.py @@ -8,4 +8,5 @@ path("profile/", views.profile_edit, name="profile_edit"), path("profile/validate-profile/", views.validate_profile_field, name="validate_profile_field"), path("profile/validate-password/", views.validate_password_field, name="validate_password_field"), + path("set-language/", views.set_language, name="set_language"), ] diff --git a/src/accounts/views.py b/src/accounts/views.py index bdec79d..ed99c95 100644 --- a/src/accounts/views.py +++ b/src/accounts/views.py @@ -1,8 +1,10 @@ +from django.conf import settings from django.contrib import messages from django.contrib.auth import update_session_auth_hash from django.contrib.auth.decorators import login_required from django.http import HttpResponse from django.shortcuts import redirect, render +from django.utils import translation from django.utils.html import escape from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_POST @@ -22,7 +24,7 @@ def profile_edit(request): profile_form = UserProfileForm(request.POST, instance=request.user) if profile_form.is_valid(): profile_form.save() - messages.success(request, _("Your profile has been updated.")) + messages.success(request, _("Profile updated.")) return redirect("accounts:profile_edit") elif "change_password" in request.POST: password_form = CustomPasswordChangeForm(request.user, request.POST) @@ -30,15 +32,20 @@ def profile_edit(request): user = password_form.save() # Keep the user logged in after password change update_session_auth_hash(request, user) - messages.success(request, _("Your password has been changed.")) + messages.success(request, _("Password changed.")) return redirect("accounts:profile_edit") + # Get current language and available languages + current_language = translation.get_language() + return render( request, "accounts/profile_edit.html", { "profile_form": profile_form, "password_form": password_form, + "languages": settings.LANGUAGES, + "current_language": current_language, }, ) @@ -70,3 +77,21 @@ def validate_password_field(request): form = CustomPasswordChangeForm(request.user, request.POST) field_name = request.POST.get("field_name") return _validate_field_htmx(form, field_name) + + +@require_POST +@login_required +def set_language(request): + """Set the user's preferred language in session.""" + language = request.POST.get("language") + + # Validate the language code + if language and language in dict(settings.LANGUAGES): + translation.activate(language) + # Store language preference in session using Django's standard key + request.session["django_language"] = language + messages.success(request, _("Language preference updated.")) + else: + messages.error(request, _("Invalid language selection.")) + + return redirect("accounts:profile_edit") diff --git a/src/config/settings.py b/src/config/settings.py index 9dd1201..b92a059 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -75,6 +75,7 @@ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", # Must be after SessionMiddleware "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -150,6 +151,18 @@ USE_TZ = True +# Supported languages +# https://docs.djangoproject.com/en/6.0/topics/i18n/translation/#how-django-discovers-language-preference +LANGUAGES = [ + ("en", "English"), + ("fr", "Français"), +] + +# Path where Django will look for translation files +LOCALE_PATHS = [ + BASE_DIR / "locale", +] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/6.0/howto/static-files/ diff --git a/src/core/models.py b/src/core/models.py index 4656a90..f5adf14 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -5,7 +5,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.utils import timezone -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ from markdownfield.models import MarkdownField, RenderedMarkdownField from markdownfield.validators import VALIDATOR_STANDARD from partial_date import PartialDateField diff --git a/src/locale/fr/LC_MESSAGES/django.po b/src/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..5fbc24a --- /dev/null +++ b/src/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,611 @@ +# Copyright (C) 2025 Pascal Repond. +# This file is distributed under the same license as the Datakult package. +# Pascal Repond , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: 0.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-01-04 10:28+0100\n" +"PO-Revision-Date: 2026-01-04 10:45+0100\n" +"Last-Translator: Pascal Repond \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: src/accounts/views.py:27 +msgid "Profile updated." +msgstr "Profil mis à jour." + +#: src/accounts/views.py:35 +msgid "Password changed." +msgstr "Mot de passe modifié." + +#: src/accounts/views.py:93 +msgid "Language preference updated." +msgstr "Préférence de langue mise à jour." + +#: src/accounts/views.py:95 +msgid "Invalid language selection." +msgstr "Sélection de langue invalide." + +#: src/core/forms.py:56 +msgid "YYYY" +msgstr "AAAA" + +#: src/core/forms.py:62 +msgid "YYYY, MM-YYYY, or YYYY-MM-DD" +msgstr "AAAA, MM-AAAA, ou AAAA-MM-JJ" + +#: src/core/models.py:42 +#, python-format +msgid "Image file size exceeds %sMB limit." +msgstr "La taille du fichier image dépasse la limite de %s Mo." + +#: src/core/models.py:58 +#, python-format +msgid "Unsupported image format: %s. Allowed: %s" +msgstr "Format d'image non pris en charge : %s. Autorisé : %s" + +#: src/core/models.py:83 +msgid "Image is too large (possible decompression bomb attack)." +msgstr "Image trop grande (attaque par bombe de décompression possible)." + +#: src/core/models.py:85 +msgid "Invalid or corrupted image file." +msgstr "Fichier image invalide ou corrompu." + +#: src/core/models.py:99 +msgid "Name" +msgstr "Nom" + +#: src/core/models.py:113 src/templates/media_edit.html:46 +#: src/templates/partials/media-list.html:15 +msgid "Title" +msgstr "Titre" + +#: src/core/models.py:120 +msgid "Contributor" +msgstr "Contributeur" + +#: src/core/models.py:125 src/templates/media_edit.html:85 +msgid "Media type" +msgstr "Type de média" + +#: src/core/models.py:129 +msgid "Book" +msgstr "Livre" + +#: src/core/models.py:130 +msgid "Video game" +msgstr "Jeu vidéo" + +#: src/core/models.py:131 +msgid "Music" +msgstr "Musique" + +#: src/core/models.py:132 +msgid "Comic" +msgstr "BD" + +#: src/core/models.py:133 +msgid "Film" +msgstr "Film" + +#: src/core/models.py:134 +msgid "TV series" +msgstr "Série TV" + +#: src/core/models.py:135 +msgid "Show/performance" +msgstr "Spectacle/Performance" + +#: src/core/models.py:136 +msgid "Broadcast (podcast, web series, etc.)" +msgstr "Diffusion (podcast, série web, etc.)" + +#: src/core/models.py:140 src/templates/media_edit.html:94 +msgid "External URI" +msgstr "URI externe" + +#: src/core/models.py:145 +msgid "" +"Link to an external page about this media (e.g., official site, IMDb, " +"Goodreads, etc.)" +msgstr "Lien vers une page externe à propos de ce média (par exemple, site officiel, IMDb, Goodreads, etc.)" + +#: src/core/models.py:148 src/templates/media_edit.html:103 +#: src/templates/partials/media-list.html:17 +msgid "Status" +msgstr "Statut" + +#: src/core/models.py:152 +msgid "Planned" +msgstr "Prévu" + +#: src/core/models.py:153 +msgid "In progress" +msgstr "En cours" + +#: src/core/models.py:154 +msgid "Completed" +msgstr "Terminé" + +#: src/core/models.py:155 +msgid "Paused" +msgstr "En pause" + +#: src/core/models.py:156 +msgid "Did not finish" +msgstr "Pas fini" + +#: src/core/models.py:161 src/templates/media_edit.html:112 +msgid "Release year" +msgstr "Année de sortie" + +#: src/core/models.py:165 src/core/models.py:166 +msgid "Year must be between -4000 and 2100." +msgstr "L'année doit être comprise entre -4000 et 2100." + +#: src/core/models.py:170 src/templates/media_edit.html:130 +#: src/templates/partials/media-list.html:16 +msgid "Review" +msgstr "Critique" + +#: src/core/models.py:181 +msgid "Review score" +msgstr "Note de critique" + +#: src/core/models.py:185 +msgid "Detested" +msgstr "Haï" + +#: src/core/models.py:186 +msgid "Hated" +msgstr "Détesté" + +#: src/core/models.py:187 +msgid "Disliked" +msgstr "Pas aimé" + +#: src/core/models.py:188 +msgid "Not appreciated" +msgstr "Pas vraiment aimé" + +#: src/core/models.py:189 +msgid "Moderately appreciated" +msgstr "Moyennement apprécié" + +#: src/core/models.py:190 +msgid "Appreciated" +msgstr "Apprécié" + +#: src/core/models.py:191 +msgid "Enjoyed" +msgstr "Aimé" + +#: src/core/models.py:192 +msgid "Really enjoyed" +msgstr "Beaucoup aimé" + +#: src/core/models.py:193 +msgid "Loved" +msgstr "Adoré" + +#: src/core/models.py:194 +msgid "Adored" +msgstr "Coup de cœur" + +#: src/core/models.py:198 src/templates/media.html:46 +#: src/templates/media_edit.html:139 src/templates/partials/media-list.html:18 +msgid "Review date" +msgstr "Date de la critique" + +#: src/core/models.py:201 +msgid "Either a full date or just year and month, or only year." +msgstr "Soit une date complète, soit seulement l'année et le mois, ou uniquement l'année." + +#: src/core/models.py:204 src/templates/media_edit.html:36 +msgid "Cover image" +msgstr "Image de couverture" + +#: src/core/templates/widgets/cover_input.html:11 +msgid "Current cover" +msgstr "Couverture actuelle" + +#: src/core/templates/widgets/cover_input.html:20 +msgid "Delete cover" +msgstr "Supprimer la couverture" + +#: src/core/templates/widgets/cover_input.html:34 +msgid "New cover preview" +msgstr "Nouvelle couverture" + +#: src/core/templates/widgets/cover_input.html:42 +msgid "Remove" +msgstr "Supprimer" + +#: src/core/templates/widgets/cover_input.html:77 +msgid "Replace cover:" +msgstr "Remplacer la couverture :" + +#: src/core/templates/widgets/cover_input.html:79 +msgid "Upload cover:" +msgstr "Télécharger la couverture :" + +#: src/core/templates/widgets/star_rating.html:26 +msgid "Clear rating" +msgstr "Retirer la note" + +#: src/core/views.py:298 +#, python-format +msgid "Backup creation failed: %(error)s" +msgstr "Échec de la création du backup : %(error)s" + +#: src/core/views.py:309 +msgid "No file selected" +msgstr "Aucun fichier sélectionné" + +#: src/core/views.py:313 +msgid "Invalid file format. Use a .tar.gz file" +msgstr "Format de fichier invalide. Utilisez un fichier .tar.gz" + +#: src/core/views.py:328 +msgid "Backup imported successfully! All data has been restored." +msgstr "Backup importé avec succès ! Toutes les données ont été restaurées." + +#: src/core/views.py:337 +#, python-format +msgid "Backup import failed: %(error)s" +msgstr "Échec de l'importation du backup : %(error)s" + +#: src/templates/accounts/profile_edit.html:4 +#: src/templates/accounts/profile_edit.html:8 src/templates/base.html:78 +msgid "Edit Profile" +msgstr "Modifier le profil" + +#: src/templates/accounts/profile_edit.html:19 +msgid "Profile Information" +msgstr "Informations du profil" + +#: src/templates/accounts/profile_edit.html:25 +#: src/templates/registration/login.html:14 +msgid "Username" +msgstr "Nom d'utilisateur" + +#: src/templates/accounts/profile_edit.html:36 +msgid "Email" +msgstr "Email" + +#: src/templates/accounts/profile_edit.html:45 +msgid "First name" +msgstr "Prénom" + +#: src/templates/accounts/profile_edit.html:56 +msgid "Last name" +msgstr "Nom" + +#: src/templates/accounts/profile_edit.html:66 +msgid "Update Profile" +msgstr "Mettre à jour le profil" + +#: src/templates/accounts/profile_edit.html:74 +msgid "Language Preference" +msgstr "Préférence de langue" + +#: src/templates/accounts/profile_edit.html:79 +msgid "Choose your preferred language" +msgstr "Choisissez votre langue préférée" + +#: src/templates/accounts/profile_edit.html:90 +msgid "Update Language" +msgstr "Mettre à jour la langue" + +#: src/templates/accounts/profile_edit.html:98 +#: src/templates/accounts/profile_edit.html:141 +msgid "Change Password" +msgstr "Changer le mot de passe" + +#: src/templates/accounts/profile_edit.html:109 +msgid "Current password" +msgstr "Mot de passe actuel" + +#: src/templates/accounts/profile_edit.html:120 +msgid "New password" +msgstr "Nouveau mot de passe" + +#: src/templates/accounts/profile_edit.html:131 +msgid "Confirm new password" +msgstr "Confirmer le nouveau mot de passe" + +#: src/templates/accounts/profile_edit.html:147 +#: src/templates/backup_manage.html:148 +msgid "Back to Home" +msgstr "Retour à l'accueil" + +#: src/templates/backup_manage.html:5 src/templates/backup_manage.html:9 +msgid "Backup Management" +msgstr "Gestion des sauvegardes" + +#: src/templates/backup_manage.html:27 +msgid "Export Backup" +msgstr "Exporter la sauvegarde" + +#: src/templates/backup_manage.html:30 +msgid "Create a complete archive of all your data (database + media files)." +msgstr "Créer une archive complète de toutes vos données (base de données + fichiers médias)." + +#: src/templates/backup_manage.html:35 +msgid "Backup contents:" +msgstr "Contenu de la sauvegarde :" + +#: src/templates/backup_manage.html:38 +msgid "All database data" +msgstr "Toutes les données de la base de données" + +#: src/templates/backup_manage.html:39 +msgid "All media files (cover images)" +msgstr "Tous les fichiers médias (images de couverture)" + +#: src/templates/backup_manage.html:40 +msgid "Metadata (version, creation date)" +msgstr "Métadonnées (version, date de création)" + +#: src/templates/backup_manage.html:48 +msgid "Download Backup" +msgstr "Télécharger la sauvegarde" + +#: src/templates/backup_manage.html:58 src/templates/backup_manage.html:95 +msgid "Import Backup" +msgstr "Importer la sauvegarde" + +#: src/templates/backup_manage.html:60 +msgid "Restore your data from a backup archive." +msgstr "Restaurer vos données à partir d'une archive de sauvegarde." + +#: src/templates/backup_manage.html:71 +msgid "WARNING:" +msgstr "ATTENTION :" + +#: src/templates/backup_manage.html:72 +msgid "Importing a backup will" +msgstr "L'importation d'une sauvegarde va" + +#: src/templates/backup_manage.html:73 +msgid "DELETE ALL your current data" +msgstr "SUPPRIMER TOUTES vos données actuelles" + +#: src/templates/backup_manage.html:74 +msgid "and replace it with the backup data." +msgstr "et les remplacer par les données de la sauvegarde." + +#: src/templates/backup_manage.html:80 +msgid "" +"⚠️ WARNING: This action will DELETE ALL your current data and replace it with " +"the backup data.\\n\\nAre you absolutely sure you want to continue?" +msgstr "⚠️ ATTENTION : Cette action va SUPPRIMER TOUTES vos données actuelles et les remplacer par les données de la sauvegarde.\\n\\nÊtes-vous absolument sûr de vouloir continuer ?" + +#: src/templates/backup_manage.html:84 +msgid "Select a backup file (.tar.gz)" +msgstr "Sélectionnez un fichier de sauvegarde (.tar.gz)" + +#: src/templates/backup_manage.html:107 +msgid "Backup Information" +msgstr "Informations sur la sauvegarde" + +#: src/templates/backup_manage.html:112 +msgid "Backup Format" +msgstr "Format de la sauvegarde" + +#: src/templates/backup_manage.html:114 +msgid "Backups are compressed archives in" +msgstr "Les sauvegardes sont des archives compressées au format" + +#: src/templates/backup_manage.html:116 +msgid "format containing all your data and files." +msgstr "contenant toutes vos données et fichiers." + +#: src/templates/backup_manage.html:120 +msgid "Automatic Backups" +msgstr "Sauvegardes automatiques" + +#: src/templates/backup_manage.html:122 +msgid "" +"An automatic backup is created daily at 1:00 AM. The 7 most recent backups " +"are kept, older ones are automatically deleted." +msgstr "Une sauvegarde automatique est créée quotidiennement à 1h00. Les 7 sauvegardes les plus récentes sont conservées, les plus anciennes sont automatiquement supprimées." + +#: src/templates/backup_manage.html:126 +msgid "Automatic Backup Location" +msgstr "Emplacement de la sauvegarde automatique" + +#: src/templates/backup_manage.html:128 +msgid "In production (Docker):" +msgstr "En production (Docker) :" + +#: src/templates/backup_manage.html:130 +msgid "In development:" +msgstr "En développement :" + +#: src/templates/backup_manage.html:134 +msgid "Best Practices" +msgstr "Bonnes pratiques" + +#: src/templates/backup_manage.html:136 +msgid "Regularly download your backups to external storage" +msgstr "Téléchargez régulièrement vos sauvegardes sur un stockage externe" + +#: src/templates/backup_manage.html:137 +msgid "Test your backups periodically in a test instance" +msgstr "Testez périodiquement vos sauvegardes dans une instance de test" + +#: src/templates/backup_manage.html:138 +msgid "" +"Keep multiple backups to be able to restore from different points in time" +msgstr "Conservez plusieurs sauvegardes pour pouvoir restaurer à partir de différents points dans le temps" + +#: src/templates/backup_manage.html:172 +msgid "Backup creation failed. Please try again." +msgstr "Échec de la création de la sauvegarde. Veuillez réessayer." + +#: src/templates/backup_manage.html:172 +msgid "Unknown error" +msgstr "Erreur inconnue" + +#: src/templates/base.html:40 +msgid "System" +msgstr "Système" + +#: src/templates/base.html:47 +msgid "Light" +msgstr "Clair" + +#: src/templates/base.html:54 +msgid "Dark" +msgstr "Sombre" + +#: src/templates/base.html:84 +msgid "Backups" +msgstr "Sauvegardes" + +#: src/templates/base.html:93 +msgid "Log Out" +msgstr "Se déconnecter" + +#: src/templates/media.html:3 src/templates/media.html:54 +msgid "Search" +msgstr "Recherche" + +#: src/templates/media.html:8 +msgid "My media" +msgstr "Mes médias" + +#: src/templates/media.html:44 +msgid "Creation date" +msgstr "Date de création" + +#: src/templates/media.html:47 src/templates/media_edit.html:121 +msgid "Score" +msgstr "Note" + +#: src/templates/media.html:65 +msgid "Add" +msgstr "Ajouter" + +#: src/templates/media_edit.html:6 src/templates/media_edit.html:29 +#: src/templates/partials/media-edit-button.html:6 +#: src/templates/partials/media-edit-button.html:7 +msgid "Edit" +msgstr "Modifier" + +#: src/templates/media_edit.html:8 src/templates/media_edit.html:31 +msgid "Add media" +msgstr "Ajouter un média" + +#: src/templates/media_edit.html:25 +msgid "Close" +msgstr "Fermer" + +#: src/templates/media_edit.html:55 +msgid "Contributors" +msgstr "Contributeurs" + +#: src/templates/media_edit.html:69 +msgid "Type name to search" +msgstr "Tapez le nom à rechercher" + +#: src/templates/media_edit.html:153 +msgid "" +"Are you sure you want to delete this media? This action cannot be undone." +msgstr "Êtes-vous sûr de vouloir supprimer ce média ? Cette action est irréversible." + +#: src/templates/media_edit.html:155 +msgid "Delete" +msgstr "Supprimer" + +#: src/templates/media_edit.html:160 +msgid "Save" +msgstr "Enregistrer" + +#: src/templates/partials/contributors-suggestions.html:17 +msgid "No result, press Enter to add" +msgstr "Aucun résultat, appuyez sur Entrée pour ajouter" + +#: src/templates/partials/filters.html:12 +msgid "All types" +msgstr "Tous les types" + +#: src/templates/partials/filters.html:25 +msgid "All statuses" +msgstr "Tous les statuts" + +#: src/templates/partials/filters.html:39 +msgid "All scores" +msgstr "Toutes les notes" + +#: src/templates/partials/filters.html:44 +msgid "Not rated" +msgstr "Non noté" + +#: src/templates/partials/filters.html:48 +msgid "Reviewed" +msgstr "Noté" + +#: src/templates/partials/filters.html:59 +msgid "to" +msgstr "à" + +#: src/templates/partials/load-more-trigger.html:11 +#: src/templates/partials/load-more-trigger.html:20 +msgid "Loading more..." +msgstr "Chargement..." + +#: src/templates/partials/media-cover.html:6 +#: src/templates/partials/media-items.html:11 +msgid "Cover of" +msgstr "Couverture de" + +#: src/templates/partials/media-list.html:14 +msgid "Cover" +msgstr "Couverture" + +#: src/templates/partials/media-list.html:19 +msgid "Actions" +msgstr "Actions" + +#: src/templates/partials/media-list.html:31 +msgid "No media found." +msgstr "Aucun média trouvé." + +#: src/templates/partials/media-review-clamped.html:8 +msgid "See more" +msgstr "Voir plus" + +#: src/templates/partials/media-review-full.html:7 +msgid "See less" +msgstr "Voir moins" + +#: src/templates/partials/spinner.html:17 +msgid "Loading..." +msgstr "Chargement..." + +#: src/templates/partials/view-mode-toggle.html:15 +msgid "List" +msgstr "Liste" + +#: src/templates/partials/view-mode-toggle.html:24 +msgid "Grid" +msgstr "Grille" + +#: src/templates/registration/login.html:4 +#: src/templates/registration/login.html:11 +#: src/templates/registration/login.html:24 +msgid "Log in" +msgstr "Se connecter" + +#: src/templates/registration/login.html:21 +msgid "Password" +msgstr "Mot de passe" diff --git a/src/templates/accounts/profile_edit.html b/src/templates/accounts/profile_edit.html index afa897d..66c863f 100644 --- a/src/templates/accounts/profile_edit.html +++ b/src/templates/accounts/profile_edit.html @@ -68,6 +68,30 @@

{% translate "Profile Information" %}

+ +
+
+

{% translate "Language Preference" %}

+
+ {% csrf_token %} +
+ + +
+
+ +
+
+
+
diff --git a/src/templates/partials/filters.html b/src/templates/partials/filters.html index 66f7825..488fd50 100644 --- a/src/templates/partials/filters.html +++ b/src/templates/partials/filters.html @@ -39,7 +39,7 @@ {% for value, label in score_choices reversed %} + {% if filters.score == value|stringformat:"d" %}selected{% endif %}>{{ value }}⭐ - {{ label }} {% endfor %} diff --git a/src/tests/accounts/test_views.py b/src/tests/accounts/test_views.py index 4ceecc0..18a96a2 100644 --- a/src/tests/accounts/test_views.py +++ b/src/tests/accounts/test_views.py @@ -120,3 +120,25 @@ def test_user_stays_logged_in_after_password_change(self, logged_in_client): # Check user is still authenticated by accessing a protected page response = logged_in_client.get(reverse("accounts:profile_edit")) assert response.status_code == 200 + + +class TestSetLanguageView: + """Tests for the set_language view.""" + + def test_set_valid_language(self, logged_in_client): + """Setting a valid language saves it in session.""" + url = reverse("accounts:set_language") + response = logged_in_client.post(url, {"language": "fr"}) + + assert response.status_code == 302 + assert logged_in_client.session["django_language"] == "fr" + + def test_set_invalid_language(self, logged_in_client): + """Setting an invalid language is rejected.""" + url = reverse("accounts:set_language") + logged_in_client.post(url, {"language": "invalid"}) + + assert ( + "django_language" not in logged_in_client.session + or logged_in_client.session["django_language"] != "invalid" + )